One of the trickiest thing to do when developing database-backed application is deciding when and where to start and finish the transaction (eg: where should you draw the transaction boundary line).
Consider a very simple banking application with an AccountService class having getBalance(), debit(), credit() and transfer() methods. Transfer() calls getBalance() to validate the from-account has enough money, and then debit() the from-account and credit() the to-account.
All of the business steps of transfer() method have to be done in one transaction right? (why? — c’mon.. if you don’t do it in one transaction then it’s possible the balance got changed by another transaction between checking for sufficient money, debiting & crediting. The bank will lose reputation and have apologize to the poor customer.).
Pseudocode/sql for each method on AccountService class look somewhat like following:
- getBalance()
SELECT balance FROM account WHERE id = ?
- debit() and credit()
UPDATE account SET balance = ? WHERE id = ?
- transfer()
if (getBalance(fromAccount) < amount) { /*insufficient balance error */; } debit(fromAccount, amount); credit(toAccount, amount);
If you programatically hard-code the transaction boundary into getBalance(), debit() and credit() method, then it might look something like this:
public void debit(long accountId, double amount) { em.getTransaction().begin(); // start a new transaction Account account = em.find(Account.class, accountId); account.setBalance(account.getBalance() - amount); em.getTransaction().commit(); // commit }
But hangon, now you need to invoke all those 3 methods within 1 single transaction, your code might need to do something more complicated than that. If you change the transaction boundary into the transfer() method, then other clients that have been using getBalance(), debit(), credit() methods happily for other stuffs will be impacted — they now have to manually open and close transaction before calling those method (ugly).
Even worse, you may be considering to copy & paste the inside of getBalance(), debit() and credit() code into the transfer() method.. PLEASE DON’T!.. Remember Don’t Repeat Yourself principle!
Fortunately Spring provides a much better way of figuring out transaction boundary. All you need to do is to declare that your method needs an active transaction to be open. You do this by adding annotation to your method, plus bootstrapping few configurations
Your AccountService methods look like following now:
public double getBalance(long accountId) { // do stuff here } public void debit(long accountId, double amount) { // do stuff here } public void credit(long accountId, double amount) { // do stuff here } public void transfer(long fromAccountId, long toAccountId, double amount) { // call getBalance(), check from account has enough money // call debit() // call credit }
And the magic happens! When you call the transfer() method, Spring detects you don’t have an open transaction, so it creates a new one. When transfer() calls getBalance(), Spring won’t create a new transaction because it detects there’s a currently open one. It will assign the open transaction into getBalance() method instead. The same applies to debit() and credit(). This is also known as Transaction Propagation.
Spring does this magic via a technique called AOP (Aspect Oriented Programming). Simply speaking, Spring transaction manager will be notified everytime annotated method is invoked / returned, and take action (start new transaction, assign existing one, close, etc.)
I’ve created a demo project with a unit test so you can try this yourself. Make sure you have jdk 1.6 and Maven 3 installed. SVN Checkout , have a feel around the classes and run mvn test. The output will be available at target/surefire-reports/com.wordpress.gerrytan.springtx.AccountServiceTest-output.txt. I’ve configured the logging such that you can see when hibernate (JPA) creates / commit / rollback transaction, and issues SQL to synchronize the persistence context.
2012-05-31 01:06:26,430 [main] DEBUG com.wordpress.gerrytan.springtx.AccountServiceTest - ----- Transfer $20 from account 2 to 1 ----- 2012-05-31 01:06:26,430 [main] DEBUG org.hibernate.transaction.JDBCTransaction - begin 2012-05-31 01:06:26,431 [main] DEBUG org.hibernate.transaction.JDBCTransaction - current autocommit status: true 2012-05-31 01:06:26,431 [main] DEBUG org.hibernate.transaction.JDBCTransaction - disabling autocommit 2012-05-31 01:06:26,431 [main] DEBUG org.hibernate.SQL - select account0_.id as id0_0_, account0_.balance as balance0_0_, account0_.name as name0_0_, account0_.version as version0_0_ from account account0_ where account0_.id=? 2012-05-31 01:06:26,432 [main] DEBUG org.hibernate.SQL - select account0_.id as id0_0_, account0_.balance as balance0_0_, account0_.name as name0_0_, account0_.version as version0_0_ from account account0_ where account0_.id=? 2012-05-31 01:06:26,433 [main] DEBUG org.hibernate.transaction.JDBCTransaction - commit 2012-05-31 01:06:26,437 [main] DEBUG org.hibernate.SQL - update account set balance=?, name=?, version=? where id=? and version=? 2012-05-31 01:06:26,437 [main] DEBUG org.hibernate.SQL - update account set balance=?, name=?, version=? where id=? and version=? 2012-05-31 01:06:26,438 [main] DEBUG org.hibernate.transaction.JDBCTransaction - re-enabling autocommit 2012-05-31 01:06:26,438 [main] DEBUG org.hibernate.transaction.JDBCTransaction - committed JDBC Connection 2012-05-31 01:06:26,438 [main] DEBUG com.wordpress.gerrytan.springtx.AccountServiceTest - ----- Completed Transfer $20 from account 1 to 2 -----
Following are useful resources for further learning:
- Spring Framework Reference Doc – Chapter 11 Transaction Management:
- Hibernate Entity Manager Reference:
- Java EE 6 Official Tutorial – Part VI Persistence: http://docs.oracle.com/javaee/6/tutorial/doc/bnbpy.html
- JSR-317 – JPA 2.0 Spec: http://download.oracle.com/otndocs/jcp/persistence-2.0-fr-eval-oth-JSpec/
- Spring Roo Reference Doc (Roo is a cool Spring source-code rapid generation tool I used for the demo code. Roo can generate a Spring-JPA-Maven source code for you in a zap!):