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:
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:
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:
Pierwszym krokiem jest skonfigurowanie dataSource. Jak używam bazy hsqldb więc u mnie wygląda to tak: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.
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)
- Niech T1 odczyta rekord R1 z tabeli A,
- Następnie niezależnie od T1, transakcja T2 modyfikuje ten rekord i zatwierdza zmiany.
- T1 ponownie odczytuje rekord R1 i widzi inne dane.
- fantomowe odczyty (phantom reads)
- T1 odczytuje dane z tabeli A.
- Następnie T2 dodaje/usuwa dane z tabeli A i zatwierdza zmiany.
- T1 ponownie odczytuje dane z tabeli A i otrzymuje inny zbiór wyników.
- 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.
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:
- Deklaratywny
- adnotacyjnie
- xml-owo
- Programowy (transactionTemplate)
<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
<tx:jta-transaction-manager />Spring wybierze najlepszą pasującą do naszego środowiska uruchomieniowego implementację spośród:
- OC4JJtaTransactionManager
- WebLogicJtaTransactionManager
- WebSphereUowTransactionManager
- JtaTransactionManager
<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); } }
- Ładuję kontekst springa i wyciągam z niego mój serwis, na którym założyłem transakcję.
- Tworzę nowe rekordy do tabeli T_ACCOUNT.
- 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.
- 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".
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.
dzieki, swietny artykul. pozdrawiam
OdpowiedzUsuńDrobna pomyłka w tabelce odnośnie poziomów izolacji, dla READ_COMMITED, niepowtarzalne odczyty powinny być na tak.
OdpowiedzUsuńDzięki, poprawione.
Usuń