In the first part of the series, I showed how transactions work in plain-vanilla JDBC. And then I showed how Spring manages JDBC-based transactions. In this second part of the series, I'll show how transactions work in plain-vanilla JPA first. And then show how Spring manages JPA-based transactions.
Funds Transfer
To help illustrate transactions, I'll be using the same case study of transferring funds from one bank account to another. Here, we show code snippets of debit, credit, and transfer methods.
... class BankAccountService { public void transfer(MonetaryAmount amount, ...) { debit(amount, ...); credit(amount, ...); ... } public void credit(MonetaryAmount amount, AccountId accountId) { ... } public void debit(MonetaryAmount amount, AccountId accountId) { ... } ... }
JPA Transactions
In plain-vanilla JPA, transactions are started by calling getTransaction().begin()
on the EntityManager
. The code snippet below illustrates this.
import javax.persistence.*; ... EntityManagerFactory emf = ...; EntityManager em = emf.createEntityManager(); try { em.getTransaction().begin(); // make changes through entities em.getTransaction().commit(); ... } catch(Exception e) { em.getTransaction().rollback(); throw e; } finally { em.close(); }
Technically, the EntityManager
is in a transaction from the point it is created. So calling begin()
is somewhat redundant. Until begin()
is called, certain operations such as persist
, merge
, remove
cannot be called. Queries can still be performed (e.g. find()
).
Objects that were returned from queries can be changed. Although the JPA specification is somewhat unclear on what will happen to these changes when no transaction has been started.
Now, let's apply JPA to the funds transfer case study.
We defined a BankAccount
entity to handle debit()
and credit()
behavior.
import javax.persistence.*; @Entity ... class BankAccount { @Id ...; ... public void debit(MonetaryAmount amount) {...} public void credit(MonetaryAmount amount) {...} ... }
We add an EntityManagerFactory
to BankAccountService
to enable the creation of EntityManager
s when needed.
import javax.persistence.*; ... class BankAccountService { private EntityManagerFactory emf; // injected via constructor ... public void transfer(MonetaryAmount amount, ...) ... { EntityManager em = emf.createEntityManager(); try { em.getTransaction().begin(); BankAccount fromAccount = em.find(BankAccount.class, ...); BankAccount toAccount = em.find(BankAccount.class, ...); fromAccount.debit(amount); toAccount.credit(amount); em.getTransaction().commit(); ... } catch(Exception e) { em.getTransaction().rollback(); // handle exception (possibly rethrowing it) } finally { em.close(); } } public void credit(MonetaryAmount amount, AccountId ...) ... { EntityManager em = emf.createEntityManager(); try { em.getTransaction().begin(); BankAccount theAccount = em.find(BankAccount.class, ...); theAccount.credit(amount); em.getTransaction().commit(); ... } catch(Exception e) { em.getTransaction().rollback(); // handle exception (possibly rethrowing it) } finally { em.close(); } } public void debit(MonetaryAmount amount, AccountId ...) ... { EntityManager em = emf.createEntityManager(); try { em.getTransaction().begin(); BankAccount theAccount = em.find(BankAccount.class, ...); theAccount.debit(amount); em.getTransaction().commit(); ... } catch(Exception e) { em.getTransaction().rollback(); // handle exception (possibly rethrowing it) } finally { em.close(); } } }
Spring-managed JPA Transactions
The transfer
, credit
, and debit
methods could sure use a template class (something like a JdbcTemplate
) to remove all the boilerplate code. Spring previously provided a JpaTemplate
class, but was deprecated as of Spring 3.1, in favor of native EntityManager
usage (typically obtained through @PersistenceContext
).
So, let's do just that — use EntityManager
obtained through @PersistenceContext
.
import javax.persistence.*; ... class BankAccountService { @PersistenceContext private EntityManager em; ... public void transfer(MonetaryAmount amount, ...) ... { try { em.getTransaction().begin(); BankAccount fromAccount = em.find(BankAccount.class, ...); BankAccount toAccount = em.find(BankAccount.class, ...); fromAccount.debit(amount); toAccount.credit(amount); em.getTransaction().commit(); ... } catch(Exception e) { em.getTransaction().rollback(); // handle exception (possibly rethrowing it) } } public void credit(MonetaryAmount amount, AccountId ...) ... { try { em.getTransaction().begin(); BankAccount theAccount = em.find(BankAccount.class, ...); theAccount.credit(amount); em.getTransaction().commit(); ... } catch(Exception e) { em.getTransaction().rollback(); // handle exception (possibly rethrowing it) } } public void debit(MonetaryAmount amount, AccountId ...) ... { try { em.getTransaction().begin(); BankAccount theAccount = em.find(BankAccount.class, ...); theAccount.debit(amount); em.getTransaction().commit(); ... } catch(Exception e) { em.getTransaction().rollback(); // handle exception (possibly rethrowing it) } } }
Our code is a little bit simpler. Since we didn't create an EntityManager
, we don't have to close it. But we are still calling getTransaction().begin()
. Is there a better way? And how does an EntityManager
get injected into the object in the first place?
From my previous post in this series, the astute reader is probably already thinking of having Spring do the work for us. And rightfully so!
EntityManager
and @PersistenceContext
We tell Spring to inject an EntityManager
from the EntityManagerFactory
by adding a PersistenceAnnotationBeanPostProcessor
(either through XML <bean>
, or simply using a Java-based configuration via @Configuration
classes loaded via AnnotationConfigApplicationContext
).
- When using XML-based configuration, a
PersistenceAnnotationBeanPostProcessor
is transparently activated by the<context:annotation-config />
element. And this element also gets transparently activated by<context:component-scan />
. - When using Java-based
@Configuration
, theAnnotationConfigApplicationContext
is used. And with it, annotation config processors are always registered (one of which is the aforementionedPersistenceAnnotationBeanPostProcessor
).
By adding a single bean definition, the Spring container will act as a JPA container and inject an EnitityManager
from your EntityManagerFactory
.
JPA and @Transactional
Now that we have an EntityManager
, how can we tell Spring to begin transactions for us?
We tell Spring to start transactions by marking methods as @Transactional
(or mark the class as @Transactional
which makes all public methods transactional). This is consistent with the way Spring enables transactions with JDBC.
import javax.persistence.*; import org.springframework.transaction.annotation.Transactional; @Transactional ... class BankAccountService { @PersistenceContext private EntityManager em; ... public void transfer(MonetaryAmount amount, ...) ... { BankAccount fromAccount = em.find(BankAccount.class, ...); BankAccount toAccount = em.find(BankAccount.class, ...); fromAccount.debit(amount); toAccount.credit(amount); } public void credit(MonetaryAmount amount, AccountId ...) ... { BankAccount theAccount = em.find(BankAccount.class, ...); theAccount.credit(amount); } public void debit(MonetaryAmount amount, AccountId ...) ... { BankAccount theAccount = em.find(BankAccount.class, ...); theAccount.debit(amount); } }
Wow, that was nice! Our code just got a lot shorter.
And just as explained in the first part of this series, when Spring encounters this annotation, it proxies the object (usually referred to as a Spring-managed bean). The proxy starts a transaction (if there is no on-going transaction) for methods that are marked as @Transactional
, and ends the transaction when the method returns successfully.
A call to debit()
will use a transaction. A separate call to credit()
will use a transaction. But what happens when a call to transfer()
is made?
Since the transfer()
method is marked as @Transactional
, Spring will start a transaction. This same transaction will be used for calls to debit()
and credit()
. In other words, debit(amount)
and credit(amount)
will not start a new transaction. It will use the on-going transaction (since there is one).
But wait! How does Spring know when to inject a proper entity manager? Is it only injected when a transactional method is invoked?
Shared EntityManager
In one of my training classes, I tried the following to better understand how Spring injects an EntityManager
via @PersistenceContext
. And I believe it will help others too. So, here's what I tried:
import javax.persistence.*; import org.springframework.transaction.annotation.Transactional; import org.springframework.beans.factory.InitializingBean; @Transactional ... class BankAccountService implements InitializingBean { @PersistenceContext private EntityManager em; ... @Override public void afterPropertiesSet() { System.out.println(em.toString()); } ... }
An output of something like this was displayed on the console after the application context started.
Shared EntityManager proxy for target factory [...]
So what is this shared entity manager?
When the application context starts, Spring injects a shared entity manager. The shared EntityManager
will behave just like an EntityManager
fetched from an application server's JNDI environment, as defined by the JPA specification. It will delegate all calls to the current transactional EntityManager
, if any; otherwise, it will fall back to a newly created EntityManager
per operation.
Going back to our question. Spring doesn't inject the right entity manager at the right time. It always injects a shared entity manager. But this shared entity manager is transaction-aware. It delegates to the current transactional EntityManager
, if there is an on-going transaction.
Conclusion
This concludes the two-part series. I hope that by starting off with the plain-vanilla versions of JDBC and JPA (sans DAOs and repositories), I was able to make it clearer as to how Spring is able to manage transactions behind the scenes. And that by having a clearer idea as to what Spring is doing behind the scenes, you can troubleshoot better, understand why you get an TransactionRequiredException
saying "No transactional EntityManager available", and add better fixes to your applications.
Now, it's time for a cold one.
No comments:
Post a Comment