20 stycznia 2009

Materiały z prezentacji o iBATISie na Warszawa JUG

Prezentacja odbyła się zgodnie z planem: całość trwała niespełna 2 godziny, były slajdy - które zgodnie z obietnicą publikuję (tu) -, były przykłady na żywo, była dyskusja. Chyba nawet udało mi się uzasadnić, że iBATIS w określonych sytuacjach jest lepszym wyborem niż Hibernate czy JPA. Kolega z widowni uzasadnił też przy okazji, podając przykłady, że i JDBC bywa dobrym wyborem, jeśli tylko natrafimy na sytuację wymagającą zastosowanie wyrafinowanych mechanizmów. Mam nadzieje, że się podobało.

14 komentarzy:

Piotr Pietrzak pisze...

Zacznę od tego, że bardzo się podobało. Gratuluje prezentacji - zapanowałeś nad tłumem i skutecznie gasiłeś dygresje. Po prostu PRO.

Podczas prezentacji zadałem pytanie dotyczące bezpieczeństwa wątkowego iBatis. Podczas prezentacji nie chciałem zanudzać, ale tu możemy wrócić do tematu.

Mysql i J/Connector:
JConnector nie musi być i nie jest bezpieczny wątkowo (wyczytałem to na gdzieś na forach mysql), czyli mogą pojawić się problemy jeśli w jednej sesji wykonujemy wiele zapytań w różnych wątkach.

Pokazywałeś wczoraj przykład w którym ibatis wykonywał Insert a potem wczytywał ostatnie nadane id. Jeśli blok w którym wykonywane są te dwa zapytania jest bezpieczny wątkowo (blokuje również dostęp do połączenia z MySQL), to OK, ale jeśli nie, to jest możliwość wykonania Inserta i otrzymania błędnych id.

Mistrzem wielowątkowości nie jestem, ale nie zrozumiałem wczoraj tłumaczenia kolegi, który odwolał się do JDBC i powiedział, ze to problem sterownika i transakcji.

Problem jest taki, że starsze wersje mysql (nie wszyscy używają >5.0) nie mają transakcji i te dwa zapytania nie są ze sobą w żaden sposób powiązane, zatem dla bazy danych widoczne są 4 zapytania z czego 2 inserty mogą być wywołane w dowolnej kolejności a potem zwócone 2 id w dowolnej kolejności (co prowadzi do błedu).
Takie zachowanie w sumie pewnie nie jest też niespodzianką, bo drivery JDBC nie muszą być bezpieczne wątkowo. Chodzi mi o to, że skoro iBatis używa pojedyńczego połączenia, to czy zapewnia bezpieczeństwo na własną rękę?

Wielowątkowość w dostępie do bazy to jest coś co słabo rozumiem i nawet jeśli coś pokręciłem, to mam okazje zrozmieć lepiej ten temat. Zatem moje pytanie jest bardziej pytaniem o tym co się stanie jak się wykonuje zapytania w jednym połączeniu w różnych wątkach, niż konkretnym pytaniem o mechanizmy iBatis (choć jeśli jest tak jak myślę, to rozwiązanie tego typu może sobie poradzić z opisanymi wyżej problemami).

Mariusz Lipiński pisze...

Cieszę się, że wypadło dobrze:) Co do twojego pytania - niestety nie mam gotowej odpowiedzi, ale zmobilizowałeś mnie żeby troszkę poszukać. Poszukam więc i napiszę, co udało mi się znaleźć.

Teraz odpowiem natomiast na inne pytanie, które padło w czasie prezentacji. Wspomniałem, że iBATIS może zwracać jako rezultat wykonania zapytania SQL dokument XML w postaci Stringa. Padło pytanie, czy da się też w drugą stronę, tj. czy iBATIS potrafi pobrać dane z XMLa aby sparametryzować nimi zapytanie SQL. Odpowiedź brzmi TAK, może, potrafi. Co więcej, XML może być nie tylko w formie String'a, ale także w formie obiektu DOM. Więcej na ten temat... w książce "iBATIS in Action":)

Mariusz Lipiński pisze...

OK, znalazłem "odpowiedź", książka "iBATIS in Action" okazuje się być lepsza niż myślałem. Pozwolę sobie zacytować jej odpowiednie fragmenty:

"The first approach is to fetch the key after you have inserted the record and
the database has generated the key. Be aware that you will need to ensure that the
driver you are using is guaranteed to return the key generated by the last insert
statement you performed. For example, if two threads execute insert statements
nearly simultaneously, the order of the execution could be [insert for user #1],
[insert for user #2], [selectKey for user #1], [selectKey for user #2]. If the
driver simply returns the last-generated key (globally), then the [selectKey for
user #1] will get the generated key for user #2, which would wreak havoc in the
application. Most drivers should work fine for this, but be sure to test this if you
are not absolutely certain whether yours does or not. (...) The second approach to consider is fetching the key before you insert the record."

Czyli mówiąc krótko twoje obawy zostały potwierdzone. Jednocześnie zostało potwierdzone, że rozwiązaniem jest uprzednie wygenerowanie klucza i następnie użycie go w poleceniu INSERT. Przykład:

<insert id="insert">
<selectKey
keyProperty="accountId"
resultClass="int">
SELECT nextVal('account_accountid_seq')
</selectKey>
INSERT INTO Account (
accountId, username, password
) VALUES(
#accountId#, #username#, #password#)
</insert>

To dodatkowo odpowiada na inne pytanie, zadane w czasie prezentacji. Zgadza się - iBATIS potrafi sam przypisać wygenerowany klucz do identyfikatora w klasie. Dzieje się to za sprawą atrybutu keyProperty="accountId". Tak więc po pobraniu z sekwencji klucz jest przypisywany do atrybutu "accountId" w klasie i stamtąd pobierany i używany w poleceniu INSERT.

Piotr Pietrzak pisze...

WOW! Czas reakcji imponujący!
Wracając do tematu - trochę to rozwiązanie (cytowane z iBatis in action) mi się nie podoba. Jeśli dobrze zrozumiałem, to wygenerowane będą znów dwa zapytania: pobranie kolejnej wartości z sekwencji i wstawienie do tabelki drugim insertem. Można uniknąć tego typu operacji , jeśli zapiszemy coś takiego w procedurze składowanej (lub raczej funkcji,która zwraca nasze id).
I właśnie w tym momencie iBatis pokazuje co potrafi - bardzo ładne mapowanie w jedną i drugą stronę z procedur składowanych nawet pewnie więcej niż jednej. Sprawdzę to w praktyce jak znajdę chwilę, ale na razie z tego co widziałem na prezentacji iBatis to może być to, czego mi brakowało.
W aplikacjach, gdzie baza stanowi serce aplikacji procedury składowane są znacznie bardziej rozbudowane i często trzeba wykonywać zapytania, których po prostu nie ma jak zmapować przez JPA, czy Hibernate. Nie wiem, czy iBatis jest lekarstwem na wszystkie bolączki, ale zachęciłeś mnie do poznania iBatisa (a to było celem prezentacji).

Kordzik pisze...

Witam,

przylaczam sie do opinii Piotra - prezentacja byla bardzo interesujaca i profesjonalnie przeprowadzona. Najlepszym dowodem jest fakt, ze nie zmruzylem oka nawet przez chwile, co rzadko mi sie zdarza, gdy o takiej porze uczestnicze w wykladzie;)

Podsumowujac problem poruszony przez Piotra: jesli mamy baze danych nie wspierajaca transakcji, to trzeba kombinowac, tak jak to przedstawil Mariusz w ostatniej odpowiedzi. W kazdym innym przypadku mozna zastosowac konstrukcje pobierajaca ID po insercie, o ile odpowiednio skonfigurujemy polaczenie (czyli autoCommit na false i odpowiedni poziom izolacji transakcji, zdaje sie ze wystarczy READ_COMMITED...?)

BTW brak wsparcia dla transakcji to chyba generyczny problem, dotykajacy wszystkie tego typu framework'i. Przeciez Hibernate tez musi robic selecty zeby ustawic ID dodawanych do bazy obiektow?

Piotr Pietrzak pisze...

Brak wsparcia transakcji po stronie bazy danych to faktycznie generyczny problem, ale moja teza jest taka, że pomimo braku wsparcia transakcji framework taki jak iBatis może sobie poradzić z takim problemem ponieważ to on nadzoruje wyknanie zapytań i posiada dostęp do połączenia. Wystarczy zablokować dostęp do połączenia na czas wykonania zapytania wstawiającego dane az do momentu wczytania id. Takie zachowanie dla iBatisa nie powinno sprawiać problemu, bo z tego co pamiętam z prezentacji z definicji mapowania iBatis może wywnioskować, że właśnie to autor miał na myśli, a jeśli by chciał uzyskać losowe id to jest Math.random().

Mariusz Lipiński pisze...

Można naturalnie użyć procedur składowanych, aczkolwiek pobranie identyfikatora z sekwencji przed wykonaniem INSERTa jest bezpieczne, nawet jeśli nie mamy transakcji, nawet jeśli wystąpią "niespodziewane" przeploty w kolejności wykonywania zapytań. Jeśli chodzi o możliwości iBATISa to są one naprawdę duże. Przypominam sobie, że osoba która broniła JDBC podczas prezentacji argumentowała, że czasem trzeba przetwarzać rekordy otwartego kursora jeden po drugim, bez pobierania całości danych na raz i tworzenia dla nich modelu obiektowego - to iBATIS też potrafi. Szczegóły w "iBATIS in Action":)

Piotr Pietrzak pisze...

Nie chodziło mi o to, że nie jest bezpieczne, tylko o to, że bez sensu jest przesyłane z serwera do klienta tylko po to, by klient przesłał na serwer. To trochę jak wyprawa na Targówek przez Wiedeń w celu ominięcia Wisły.
Chyba powinienem zaprzestać trollowania:)

Adaslaw pisze...

Odnośnie odczytywania wygenerowanych wartości kluczy głównych i - generalnie - o generowaniu kluczy.

1. Nie jest do końca prawdą, że np. Hibernate korzystający z sekwencji musi przed każdym INSERT-em zrobić SELECT do bazy, aby otrzymać kolejną wartość z sekwencji. Zarówno same sekwencje bazodanowe jak i Hibernate idzie tak użyć, aby pobierał wartości ID paczkami (np. rezerwować 10 lub 100 unikalnych wartości z sekwencji, a następnie korzystanie z tej puli, aż do jej wyczerpania). I tak np. jeśli pobieramy identyfikatory z sekwencji paczkami po 10 sztuk, to chcąc wykonać 10 INSERT-ów wykonamy jedynie 11 zapytań (1 zapytanie o paczkę sekwencji + 10 pojedynczych INSERT-ów), a nie 20 zapytań (10 zapytań po 10 kolejnych wartości z sekwencji + 10 pojedynczych INSERT-ów).

2. Ten mechanizm, gdzie pytając się o wartość wygenerowanego ID dostajemy w odpowiedzi ostatnią *globalnie* wartość otrzymaną z sekwencji jest kompletnie bez sensu i niczego nie gwarantuje. To się do niczego nie nadaje (nie możemy w żaden sposób polegać na wartościach zwracanych przez ten mechanizm). Jednym z eleganckich i wydajnych sposobów radzenia sobie z problemem dowiadywania się o wygenerowanych wartościach ID jest mechanizm JDBC:
Statement.getGeneratedKeys

Obawiam się (czy o ile pamiętam) ten mechanizm nie jest jeszcze zaimplementowany we wszystkich sterownikach JDBC.

Adaslaw pisze...

Wielowątkowość i JDBC:
O ile mnie pamięć nie myli to współdzielenie pojedynczego połączenia JDBC pomiędzy wieloma wątkami (bez dodatkowej synchronizacji) nie jest dobrym pomysłem. W sumie możliwe, że to jest antipattern, bo o ile pamiętam, JDBC nie ma obowiązku gwarantować, że instancja Session ma być multithreaed-safe.

Mariusz Lipiński pisze...

Przecież po to są właśnie pule połączeń, żeby różne wątki wykorzystywały różne połączenia (w sposób wydajny). Wątek pobiera połączenie z puli, używa go, po czym oddaje i dopiero wtedy inny wątek może zacząć używać tego samego połączenia.

Adaslaw pisze...

Mariusz:
Zgadza się, w takich sytuacjach świetnie sprawują się pule połączeń.

A napisałem ten komentarz widząc komentarz (pierwszy) Piotra, gdzie pisze o jakichś pokracznych modelach współdzielenia Session pomiędzy wątkami.

PS. Żałuję, że nie mogłem być na Twojej prezentacji na MIMUW (choroba mnie złamała).

Piotr Pietrzak pisze...

Miałem już nie trolować, ale skoro Cie nie było, to napiszę o co chodziło. Podczas prezentacji zapytałem o bezpieczeństwo wątkowe w pewnej sytuacji (chodzi o to całe wstawianie i wyciąganie id). Na początku omawialiśmy konfigurację w której iBatis łączy się z bazą po JDBC bez puli połączeń i to jest standardowa konfiguracja. Mariusz również wspomniał o tym, że konfiguracja iBatisa zajmuje sporo czasu, więc lepiej ją sobie gdzieś tam statycznie udostępnić.

Zgadzam się - pomysł z jednym połączeniem jest bez sensu,ale w standardowej konfiguracji iBatis właśnie tak działa - nie zapewnia bezpieczeństwa wątkowego na własną rękę, ale za to zachęca do stosowania statycznej referencji do swojej jakiejś tam konstrukcji, która to zapewnia mapowanie i dostęp do JDBC. Dając w sumie dostęp do niebezpiecznej wątkowo pojedynczej sesji JDBC, co z kolei jest błędem w projektowaniu wielowątkowym zwanym "publikacją i ucieczką".

Cała dyskusja jest bardziej akademicka niż praktyczna i nie jest też tak, że osobiście lansuje jakiś dziwaczny model współdzielenia sesji bazodanowych. Tak się tego nie robi, ale można zrobić to dobrze i nie kosztuje to wiele.

Zobacz jaką masz niespodziankę i jak długo szuka się błędu, gdy stanie się to co opisałem, a dla twórców rusztowania stworzenie zabezpieczenia to dodanie prostego mechanizmu (zwiększającego tym samym elegancję, bo dla puli połączeń konfliktów nie będzie),a dla początkującego programisty to godziny czasu poświęconego na śledzenie niezbyt trywialnego błędu, który w prostych warunkach laboratoryjnych może się nigdy nie pojawić.

Nie ma co dalej drążyć tematu - wszystko jasne. Ja również dowiedziałem się czegoś nowego. Na co dzień w pracy używam albo JPA(głównie z Hibernate), albo JDBC(ze Springiem) i na starcie mam pule połączeń, wiec nie jest tak, że jestem jakimś maniakiem hakowania wielowątkowego w oparciu o niebezpieczny wątkowo JDBC. Po prostu chciałem sobie potrolować i dowiedzieć się czemu się tak nie robi i tyle.

Kordzik pisze...

Hej, widze ze temat juz sie rozwinal daleko i gleboko, wiec juz nie bede odpowiadal do kazdej wypowiedzi (a kusi:), tylko 'z urzedu' dokonam drobnej korekty ostatniej wypowiedzi Piotra. Otoz Piotrze, musze zaprzeczyc stanowczo, ze iBatis wspiera jakies pokrecone, niesynchroniczne uzywanie polaczen do bazy. Po prostu Mariusz przelecial po temacie dostepnych konfiguracji dosc szybko, zeby wyrobic sie z calym zaplanowanym materialem, i wspomniana konfiguracja zrodla danych typu JDBC nie zostala w pelni wyjasniona. Otoz, w skrocie, nie chodzi tutaj o dostep do bazy przez pojedyncze polaczenie JDBC, tylko wlasna implementacje prostego connection pool'a (wlasna, tzn. zespolu iBatis). Mamy tutaj zatem jak najbardziej wiele polaczen!!! Po szczegoly odsylam do wspomnianego przez Mariusza Developer Guide (http://ibatis.apache.org/javadownloads.cgi), ok. str. 13. Dodatkowo polecam zajrzec w JavaDoc'a iBatis'a, w szczegolnosci kontrakt interfejsu SqlMapClient. Wynika z niego jasno, ze sa 2 opcje uzycia:
1) domyslna (wywolujemy metody tupu startTransaction(), update(), insert(), commitTransaction(), endTransaction() itp. - w tym przypadku sesja (polaczenie) jest ukryta przed uzytkownikiem i automatycznie zarzadzana z zapewnionym bezpieczenstwem wielowatkowym (w praktyce implementacja, o ile sie nie myle, tworzy sesje w przypadku jej braku i binduje do aktualnego watku z uzyciem ThreadLocal) - i do tej opcji odnosza sie 'propozycje' korzystania z SqlMapCLient'a jako singleton'a - w tle dla kazdego wywolania (z osobnego watku) tworzy on korespondujaca sesje, wiec nie ma zadnego zagrozenia
2) bezposrednie manipulowanie na sesji - przez metody openSession(), czy getSession(), wtedy klient SqlMapClient'a, czyli deweloper, moze, jak sie bardzo postara, namieszac, np. przez otwarcie sesji za pomoca openSession i zrobienie z niej niesychronizowanego singletona.

Wydaje mi sie, ze pierwotny problem, ktory poruszyles juz na prezentacji, ma charakter bardziej jednoczesnego dostepu do bazy danych (i to niekoniecznie przez jedna aplikacje, rownie dobrze podczas insertow robionych na bazie przez nasza aplikacje moze sie wtegowac miedzy wodke a zakaske jakis reczny insert, czy tez insert z innej aplikacji), a nie wielowatkowosci. Dlatego wydaje mi sie, ze wspieranie przez baze transakcji, i odpowiednie tego wykorzystanie, rozwiazuje problem.

To tyle mojego trolowania, ja tez juz koncze temat:)