08 lutego 2007

Java Persistence - 3.4 Optymistyczne blokowanie i współbieżność

Rozdział 3.4 Optymistyczne blokowanie i współbieżność rozpoczyna się dywagacjami architektonicznymi obsługi wielodostępności do danych w bazach danych. Specyfikacja JPA zakłada użycie optymistycznego blokowania (ang. optimistic locking), tj. konfiguracji bazy danych, z którymi jednostki utrwalania (ang. persistence units) są związane, o poziomie izolacji transakcji read committed (lub analogicznej - ciekawe wyjaśnienia różnych poziomów izolacji można znaleźć w dokumnetacji wybranej bazy danych, ale mnie przypadł do gustu materiał Filipa Sielimowicza Poziomy izolacji transakcji) oraz że zapisy do bazy danych zazwyczaj występują jedynie podczas wywołania metody flush (wprost przez aplikację, bądź zgodnie z ustawieniem trybu zapisu kontekstu utrwalania - synchronizacji kontekstu, cf. FlushModeType). Jeśli transakcja jest aktywna, dostawca JPA ma prawo zapisu zmian encji natychmiast, jednakże ten sposób konfiguracji nie jest w zakresie specyfikacji (domyślnie zapis zmian następuje podczas zatwierdzenia transakcji, nawet korzystając z przedłużonego kontekstu utrwalania, który potrafi przekraczać granice poszczególnych transakcji). Konfiguracja pesymistycznego blokowania (ang. pessimistic locking) może wymagać podniesienia izolacji danych na poziomie konfiguracji bazy danych i również nie jest tematem specyfikacji.

Optymistyczne blokowanie

Optymistyczne blokowanie jest techniką zapewniającą, że modyfikacje w bazie danych odpowiadające stanowi encji są wykonane jedynie, kiedy żadna inna transakcja nie zmodyfikowała danych w międzyczasie, tj. od ostatniego odczytania stanu encji nie nastąpiła modyfikacja jego reprezentacji w bazie danych. Technika optymistycznego blokowania zapewnia, że modyfikacja (w szczególności skasowanie) danych jest spójna ze stanem bazy danych i że zatwierdzone modyfikacje innych transakcji nie są stracone. Transakcja powodująca modyfikację danych nieaktualnych powoduje wystąpienie wyjątku OptimisticLockException, a w rezultacie swoje wycofanie. Przenośne aplikacje, które chcą skorzystać z optymistycznego blokowania dla encji, muszą określić wersjonowany atrybut encji, tj. skorzystać z adnotacji @Version lub skorzystać z odpowiedniej konfiguracji w deskryptorze XML. Zaleca się, aby aplikacje uaktywniły optymistyczne blokowanie (poprzez wersjonowane atrybuty) dla wszystkich encji, które będą używane współbieżnie lub, których stan będzie uwspólniany ze stanu odłączonego (metoda merge). Brak wykorzystania wersjonowanych atrybutów w encjach może prowadzić do niepoprawnego działania aplikacji (niespójny stan encji, nadpisywanie/utrata zmian, etc.) i jednocześnie przenosi odpowiedzialność za zarządzanie spójnością danych bezpośrednio na aplikację.

Wersjonowane atrybuty

Pole lub właściwość (dalej zwane atrybutami) encji udekorowane adnotacją @Version służy dostawcy JPA do wykonywania optymistycznego blokowania. Dostawca korzysta z wersjonowanego atrybutu w trakcie wykonywania operacji wpływających na cykl rozwojowy encji. Wprowadzenie wersjonowanego atrybutu w encji powoduje uaktywnienie obsługi optymistycznego blokowania dla encji. Ustawianie i modyfikacja wartości wersjonowanego atrybutu leży wyłącznie w gestii dostawcy JPA (istnieje jednak odstępstwo od tego wymagania opisane w rozdziale 4.10), jednakże możliwy jest odczyt jego wartości bądź upublicznienie (za pomocą metody odczytującej - getter).

Wersjonowany atrybut jest modyfikowany przez dostawcę JPA podczas każdorazowego zapisu do bazy danych. Wszystkie atrybuty (tj. pola i właściwości) encji nie będące stroną w jakiejkolwiek relacji i wszystkie relacje, której stroną inicjującą jest ta encja, są włączone w mechanizm kontroli wersji (w zasadzie powinienem napisać, że w mechanizm optymistycznego blokowania).

Implementacja operacji włączania encji do kontekstu utrwalania (metoda merge) jest zobowiązana do sprawdzenia wartości wersjonowanego atrybutu i zakończyć się wyjątkiem OptimisticLockException, jeśli wersja dołączanej encji jest nieaktualna, tj. kiedy encja została zmodyfikowana od czasu jej odłączenia (wyjątek może zostać rzucony dopiero podczas operacji flush lub podczas zatwierdzania transakcji, którykolwiek nastąpi pierwszy).

Dostawca JPA korzysta z wersjonowanych atrybutów podczas kontroli optymistycznego blokowania. Możliwe są inne mechanizmy obsługi optymistycznego blokowania, jednakże nie są one wymagane przez specyfikację.

Wyłącznie encje wersjonowane są kontrolowane pod względem optymistycznego blokowania. Spójność grafu zależności encji nie jest gwarantowany (ze względu na technikę optymistycznego blokowania) w przypadku korzystania z encji wersjonowanych i niewersjonowanych, jednakże nie wpływa to na wykonanie operacji (chyba, że wystąpi błąd weryfikacji wersjonowanych atrybutów encji).

Przeglądając dokumentację Javadoc dla adnotacji @Version doszukałem się jeszcze wymagania, że encja może posiadać wyłącznie jednen atrybut wersjonowany (innymi słowy: wyłącznie pojedyńcze pole/właściwość może zostać oznaczone jako wersjonowane). Korzystanie z wielu atrybutów wersjonowanych może powodować nieprzenośność encji. Dodatkowo, atrybut wersjonowany powinien być mapowany do podstawowej tabeli encji, gdyż w przeciwnym przypadku może spowodować nieprzenośność encji.

Wspierane typy atrybutów wersjonowanych to: int, Integer, short, Short, long, Long, Timestamp.

Tryby blokowania

Poza optymistycznym blokowaniem, do zarządzania spójnością danych encji, możliwe jest skorzystanie z metody lock instancji EntityManager.

Wyróżniamy dwa tryby blokad (wartości typu wyliczeniowego LockModeType):
  • READ - blokowanie na odczyt
  • WRITE - blokowanie na zapis
Jeśli transakcja T1 wywołuje lock z blokadą READ na instancji encji wersjonowanej (użycie adnotacji @Version), zarządca encji (EntityManager) musi zapewnić, że nie wystąpią następujące sytuacje:
  • P1 (odczyt niezatwierdzonych danych, ang. dirty read) - transakcja T1 modyfikuje wiersz. Inna transakcja T2 odczytuje dane wiersza wraz z modyfikacjami zanim T1 zostanie zatwierdzona lub wycofana. Transakcja T2 ostatecznie zostanie zatwierdzona (bez względu na wynik działania T1 i momentu zatwierdzenia/wycofania w stosunku do T2).
  • P2 (niepowtarzalny odczyt, ang. non-repeatable read) - transakcja T1 odczytuje wiersz. Inna transakcja T2 modyfikuje lub kasuje wiersza, zanim T1 zostanie zatwierdzona. Obie transakcje zostają zatwierdzone.
Jest to najczęściej zaimplementowane za pomocą nałożenia blokady na wiersz w bazie danych. Możliwe jest nałożenie blokady natychmiast (zakładając, że będzie utrzymywany do czasu zatwierdzenia transakcji) lub z opóźnieniem, do momentu zatwierdzania transakcji (wtedy musi być utrzymywany do czasu zakończenia zatwierdzenia transakcji). Dowolna implementacja jest akceptowalna przez specyfikację.

Dostawca JPA nie jest zobowiązany dostarczać obsługę blokady READ na niewersjonowanych encjach. Brak wsparcia musi zakończyć się wyjątkiem PersistenceException. Wsparcie dla READ musi zawsze zapobiegać wystąpieniu w/w sytuacji (P1 oraz P2). Poleganie na możliwości zakładania blokad READ na niewersjonowanych encjach powoduje nieprzenośność aplikacji.

Jeśli transakcja T1 wywołuje lock z blokadą WRITE na instancji encji wersjonowanej, zarządca encji musi również zapobiec wystąpieniu w/w sytuacji (P1 oraz P2) i obowiązkowo uaktualnić (zmodyfikować) atrybut wersjonowany. Modyfikacja wersji może nastąpić natychmiast, lub z opóźnieniem, w momencie wywołania flush bądź zatwierdzania transakcji. Jeśli encja zostanie skasowana zanim nastąpi uaktualnienie wersji, uaktualnienie zostanie zignorowane (skoro nie ma odpowiadającego jemu wiersza w bazie danych).

Dostawca JPA nie jest zobowiązany dostarczać obsługę blokady WRITE na niewersjonowanych encjach. Brak wsparcia musi zakończyć się wyjątkiem PersistenceException. Wsparcie dla WRITE musi zawsze zapobiegać wystąpieniu w/w sytuacji. Poleganie na możliwości zakładania blokad WRITE na niewersjonowanych encjach powoduje nieprzenośność aplikacji.

Specyfikacja zezwala, aby dla wersjonowanych encji, obsługa blokady READ była realizowana za pomocą mechanizmu WRITE, ale nie na odwrót.

Jeśli wersjonowana encja jest w jakikolwiek sposób zmodyfikowana bądź skasowana, dostawca JPA musi zapewnić, że wymagania związane z blokadą WRITE są spełnione, nawet bez bezpośredniego wywołania metody lock.

Dla zapewnienia przenośności aplikacji, zaleca się wyłącznie korzystanie z metody lock w celu zapewnienia poprawności powtarzanych odczytów dla encji, które nie została zmodyfikowane lub skasowane i nie poleganiu na specyficznych dla dostawcy JPA rozszerzeniach. Jednakże, jeśli dostawca JPA nałożył już pesymistyczne blokady na wybrane wiersze, wywołanie lock z blokadą READ na instancjach encji reprezentujących te wiersze może zostać zignorowane.

OptimisticLockException

Dostawca JPA może opóźnić zapis do bazy danych do momentu zakończenia transakcji, jeśli jest to zgodnie z ustawieniami trybu flush. W takiej sytuacji wymogi optymistycznego blokowania mogą być sprawdzone dopiero podczas zatwierdzania transakcji i spowodować wystąpienie wyjątku OptimisticLockException w pierwszej fazie zatwierdzania transakcji (ang. "before completion"). Aplikacja obsługująca wystąpienie wyjątku OptimisticLockException, powinna najpierw wywołać metodę flush (aby wymusić próbę zapisania modyfikacji do bazy danych) i przy wystąpieniu wyjatku, przechwycić go wykonując czynności porządkujące stan wersjonowanych encji, co ostatecznie zapobiegnie wystąpieniu tego wyjątku podczas zatwierdzaniu transakcji.

Zaleca się, aby dostawca JPA zawarł w wyjątku OptimisticLockException właściwy wyjątek, który spowodował jego wystąpienie. Nie jest to jednak gwarantowane.

W sytuacjach, kiedy wyjątek OptimisticLockException jest opakowany przez inny wyjątek, np. RemoteException, kiedy przekazywany jest do zdalnego klienta, encje, z których korzystają zagnieżdżone wyjątki powinny być Serializable, aby zapis stanu obiektów do strumienia (ang. marshalling) nie zakończył się błędem (implementowanie interfejsu Serializable przez dowolną klasę encyjną można nazwać dobrą praktyką w JPA).

Wyjątek OptimisticLockException zawsze wycofuje transakcję.

Pod koniec podrozdziału 3.4 w sekcji 3.4.4. pojawia się opis potencjalnej sytuacji, w której może wystąpić wyjątek OptimisticLockException. Jednakże nie mam najmniejszego pojęcia, o co autorom w tym końcowym zdaniu chodziło. Poszukuję chętnych wytłumaczenia mi tego wycinka - koniec sekcji 3.4.4 strona 57 (oczywiście najlepszy byłby przykład, który zobrazowałby sytuację):

Refreshing objects or reloading objects in a new transaction context and then retrying the transaction is a potential response to an OptimisticLockException.

Następna sekcja to 3.5 Entity Listeners and Callback Methods.