Mapping Transactional Exceptions in Spring

Categories: Java

Overview

Recently I wrote a simple Java Spring/JPA application which included writing data to a database, where the underlying table had a uniqueness-constraint. The user-provided inputs could potentially violate that uniqueness constraint, and so I wanted to detect the error reported by the database operation and transform it into a user-friendly “name already in use” error.

This proved surpisingly difficult to do. Below I describe why this was so, and what solution I eventually implemented.

The Problem

When using JPA (Java Persistence API) to write to a database, various exceptions can occur. However it is desirable not to have exceptions related to a specific technology propagating through the entire application.

Spring and Hibernate do their parts by remapping exceptions from driver-specific exceptions (PostgreSQL in my case) to Hibernate’s standard exceptions and from there to Spring’s standard persistence exceptions. See:

  • org.springframework.dao.DataAccessException – Spring’s exception hierarchy
  • org.springframework.dao.support.PersistenceExceptionTranslator – applied automatically by Spring

However in many cases it is still desirable to map the Spring exception to something more domain-specific. As an example, when persisting an Account object it may be appropriate to remap a DataIntegrityViolationException (DB language for trying to insert a duplicate value into a column with a uniqueness constraint) into a “Duplicate Account name” exception, or similar.

A method which is not transactional can use the usual approach:

public Account addAccount(..) throws DuplicateAccount {
  try {
    Account account = ...
    accountDao.save(account);
  } catch(DataIntegrityViolationException e) {
    throw new DuplicateAccount();
  }
}

When invoked with no transaction active, the call to save triggers an insert immediately, ie is performed as an “auto-commit” operation; the exception-catching-and-mapping then executes as expected.

However if some code starts a transaction then calls the above method, the behaviour is quite different: the catch-clause is ignored and the exception-remapping never happens!

The problem is that the database verifies various constraints only on commit - and the above code has no control over where that commit occurs.

This is a very nasty problem actually - whether this code works or not depends on invisible state (whether a thread-local transaction object exists).

The same problem occurs if the above method is annotated with @Transactional; in fact that guarantees that the commit occurs after the method has returned and thus the catch-clause has no effect. Fortunately in that case, the problem is visible in the source-code.

Solutions

The only solutions that occurred to me were:

  • force a commit at a place where the exception can be caught
  • explicitly test for uniqueness before trying the insert
  • register a list of error-handlers to apply on commit

Some searching on the internet revealed no other solutions - people just seem to be happy with returning “failed due to database problem” to their clients, without further detail. Or people are implementing the “ahead-of-time-test” approach and find it so self-evident that they don’t mention it.

Forcing a Commit

When using Spring-Data annotations, a commit could potentially be forced with something like:

public Account addAccount(..) throws DuplicateAccount {
  try {
    Account account = ...
    self.doSave(account);
  } catch(DataIntegrityViolationException e) {
    throw new DuplicateAccount();
  }
}

@Transactional(propagation=Propagation.REQUIRES_NEW, rollbackOn=Exception.class)
public void doSave(Account account) {
  accountDao.save(account);
}

This isn’t a very elegant solution though:

  • if a transaction is currently active when addAccount is called, then the account-save operation will be done in an unrelated transaction which is something the caller might not expect.
  • the REQUIRES_NEW specifier is not supported by all transaction-manager implementations when another transaction is already active
  • the “self” invocation is ugly

Possibly Propagation.NESTED would also work - I haven’t tested that. In this example where the operation that can potentially trigger a constraint-violation is just a single statement, then Propagation.NOT_SUPPORTED or Propagation.NEVER might also do, depending on context and requirements.

Note that the “self” invocation is needed because Spring annotations are implemented via proxy objects; such proxies do not intercept intra-class method calls (and it is therefore pointless to annotate a private method, as its invocations are aways intra-class).

Of course, explicit calls to EntityManager methods can be used rather than Spring-data annotations to implement transactions. That avoids the clumsy self-reference and having to factor the actual save out into a different method. It does not remove the need to use Propagation.REQUIRES_NEW.

Explicit Testing

This approach takes the brute-force solution of just testing ahead-of-time whether a particular uniqueness-constraint would fail later - ie in this example doing something like “select 1 from ACCOUNT where name=?” to see if a record with that name already exists. The database uniqueness-constraint is then just a “backup validation” that should never fail.

This has the cost of an extra round-trip to the database, just to query data that the insert will actually validate anyway (via the constraint). However the point-in-time at which the validation is done can be controlled, unlike the constraint-check which occurs only on commit.

While part of me dislikes the idea of enforcing the constraint in two places (code and database), there are also arguments for this. The validation rule “account names must be unique” is a business rule, and therefore logically should be enforced in the business tier. There are also persistence technologies which do not offer uniqueness-constraint-enforcement (eg various NoSQL databases). And there are also similar constraints that a database could not enforce - eg “Account name must have at least one upper-case letter”. It is therefore normal for such constraints to be implemented in the code.

Register Error Handlers for Commit

An idea I briefly considered was to store a list of exception-handler objects in a thread-local, then somehow ensure that when Spring invokes the commit it passes any resulting exception to the list of handlers which may map the exception to a different type. The new exceptions would need to be subtypes of RuntimeException, but that’s not unreasonable.

That would seem to be a nice feature for Spring-Data (or Hibernate) to implement, but probably rather complex.

Conclusion

In the end, I chose to implement the “explicit ahead-of-time test” solution. Detecting constraint violation at commit-time was just too imprecise:

  • it is not localised in the code (exception occurs when commit occurs)
  • it is not localised to a specific SQL operation (in a transaction with multiple inserts, it is not clear which constraint failed)