26
votes

I am fairly new to JPA and want to find best practices when handling persistence exceptions from JPA for things like say, unique constraint violations which can be addressed by the user. There are tons of examples on how to write JPA apps, but almost nothing on how to hand exceptions kicked out by them. :/

For example registering a user, the person enters an email address that is already in active use by the system and gets a constraint violation:

try {
     em.persist(credentials);
} catch (javax.persistence.PersistenceException ex) {

which produces this error when a duplicate email is added:

WARNING: SQL Error: 0, SQLState: 23505
SEVERE: ERROR: duplicate key value violates unique constraint "EMAIL_UQ_IDX"
  Detail: Key (email)=([email protected]) already exists.

How can I get a meaningful answer back to the user? For example something like: Oops looks like someone is using that email address already, are you sure you haven't registered before? Is there a facility built in to parse this or will I need to run regexes against the exception message in a (possibly a series of) if statement(s)?

And what about if it is caught on the business tier... what are best practices for kicking it up to the presentation tier... like I said before, so that a 'nice' message can be provided for the user.


Added for clarity: Just so people know, I had, have, and am still looking at all the different types of persistence exceptions, and here is some of the research I've been doing I didn't include with the "try statement" example I included above:

try {
     em.persist(credentials);
     } catch (javax.persistence.PersistenceException ex) {
         System.out.println("EXCEPTION CLASS NAME: " + ex.getClass().getName().toString());
         System.out.println("THROWABLE CLASS NAME: " + ex.getCause().getClass().getName().toString());
                Throwable th = ex.getCause();
         System.out.println("THROWABLE INFO: " + th.getCause().toString());
         Logger.getLogger(CredentialsControllerImpl.class
              .getName()).log(Level.INFO, "Credentials Controller "
                  + "persistence exception "
                      + "EXCEPTION STRING: {0}", ex.toString());
         Logger.getLogger(CredentialsControllerImpl.class
              .getName()).log(Level.INFO, "Credentials Controller "
                  + "persistence exception "
                      + "THROWABLE MESSAGE: {0}", th.getMessage());
         Logger.getLogger(CredentialsControllerImpl.class
              .getName()).log(Level.INFO, "Credentials Controller "
                  + "persistence exceptions "
                      + "THROWABLE STRING: {0}", th.toString());
     }

:)

5

5 Answers

6
votes

You typically don't use the low-level exceptions to do that.

Instead, you explicitely check that the email is available (using a query), and perist the email only if it doesn't exist.

Sure, there could be a race condition if two threads do the same check in parallel, but it will be extremely rare, and the database constraint is there to guarantee the uniqueness.

3
votes

There are subclasses of PersistenceException: EntityExistsException, EntityNotFoundException, NonUniqueResultException, NoResultException, OptimisticLockException, RollbackException, TransactionRequiredException. Source: http://docs.oracle.com/javaee/5/api/javax/persistence/PersistenceException.html

You can use them. Try to check the type of exception or overload an error handling method (which is better). EntityExistsException I think the error you are searching for in the example you gave above. But you should check "if it exists" yourself. That's the best practice.

The SQL Error should never be needed to shown to the user. That error is always for you. Any data related error which needs informing user must be checked manually.

I use J2EE Web Environment. I just forward the request to error.jsp if there's an exception. I also provide an extra object for error.jsp to clarify the information like user can go back, can go which page after error etc. Of course I automated this, I don't like writing redundant code, because it is hard to update. So I just write send the exception and error message to another class in the catch block.

3
votes

I'm doing something similar in my DB service layer to figure out whether the exception was caused by a conflicting constraint or general DB failure:

try {
....
} catch (final PersistenceException e) {
    final Throwable cause = e.getCause();
    if (cause instanceof MySQLIntegrityConstraintViolationException) {
        throw new ConflictException(cause);
    }
    throw new ServiceException(e);
}
3
votes

Integrate your JPA project with Spring and let spring throw the exceptions as DataAccessException

Subclasses of DataAccessException in org.springframework.dao

class   CannotAcquireLockException
      Exception thrown on failure to aquire a lock during an update, for example during a "select for update" statement.
class   CannotSerializeTransactionException
      Exception thrown on failure to complete a transaction in serialized mode due to update conflicts.
class   CleanupFailureDataAccessException
      Exception thrown when we couldn't cleanup after a data access operation, but the actual operation went OK.
class   ConcurrencyFailureException
      Exception thrown on concurrency failure.
class   DataAccessResourceFailureException
      Data access exception thrown when a resource fails completely: for example, if we can't connect to a database using JDBC.
class   DataIntegrityViolationException
      Exception thrown when an attempt to insert or update data results in violation of an integrity constraint.
class   DataRetrievalFailureException
      Exception thrown if certain expected data could not be retrieved, e.g.
class   DeadlockLoserDataAccessException
      Generic exception thrown when the current process was a deadlock loser, and its transaction rolled back.
class   EmptyResultDataAccessException
      Data access exception thrown when a result was expected to have at least one row (or element) but zero rows (or elements) were actually returned.
class   IncorrectResultSizeDataAccessException
      Data access exception thrown when a result was not of the expected size, for example when expecting a single row but getting 0 or more than 1 rows.
class   IncorrectUpdateSemanticsDataAccessException
      Data access exception thrown when something unintended appears to have happened with an update, but the transaction hasn't already been rolled back.
class   InvalidDataAccessApiUsageException
      Exception thrown on incorrect usage of the API, such as failing to "compile" a query object that needed compilation before execution.
class   InvalidDataAccessResourceUsageException
      Root for exceptions thrown when we use a data access resource incorrectly.
class   OptimisticLockingFailureException
      Exception thrown on an optimistic locking violation.
class   PermissionDeniedDataAccessException
      Exception thrown when the underlying resource denied a permission to access a specific element, such as a specific database table.
class   PessimisticLockingFailureException
      Exception thrown on a pessimistic locking violation.
class   TypeMismatchDataAccessException
      Exception thrown on mismatch between Java type and database type: for example on an attempt to set an object of the wrong type in an RDBMS column.
class   UncategorizedDataAccessException
      Normal superclass when we can't distinguish anything more specific than "something went wrong with the underlying resource": for example, a SQLException from JDBC we can't pinpoint more precisely.
1
votes

I don’t like the idea of doing a query to check if the data can probably be inserted because it adds round-trips for a check the database server does anyway and it does not even work in every case because the database can change between the SELECT and the INSERT (though this may depend on how you handle transactions).

Anyway, handling the error looks like the only safe option to me, it’s “free” (no redundant checks, no additional round-trips) and it’s not hard to do. But it depends on your JDBC driver. For example, with PostgreSQL you can do:

try {
    em.persist(credentials);
} catch (javax.persistence.PersistenceException ex) {
    // use a loop to get the PSQLException
    for (Throwable current = ex; current != null; current = current.getCause()) {
        if (current instanceof PSQLException) {
            final PSQLException psqlEx = (PSQLException) current;
            final ServerErrorMessage serverErrorMessage = psqlEx.getServerErrorMessage();
            if ("EMAIL_UQ_IDX".equals(serverErrorMessage.getConstraint())) {
                // handle duplicate E-Mail address
            }
            break;
        }
    }
}

ServerErrorMessage (Javadoc, source code) provides a lot of information (which is used to generate the exception message):

System.out.println(serverErrorMessage.getColumn());
System.out.println(serverErrorMessage.getConstraint());
System.out.println(serverErrorMessage.getDatatype());
System.out.println(serverErrorMessage.getDetail());
System.out.println(serverErrorMessage.getFile());
System.out.println(serverErrorMessage.getHint());
System.out.println(serverErrorMessage.getInternalPosition());
System.out.println(serverErrorMessage.getInternalQuery());
System.out.println(serverErrorMessage.getLine());
System.out.println(serverErrorMessage.getMessage());
System.out.println(serverErrorMessage.getPosition());
System.out.println(serverErrorMessage.getRoutine());
System.out.println(serverErrorMessage.getSQLState());
System.out.println(serverErrorMessage.getSchema());
System.out.println(serverErrorMessage.getSeverity());
System.out.println(serverErrorMessage.getTable());
System.out.println(serverErrorMessage.getWhere());

When you do checks in a trigger, you can set many of these fields yourself by using the USING option = expression syntax, for example

RAISE integrity_constraint_violation USING CONSTRAINT = 'EMAIL_UQ_IDX'