Saturday, June 18, 2011

Spring/Hibernate/JPA and Transactions

I spent much of the day yesterday researching how transactions are handled under Spring 3.0.x with JPA and Hibernate 3.6.x handling persistence.  I thought I would take some time to write down what I learned.

We originally made it standard practice to annotate both the service and repository layers with Spring @Transactional annotations.  For example:

Here's the repository layer:

// *HINT* This is incorrect.  It will compile but will probably not behave as you expect
@Repository
class ProductDaoImpl {
  @Autowired
  EntityManager entityManager;

    @Transactional(readOnly = true)
    public Product findProductById(String id) {
        ...
    }

    @Transactional(readOnly = false)
    public void saveProduct(Product product) {
        entityManager.merge(product);
    }
}
And the service layer
@Service
class ProductManagerImpl {

    @Autowired
    ProductDao productDao;

    @Transactional(readOnly = true)
    public Product findProductById(String id) {
        productDao.findProductById(id);
    }

    @Transactional(readOnly = false) {
    public void saveProduct(Product product) {
        productDao.saveProduct(product);
    }
}
In order to test transactions in the system we did something like this in the repository layer:
@Repository
class ProductDaoImpl {
  @Autowired
  EntityManager entityManager;

    @Transactional(readOnly = true)
    public Product findProductById(String id) {
        ...
    }

    @Transactional(readOnly = true)
    public void saveProduct(Product product) {
        ...
    }
} 

While leaving the service layer unchanged.

Our thinking was one of two things would happen: 1) the Service layer would start a read-write transaction, hit the read-only demarcation in the repository layer, and start a new read-only transaction, or 2) Spring would figure out that a read-only transaction was needed and make the whole interaction read-only.

Imagine our surprise when we found our changes persisted to the database without complaint.

It turns out that when you start a transaction the first thing that happens is the system checks to see if there's already a transaction open that it can piggyback on.  So when the system opens a read/write transaction then tries to start a new transaction in the dao, it finds the already opened transaction, piggybacks on it, thereby allowing the dao to write to the database.

So it turns out to be better practice to put all of your @Transactional annotation on the service layer.  This eliminates any ambiguity as to whether or not an operation will be read-only.

However, even after moving all of our annotations to the service layer, we were still surprised to find that we received no exceptions when we called merge in a transaction marked as read-only.  The system would quietly fail with no notification whatsoever.

After some digging around I found that Spring-managed transactions don't actually enforce read/write permissions.  Instead it delegates enforcement to the database and to Hibernate.  (Well that's the rumor anyways.  I never actually saw any proof that Spring informed either the database--PostgreSQL in my case--or the JDBC driver that the transaction was read-only.)  And since Hibernate has no notion of a read-only transaction, it does the next-best thing by setting the flushmode to FlushMode.MANUAL.  Luckily the Hibernate implementation of Transaction.commit() won't flush if the flushmode is manual.  That turns out to be the only real check on the read-only transactions.  So it is possible write to the database by calling EntityManager.flush() before the transaction closes.

So, in short:
1) Put all of your @Transactional annotations on the service layer to eliminate ambiguity.
2) Don't depend on a @Transactional(readonly = true) demarcation to actually be read-only.  You have to make certain that the EntityManager isn't flushed during the transaction because that will indeed persist changes.

No comments:

Post a Comment