piątek, 21 stycznia 2011

Transakcyjność w springu

Czołem.
Dziś zaprezentuję jak w springu skonfigurować transakcyjność. Najpierw przypomnijmy kilka pojęć.

Propagacja transakcji polega na zdefiniowaniu zachowania transakcji przy wywołaniu metody wewnątrz innej transakcyjnej metody.

Jeśli metoda A jest uruchomiona w traksakcji to co ma się wydarzyć, gdy wywoła ona metodę B, która również jest transakcyjna? Czy transakcja powinna się propagować czy nie? Czy może B powinna być uruchomiona w swojej transakcji? Jest to konfigurowalne. Spójrzmy na tabelę z opisem 3 podstawowych poziomów propagacji:



Propagacja dla metody B

A jest w transakcji

A nie jest w transakcji

REQUIRED (domyślna
wartość)

B będzie
uruchomiona w tej samej transakcji

B będzie
uruchomiona w nowej transakcji

REQUIRES_NEW

B będzie
uruchomiona w nowej transakcji

B będzie
uruchomiona w nowej transakcji

MANDATORY

B będzie
uruchomiona w tej samej transakcji

otrzymamy wyjątek -
B nie będzie transakcji, choć jest wymagana


Jest ich więcej, ale tylko te omówię.

Poziomy izalacji - pojęcie bazodanowe związane z izolowaniem jednej transakcji od drugiej.
Wspomnę jeszcze pojęcia:
  • brudne odczyty (dirty reads) Na mój stan wiedzy - w żadnej z dzisiejszych baz nie wystąpi ten problem, jest już czysto teoretyczny. Problem polega na odczytaniu przez transakcję danych niezatwierdzonych jeszcze przez drugą transakcję.
  • niepowtarzalne odczyty (non repetable reads)
Wyobraźmy sobie że rozpoczynamy dwie niezależne transakcje T1 i T2.
  1. Niech T1 odczyta rekord R1 z tabeli A, 
  2. Następnie niezależnie od T1, transakcja T2 modyfikuje ten rekord i zatwierdza zmiany.
  3. T1 ponownie odczytuje rekord R1 i widzi inne dane.
Zwrócę uwagę, że mechanizm optimistic lock w hibernate działa właśnie na tej zasadzie. Mamy kolumnę w tabeli, która przechowuje wersję rekordu (np jako timestamp ostatniej modyfikacji). Przed zatwierdzeniem zmian hibernate robi automatycznie select i sprawdza czy wartość określająca wersję naszego rekordu jest identyczna z tą, którą posiada encja, której zmiany chcemy pchnąć do bazy. Jeśli wersja się różni - oznacza to, że mamy problem niepowtarzalnego odczytu - ktoś w między czasie zmodyfikował nasz wiesz.

  • fantomowe odczyty (phantom reads)
Wyobraźmy sobie że rozpoczynamy dwie niezależne transakcje T1 i T2.
  1. T1 odczytuje dane z tabeli A.
  2. Następnie T2 dodaje/usuwa dane z tabeli A i zatwierdza zmiany.
  3. T1 ponownie odczytuje dane z tabeli A i otrzymuje inny zbiór wyników.
Aby chronić się przed opisanymi problemami mamy do dyspozycji poziomy izolacji transakcji. Wspierane przez springa to:
  • READ_UNCOMMITED - wystąpić mogą brudne odczyty
  • READ_COMMITED - domyślna dla większości baz danych, zapobiega brudnym odczytom, mogą wystąpić odczyny niepowtarzalne oraz fantomowe
  • REPETABLE_READ - zapobiegamy odczytom niepowtarzalnym. Bazodanowcy, z którymi rozmawiałem o tym twierdzili, że wymuszanie na bazie transakcji z powtarzalnymi odczytami może skutkować drastycznym spadkiem wydajności. Myślę, że warto ich posłuchać. Mogą wystąpić odczyty fantomowe.
  • SERIALIZABLE - nie wystąpią odczyty fantomowe.
  •  
Spójrzmy na tabelę:

Poziom izolacji

Brudne odczyty

Niepowtarzalne odczyty

Fantomowe odczyty

READ_UNCOMMITED

TAK

TAK

TAK

READ_COMMITED

NIE

TAK

TAK

REPETABLE_READ

NIE

NIE

TAK

SERIALIZABLE

NIE

NIE

NIE


Po dokładniejszy opis powyższych problemów polecam lekturę artykułu Jarosława Błąda w 9 numerze java exPress.

To tyle ogólnej teorii. Będę opierał się o mojego posta na temat jdbc, i na jdbc będę prezentował jak w springu konfigurować transakcje. Pracuję na hsqldb ze względu na ekstremalnie prostą konfigurację.
Spring udostępnia nam dwa sposoby konfiguracji transakcji:
  1. Deklaratywny 
    1. adnotacyjnie
    2. xml-owo
  2. Programowy (transactionTemplate)
W dzisiejszym wpisie skupię się na deklaratywnym.
Pierwszym krokiem jest skonfigurowanie dataSource. Jak używam bazy hsqldb więc u mnie wygląda to tak:
<jdbc:embedded-database id="dataSource">
    <jdbc:script location="classpath:/META-INF/spring/schema.sql" />
</jdbc:embedded-database>
Dla innych baz możemy zrobić to tak:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>
Oraz odpowiedni PropertyPlaceholderConfigurer:
<context:property-placeholder location="classpath:jdbc.properties"/>
Plik jdbc.properties:
jdbc.driver=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@localhost:1521:xe
jdbc.username=test
jdbc.password=slawek
hibernate.dialect=org.hibernate.dialect.Oracle10gDialect

Ok, mamy już dataSource, następnie potrzeba nam skonfigurować trasaction managera. Spring daje kilka implementacji:
  • DataSourceTransactionManager
  • HibernateTransactionManager
  • JtaTransactionManager
Dla JTA możemy użyć skróconej konfiguracji
<tx:jta-transaction-manager />
Spring wybierze najlepszą pasującą do naszego środowiska uruchomieniowego implementację spośród:
  • OC4JJtaTransactionManager
  • WebLogicJtaTransactionManager
  • WebSphereUowTransactionManager
  • JtaTransactionManager
W moim przypadku, korzystam z jdbc więc konfiguruję:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

W podejściu deklaratywnym możemy konfigurować nasze transakcje albo adnotacjami, albo w xml-u. Pokażę jak to zrobić adnotacyjnie, a później w xml-u.
Zatem aby uruchomić adnotacje należy do konfiguracji springa dodać wpis:
<tx:annotation-driven />
który doda odpowiednie post processory - wykrywające adnotacje w naszych beanach.
Cały mój plik konfiguracyjny:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    
    <!-- adnotacyjnie konfiguruję beany -->
    <context:component-scan base-package="pl.turo.spring.jdbc" />

    <!-- adnotacyjnie konfiguruję transakcje -->
    <tx:annotation-driven />
    
    <!-- dataSource dla bazy danych hsql -->
    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:/META-INF/spring/schema.sql" />
    </jdbc:embedded-database>
        
    <!-- manager transakcji dla jdbc -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans>

Ponieważ bazę stawiam w pamięci, to korzystam ze skryptu schema.sql, który postawi mi odpowiednie tabele i doda wymagane przezemnie dane. Oto on:
drop table T_USER if exists;
drop table T_ACCOUNT if exists;

create table T_USER (ID integer identity primary key, NAME varchar(50) not null);
create table T_ACCOUNT (ID integer identity primary key, USER_ID integer not null, ACCOUNT_NAME varchar(25));

alter table T_ACCOUNT add constraint FK_USER_ID foreign key (USER_ID) references T_USER(ID) on delete cascade;

insert into T_USER (ID, NAME) values (1, 'Ala');
Mamy już gotowe środowisko do konfigurowania transakcji. W springu transakcyjne mogą być publiczne metody na beanach, ponieważ spring korzysta z AOP aplikując transakcje do metod. Zatem wszystkie ograniczenia spring AOP przenoszą się na moduł transakcyjny.
Aby oznaczyć jakąś metodę jako transakcyją dodajemy nad nią adnotację @Transactional:
@Transactional
public void addAccounts(AppUser user, Account... acc) {}
Domyślnie transakcja ma poziom izolacji ustalony przez bazę do której się łączymy, w większości przypadków będzie to READ_COMMMITED. Domyślny poziom propagacji to REQUIRED.
Po wykonaniu metody transakcja zostanie zatwierdzona. Domyślnie jeśli z metody wyleci wyjątek nie weryfikowalny (RuntimeException lub dziedziczący po nim, bądź Error) to transakcja zostanie wycofana. Dla wyjątków weryfikowalnych zostanie zatwierdzona.
Zachowania te można przekonfigurować:
@Transactional(rollbackFor=SQLException.class, noRollbackFor=RuntimeException.class)
public void addAccounts(AppUser user, Account... acc) {
    for (Account account : acc) {
        userDao.addAccount(user, account);
    }
    throw new RuntimeException("Pomimo wyjątku runtimeowego trasankcja zostanie zatwierdzona");
}
Ta konfiguracja odwraca domyślne zachowanie. Zauważmy, że na końcu metody rzucam nie weryfikowalnym wyjątkiem RuntimeException. Domyślnie spowodowałoby to wycofanie transakcji i niezapisanie do bazy danych żadnych wierszy.
Jednak zachowanie przekonfigurowałem mówiąc springowi, że nie chcę wycofywania zmian dla wyjątków RuntimeException (i dziedzicących po nim). Sprawdźmy to:
public static void main(String[] args) {
    // 1
    ApplicationContext context = new ClassPathXmlApplicationContext("/META-INF/spring/app-context.xml");
    AccountService service = context.getBean(AccountService.class);
    
    // 2
    Account[] acc = new Account[2];
    acc[0] = new Account(1, "janko muzykant");
    acc[1] = new Account(2, "krzysztof odkrywca");

    // 3
    try {
        service.addAccounts(new AppUser(1, "Ala"), acc);
    } catch (Exception e) {
        e.printStackTrace();
    }

    // 4
    AccountDao dao = context.getBean(AccountDao.class);
    Set<Account> findAll = dao.findAll();
    for (Account account : findAll) {
        System.out.println(account);
    }
}
  1. Ładuję kontekst springa i wyciągam z niego mój serwis, na którym założyłem transakcję.
  2. Tworzę nowe rekordy do tabeli T_ACCOUNT.
  3. Uruchamiam moją transakcyją metodę. Wiem, że w bazie jest użytkownik "Ala" z id 1. Wywołanie opakowuję w try-catch aby móc dalej sprawdzić co siedzi w bazie.
  4. Pobieram sobie dao z kontekstu springa, aby móc sprawdzić co siedzi w tabeli T_ACCOUNT. Metoda findAll zwraca wynik zapytania "select * from T_ACCOUNT".
Na mojej konsoli widzę wynik:
INFO [main] (AbstractApplicationContext.java:447) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@c7e553: startup date [Fri Jan 21 14:20:19 CET 2011]; root of context hierarchy
 INFO [main] (XmlBeanDefinitionReader.java:315) - Loading XML bean definitions from class path resource [META-INF/spring/app-context.xml]
 INFO [main] (DefaultListableBeanFactory.java:532) - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@6cb8: defining beans [jdbcAccountDao,accountServiceImpl,simpleJdbcDaoSupportUserDaoImpl,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.aop.config.internalAutoProxyCreator,org.springframework.transaction.annotation.AnnotationTransactionAttributeSource#0,org.springframework.transaction.interceptor.TransactionInterceptor#0,org.springframework.transaction.config.internalTransactionAdvisor,dataSource,transactionManager]; root of factory hierarchy
 INFO [main] (EmbeddedDatabaseFactory.java:127) - Creating embedded database 'testdb'
 INFO [main] (ResourceDatabasePopulator.java:135) - Executing SQL script from class path resource [META-INF/spring/schema.sql]
 INFO [main] (ResourceDatabasePopulator.java:185) - Done executing SQL script from class path resource [META-INF/spring/schema.sql] in 15 ms.
java.lang.RuntimeException: Pomimo wyjątku nie weryfikowalnego trasankcja zostanie zatwierdzona
    at pl.turo.spring.jdbc.dao.account.service.AccountServiceImpl.addAccounts(AccountServiceImpl.java:24)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:107)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
    at $Proxy9.addAccounts(Unknown Source)
    at pl.turo.spring.Main.main(Main.java:26)
Account [id=1, accountName=janko muzykant, userId=1]
Account [id=2, accountName=krzysztof odkrywca, userId=1]
Jak widać do bazy dodały się dwa wiersze, pomimo iż z naszej transakcyjnej metody poleciał wyjątek. Sprawdźmy jak się zachowa metoda na domyślnych ustawieniach:
@Transactional
public void addAccounts(AppUser user, Account... acc) {
    for (Account account : acc) {
        userDao.addAccount(user, account);
    }
    throw new RuntimeException("Wycofujemy zmiany");
}
Odpalam maina i mam wynik:
INFO [main] (AbstractApplicationContext.java:447) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@c7e553: startup date [Fri Jan 21 14:26:34 CET 2011]; root of context hierarchy
 INFO [main] (XmlBeanDefinitionReader.java:315) - Loading XML bean definitions from class path resource [META-INF/spring/app-context.xml]
 INFO [main] (DefaultListableBeanFactory.java:532) - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@288051: defining beans [jdbcAccountDao,accountServiceImpl,simpleJdbcDaoSupportUserDaoImpl,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.aop.config.internalAutoProxyCreator,org.springframework.transaction.annotation.AnnotationTransactionAttributeSource#0,org.springframework.transaction.interceptor.TransactionInterceptor#0,org.springframework.transaction.config.internalTransactionAdvisor,dataSource,transactionManager]; root of factory hierarchy
 INFO [main] (EmbeddedDatabaseFactory.java:127) - Creating embedded database 'testdb'
 INFO [main] (ResourceDatabasePopulator.java:135) - Executing SQL script from class path resource [META-INF/spring/schema.sql]
 INFO [main] (ResourceDatabasePopulator.java:185) - Done executing SQL script from class path resource [META-INF/spring/schema.sql] in 0 ms.
java.lang.RuntimeException: Wycofujemy zmiany
    at pl.turo.spring.jdbc.dao.account.service.AccountServiceImpl.addAccounts(AccountServiceImpl.java:22)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:107)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
    at $Proxy9.addAccounts(Unknown Source)
    at pl.turo.spring.Main.main(Main.java:26)
Tak jak się spodziewałem - nie ma nic w bazie. W projekcie, na którym testowałem opisywane funkcjonalności napisałem testy, które polecam sobie uruchomić, aby pobawić się nieco konfiguracją transakcji.
Należy jeszcze wspomnieć, że jeśli adnotacją @Transactional oznaczymy klasę, to wszystkie jej publiczne metody będą transakcyjne.
Wyżej opisałem teoretycznie kwestię propagacji transakcji. Będę uruchamiał tą samą metodę z serwisu, a propagację będę konfigurował w klasie UserDAO:
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void addAccount(AppUser user, Account account) {
    getSimpleJdbcTemplate().update("insert into T_ACCOUNT (ID, USER_ID, ACCOUNT_NAME) values (?, ?, ?)", account.getId(), user.getId(), account.getAccountName());
    account.setUserId(user.getId());
}
oraz metoda serwisowa:
@Transactional
public void addAccounts(AppUser user, Account... acc) {
    for (Account account : acc) {
        userDao.addAccount(user, account);
    }
    throw new RuntimeException("Wycofujemy zmiany");
}
Zatem widzimy, że transakcja założona na serwisie jest transakcyjna i na jej końcu rzucamy wyjątkiem, który wycofa jej zmiany. W jej środku wołamy metodę z dao i jej ustawiliśmy poziom propagacji na Propagation.REQUIRES_NEW. Oznacza to, że metoda w dao będzie uruchomiona w osobnej transakcji i powinniśmy zobaczyć w bazie wstawiane rekordy.
Uruchamiam ponownie maina:
INFO [main] (AbstractApplicationContext.java:447) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@e2eec8: startup date [Fri Jan 21 14:56:45 CET 2011]; root of context hierarchy
 INFO [main] (XmlBeanDefinitionReader.java:315) - Loading XML bean definitions from class path resource [META-INF/spring/app-context.xml]
 INFO [main] (DefaultListableBeanFactory.java:532) - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@ee7a14: defining beans [jdbcAccountDao,accountServiceImpl,simpleJdbcDaoSupportUserDaoImpl,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.aop.config.internalAutoProxyCreator,org.springframework.transaction.annotation.AnnotationTransactionAttributeSource#0,org.springframework.transaction.interceptor.TransactionInterceptor#0,org.springframework.transaction.config.internalTransactionAdvisor,dataSource,transactionManager]; root of factory hierarchy
 INFO [main] (EmbeddedDatabaseFactory.java:127) - Creating embedded database 'testdb'
 INFO [main] (ResourceDatabasePopulator.java:135) - Executing SQL script from class path resource [META-INF/spring/schema.sql]
 INFO [main] (ResourceDatabasePopulator.java:185) - Done executing SQL script from class path resource [META-INF/spring/schema.sql] in 15 ms.
java.lang.RuntimeException: Wycofujemy zmiany
    at pl.turo.spring.jdbc.dao.account.service.AccountServiceImpl.addAccounts(AccountServiceImpl.java:22)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:107)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
    at $Proxy9.addAccounts(Unknown Source)
    at pl.turo.spring.Main.main(Main.java:26)
Account [id=1, accountName=janko muzykant, userId=1]
Account [id=2, accountName=krzysztof odkrywca, userId=1]
Rekordy w bazie są, zatem widać że propagacja transakcji rzeczywiście działa.
Ponieważ transakcje w springu są dodawane aspektowo, to wszystko co dotyczy dodawania aspektów jest również prawdziwe dla transakcji. Napisałem wyżej już o ograniczeniu do publicznych metod.
Spring AOP domyślnie działa poprzez proxowanie obiektów. Polecam poczytanie tego posta, gdzie wyjaśniam kiedy spring i dlaczego użyje proxy a kiedy skorzysta z CGLIB-a do modyfikacji bytecode-u.

Pokażę jeszcze jak w xml-u skonfigurować transakcje.
Spójrzmy na plik konfiguracyjny:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <!-- 1. adnotacyjnie konfiguruję beany -->
    <context:component-scan base-package="pl.turo.spring.jdbc" />

    <!-- 2. dataSource dla bazy danych hsql -->
    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:/META-INF/spring/schema.sql" />
    </jdbc:embedded-database>

    <!-- 3. manager transakcji dla jdbc -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
    
    <!-- 4. Tutaj konfiguruję poziomy izolacji, propagacji itd. -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- metody zaczynające się na 'find' będą działały w trybie read-only -->
            <tx:method name="find*" read-only="true" no-rollback-for="java.lang.NullPointerException"/>

            <!-- metoda z dao będzie miała propagację REQUIRES_NEW -->
            <tx:method name="addAccount" propagation="REQUIRES_NEW" />

            <!-- resztę pozostawiam domyślnie -->
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>

    <!-- 5. Tutaj konfiguruję pointcut-y, które wyciągają mi metody, do którzch chcę dodać transakcyjność -->
    <aop:config>
        <!-- wszystkie moje serwisy i wyszystkie na nich metody -->
        <aop:pointcut id="serviceOperation"
            expression="execution(* pl.turo..*Service*.*(..))" />
            
        <!-- wszystkie moje klasy dao i wyszystkie na nich metody -->
        <aop:pointcut id="daoOperation"
            expression="execution(* pl.turo..*Dao*.*(..))" />
        
        <!-- tutaj mówię springowi: weż pontcut 'serviceOperation' i do niego zaaplikuj advice 'txAdvice' podobnie z 'daoOperation'-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation" />
        <aop:advisor advice-ref="txAdvice" pointcut-ref="daoOperation" />
    </aop:config>
</beans>
Od 1-3 jest identycznie jak w podejściu adnotacyjnym.
4. Tutaj mamy konfigurację aspektu który będzie dodawał transakcyjność na metody. Tutaj w tagach tx:method konfiguruję poziomy izolacji, propagacji, rollback-for i no-rollback-for.
Istotne jest aby zapamiętać, że dla każdej metody na którą dodajemy transakcyjność spring szuka jej konfiguracji od góry do dołu. Pierwszy patten na nazwę, który będzie pasował zostanie użyty. Dlatego na samym dole mam <tx:method name="*" />. Zasada jest taka: najpierw najbardziej specyficzne metody, później ogólniejsze.
5. Tutaj konfiguruję aspekt. Najpierw definiuję pointcut-y, które wyciągają nam join point-y (metody) na które chcemy dodać transakcyjność. Następnie należy powiedzieć springowi który advice użyć dla danego pointcut-a (tagi <aop:advice...>).
Uruchamiam ponownie mojego maina i widzę:
INFO [main] (AbstractApplicationContext.java:447) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@1a0c10f: startup date [Fri Jan 21 15:36:36 CET 2011]; root of context hierarchy
 INFO [main] (XmlBeanDefinitionReader.java:315) - Loading XML bean definitions from class path resource [META-INF/spring/app-context.xml]
 INFO [main] (DefaultListableBeanFactory.java:532) - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@b01d43: defining beans [jdbcAccountDao,accountServiceImpl,simpleJdbcDaoSupportUserDaoImpl,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,dataSource,transactionManager,txAdvice,org.springframework.aop.config.internalAutoProxyCreator,serviceOperation,daoOperation,org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor#0,org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor#1]; root of factory hierarchy
 INFO [main] (EmbeddedDatabaseFactory.java:127) - Creating embedded database 'testdb'
 INFO [main] (ResourceDatabasePopulator.java:135) - Executing SQL script from class path resource [META-INF/spring/schema.sql]
 INFO [main] (ResourceDatabasePopulator.java:185) - Done executing SQL script from class path resource [META-INF/spring/schema.sql] in 16 ms.
java.lang.RuntimeException: Wycofujemy zmiany
    at pl.turo.spring.jdbc.dao.account.service.AccountServiceImpl.addAccounts(AccountServiceImpl.java:22)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:107)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:89)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
    at $Proxy11.addAccounts(Unknown Source)
    at pl.turo.spring.Main.main(Main.java:26)
Account [id=1, accountName=janko muzykant, userId=1]
Account [id=2, accountName=krzysztof odkrywca, userId=1]
I jest to oczekiwane zachowanie. W metodzie serwisowej rzucam wyjątkiem nie weryfikowalnym, który wycofuje nam tą transakcję. Ale metody dao (wołane wewnątrz serwisowej) mają transakcję z propagacją REQUIRES_NEW (<tx:method name="addAccount" propagation="REQUIRES_NEW" />) i dlatego widzimy dodane rekordy.

To tyle w temacie transakcji. W następnym poście mam zamiar opisać jak testować w JUnit metody transakcyjne. Jak zwykle udostępniam projekt, na którym testowałem opisywane funkcjonalności.

3 komentarze:

  1. dzieki, swietny artykul. pozdrawiam

    OdpowiedzUsuń
  2. Drobna pomyłka w tabelce odnośnie poziomów izolacji, dla READ_COMMITED, niepowtarzalne odczyty powinny być na tak.

    OdpowiedzUsuń