środa, 16 października 2013

Spring i transakcje bazodanowe

Dzisiaj będzie trochę o programowaniu.

Spring to świetny framework.

Transakcyjność obsługiwana przez niego jest nie do przecenienia.... pod warunkiem że wszystko działa i nie trzeba debugować tych wszystkich początków i końców transakcji, commitów i rollbacków :)

Przykład z życia wzięty (oczywiście w uproszczeniu):

mamy jakiś serwis springowy. Załóżmy że nazywa się BusinessLogicService:

Konfigurację transakcji mamy zdefiniowaną za pomocą aspektów następująco:

Oczywiście konfiguracja jest niekompletna - poglądowa. Zakładamy że metoda doLogic()nie działa w kontekście transakcyjnym.

Co w takim razie stanie się gdy w innym miejscu kodu zrobimy wywołanie:

BusinessLogicService s = ctx.getBean("businessLogicService");
s.doLogic();

Zdrowy rozsądek podpowiada (przynajmniej mnie podpowiadał :) ), że nietransakcyjna metoda doLogic() wywoła metodę doTransactionalLogic(). Jako że w konfiguracji określiliśmy że ta metoda wymaga nowej transakcji powinna ona zostać w tym momencie utworzona. Dane zostaną zapisane do bazy przez metodę saveDataToDatabase(), a następnie zostanie rzucony wyjątek, który wycofa zmiany zapisane przez saveDataToDatabase().

Tym razem jednak mój zdrowy rozsądek się mocno mylił :)
Otóż w tym przypadku ŻADNA transakcja nie zostanie utworzona. Zmiany zapisane przez saveDataToDatabase() zostaną od razu zacommitowane do bazy i wyjątek RuntimeException nie wycofa żadnych modyfikacji!

Sporo czasu zajęło mi dojście do odpowiedzi dlaczego tak się dzieje, chociaż wyjaśnienie jest dość oczywiste.

Spring zarządza transakcjami za pomocą obiektów Proxy, które opakowują prawdziwe serwisy.
Aby transakcja została utworzona wywołanie MUSI przejść przez Proxy.
W tym wypadku funkcja doLogic() woła BEZPOŚREDNIO publiczną metodę doTransactionalLogic(). Wywołanie nie przechodzi przez Proxy - czyli nie ma transakcji.

Jakie są rozwiązania? Jest ich co najmniej kilka.
Przykładowo można przerobić serwis aby wyglądał tak:


W metodzie doLogic() pobieramy serwis z kontekstu spring'owego i w tym wypadku otrzymamy Proxy tego obiektu, a co za tym idzie wywołanie doTransactionalLogic() zadziała poprawnie!

Można również przenieść metodę doTransactonalLogic() do odrębnego serwisu, który następnie należy wstrzyknąć do BusinessLogicService. Wtedy wywołanie doTransactionalLogic() również zadziała poprawnie.

Oczywiście wszystkie te informacje można znaleźć w dokumentacji Spring'a, ale kto by tam czytał dokumentacje ;)

3 komentarze:

  1. Dodam tylko, że opisane zachowanie tyczy się wszystkich aspektów, nie tylko transakcyjnych (np. cache). Nieco bardziej intuicyjne zachowanie da się uzyskać przy użyciu AspectJ z LTW zamiast Springowej implementacji AOP.
    Pierwsze rozwiązanie problemu (z zaczytanie beana z kontekstu) będzie bardzo brzydko wyglądać w testach unitowych (trzeba mockować kontekst, a przecież dont mock interfaces you dont own).

    Z aspektami jest jeszcze jeden ciekawy case. Jeśli stosuje się wstrzykiwanie przez konstruktor (polecam!), to Spring nie będzie w stanie stworzyć oaspektowanej klasy jeśli nie implementuje ona żadnego interfejsu - potrzebny jest default constructor albo interfejs do stworzenia AOP proxy. (stare zgłoszenie, co jakiś czas bumpowane: https://jira.springsource.org/browse/SPR-3150). Ten problem dla odmiany jest całkowicie przemilaczany w dokumentacji :)

    OdpowiedzUsuń