Elegantly handling constraint violations in EJB/JPA environment?
I'm working with EJB and JPA on a Glassfish v3 app server. I have an Entity class where I'm forcing one of the fields to be unique with a @Column annotation.
@Entity
public class MyEntity implements Serializable {
private String uniqueName;
public MyEntity() {
}
@Column(unique = true, nullable = false)
public String getUniqueName() {
return uniqueName;
}
public void setUniqueName(String uniqueName) {
this.uniqueName = uniqueName;
}
}
When I try开发者_运维技巧 to persist an object with this field set to a non-unique value I get an exception (as expected) when the transaction managed by the EJB container commits.
I have two problems I'd like to solve:
1) The exception I get is the unhelpful "javax.ejb.EJBException: Transaction aborted". If I recursively call getCause() enough times, I eventually reach the more useful "java.sql.SQLIntegrityConstraintViolationException", but this exception is part of the EclipseLink implementation and I'm not really comfortable relying on it's existence.
Is there a better way to get detailed error information with JPA?
2) The EJB container insists on logging this error even though I catch it and handle it.
Is there a better way to handle this error which will stop Glassfish from cluttering up my logs with useless exception information?
Thanks.
The exception I get is the unhelpful "javax.ejb.EJBException: Transaction aborted". (...)
I did a test on my side (with GFv3 and EclipseLink) and I confirm this behavior. The full stacktrace is :
javax.ejb.EJBException: Transaction aborted at com.sun.ejb.containers.BaseContainer.completeNewTx(BaseContainer.java:4997) at com.sun.ejb.containers.BaseContainer.postInvokeTx(BaseContainer.java:4756) at com.sun.ejb.containers.BaseContainer.postInvoke(BaseContainer.java:1955) at com.sun.ejb.containers.BaseContainer.postInvoke(BaseContainer.java:1906) at com.sun.ejb.containers.EJBLocalObjectInvocationHandler.invoke(EJBLocalObjectInvocationHandler.java:198) at com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate.invoke(EJBLocalObjectInvocationHandlerDelegate.java:84) at $Proxy218.myBusinessMethod(Unknown Source) at com.stackoverflow.q2522643.__EJB31_Generated__MyEJB__Intf____Bean__.myBusinessMethod(Unknown Source) at com.stackoverflow.q2522643.MyServlet.doGet(MyServlet.java:28) at javax.servlet.http.HttpServlet.service(HttpServlet.java:734) at javax.servlet.http.HttpServlet.service(HttpServlet.java:847) at org.apache.catalina.core.StandardWrapper.service(StandardWrapper.java:1523) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:279) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:188) at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:641) at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:97) at com.sun.enterprise.web.PESessionLockingStandardPipeline.invoke(PESessionLockingStandardPipeline.java:85) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:185) at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:332) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:233) at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:165) at com.sun.grizzly.http.ProcessorTask.invokeAdapter(ProcessorTask.java:791) at com.sun.grizzly.http.ProcessorTask.doProcess(ProcessorTask.java:693) at com.sun.grizzly.http.ProcessorTask.process(ProcessorTask.java:954) at com.sun.grizzly.http.DefaultProtocolFilter.execute(DefaultProtocolFilter.java:170) at com.sun.grizzly.DefaultProtocolChain.executeProtocolFilter(DefaultProtocolChain.java:135) at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:102) at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:88) at com.sun.grizzly.http.HttpProtocolChain.execute(HttpProtocolChain.java:76) at com.sun.grizzly.ProtocolChainContextTask.doCall(ProtocolChainContextTask.java:53) at com.sun.grizzly.SelectionKeyContextTask.call(SelectionKeyContextTask.java:57) at com.sun.grizzly.ContextTask.run(ContextTask.java:69) at com.sun.grizzly.util.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:330) at com.sun.grizzly.util.AbstractThreadPool$Worker.run(AbstractThreadPool.java:309) at java.lang.Thread.run(Thread.java:619) Caused by: javax.transaction.RollbackException: Transaction marked for rollback. at com.sun.enterprise.transaction.JavaEETransactionImpl.commit(JavaEETransactionImpl.java:450) at com.sun.enterprise.transaction.JavaEETransactionManagerSimplified.commit(JavaEETransactionManagerSimplified.java:837) at com.sun.ejb.containers.BaseContainer.completeNewTx(BaseContainer.java:4991) ... 34 more Caused by: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.0.0.v20091127-r5931): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: java.sql.SQLIntegrityConstraintViolationException: The statement was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index identified by 'SQL100326111558470' defined on 'MYENTITY'. Error Code: -1 Call: INSERT INTO MYENTITY (ID, NAME) VALUES (?, ?) bind => [2, Duke!] Query: InsertObjectQuery(com.stackoverflow.q2522643.MyEntity@dba6a9) at org.eclipse.persistence.exceptions.DatabaseException.sqlException(DatabaseException.java:324) at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeDirectNoSelect(DatabaseAccessor.java:800) at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeNoSelect(DatabaseAccessor.java:866) at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.basicExecuteCall(DatabaseAccessor.java:586) at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeCall(DatabaseAccessor.java:529) at org.eclipse.persistence.internal.sessions.AbstractSession.executeCall(AbstractSession.java:914) at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:205) at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:191) at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.insertObject(DatasourceCallQueryMechanism.java:334) at org.eclipse.persistence.internal.queries.StatementQueryMechanism.insertObject(StatementQueryMechanism.java:162) at org.eclipse.persistence.internal.queries.StatementQueryMechanism.insertObject(StatementQueryMechanism.java:177) at org.eclipse.persistence.internal.queries.DatabaseQueryMechanism.insertObjectForWrite(DatabaseQueryMechanism.java:461) at org.eclipse.persistence.queries.InsertObjectQuery.executeCommit(InsertObjectQuery.java:80) at org.eclipse.persistence.queries.InsertObjectQuery.executeCommitWithChangeSet(InsertObjectQuery.java:90) at org.eclipse.persistence.internal.queries.DatabaseQueryMechanism.executeWriteWithChangeSet(DatabaseQueryMechanism.java:286) at org.eclipse.persistence.queries.WriteObjectQuery.executeDatabaseQuery(WriteObjectQuery.java:58) at org.eclipse.persistence.queries.DatabaseQuery.execute(DatabaseQuery.java:675) at org.eclipse.persistence.queries.DatabaseQuery.executeInUnitOfWork(DatabaseQuery.java:589) at org.eclipse.persistence.queries.ObjectLevelModifyQuery.executeInUnitOfWorkObjectLevelModifyQuery(ObjectLevelModifyQuery.java:109) at org.eclipse.persistence.queries.ObjectLevelModifyQuery.executeInUnitOfWork(ObjectLevelModifyQuery.java:86) at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.internalExecuteQuery(UnitOfWorkImpl.java:2863) at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1225) at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1207) at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1167) at org.eclipse.persistence.internal.sessions.CommitManager.commitNewObjectsForClassWithChangeSet(CommitManager.java:197) at org.eclipse.persistence.internal.sessions.CommitManager.commitAllObjectsWithChangeSet(CommitManager.java:103) at org.eclipse.persistence.internal.sessions.AbstractSession.writeAllObjectsWithChangeSet(AbstractSession.java:3260) at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.commitToDatabase(UnitOfWorkImpl.java:1405) at org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork.commitToDatabase(RepeatableWriteUnitOfWork.java:547) at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.commitToDatabaseWithChangeSet(UnitOfWorkImpl.java:1510) at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.issueSQLbeforeCompletion(UnitOfWorkImpl.java:3134) at org.eclipse.persistence.internal.sessions.RepeatableWriteUnitOfWork.issueSQLbeforeCompletion(RepeatableWriteUnitOfWork.java:268) at org.eclipse.persistence.transaction.AbstractSynchronizationListener.beforeCompletion(AbstractSynchronizationListener.java:157) at org.eclipse.persistence.transaction.JTASynchronizationListener.beforeCompletion(JTASynchronizationListener.java:68) at com.sun.enterprise.transaction.JavaEETransactionImpl.commit(JavaEETransactionImpl.java:412) ... 36 more Caused by: java.sql.SQLIntegrityConstraintViolationException: The statement was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index identified by 'SQL100326111558470' defined on 'MYENTITY'. at org.apache.derby.client.am.SQLExceptionFactory40.getSQLException(Unknown Source) at org.apache.derby.client.am.SqlException.getSQLException(Unknown Source) at org.apache.derby.client.am.PreparedStatement.executeUpdate(Unknown Source) at com.sun.gjc.spi.base.PreparedStatementWrapper.executeUpdate(PreparedStatementWrapper.java:108) at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeDirectNoSelect(DatabaseAccessor.java:791) ... 69 more Caused by: org.apache.derby.client.am.SqlException: The statement was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index identified by 'SQL100326111558470' defined on 'MYENTITY'. at org.apache.derby.client.am.Statement.completeExecute(Unknown Source) at org.apache.derby.client.net.NetStatementReply.parseEXCSQLSTTreply(Unknown Source) at org.apache.derby.client.net.NetStatementReply.readExecute(Unknown Source) at org.apache.derby.client.net.StatementReply.readExecute(Unknown Source) at org.apache.derby.client.net.NetPreparedStatement.readExecute_(Unknown Source) at org.apache.derby.client.am.PreparedStatement.readExecute(Unknown Source) at org.apache.derby.client.am.PreparedStatement.flowExecute(Unknown Source) at org.apache.derby.client.am.PreparedStatement.executeUpdateX(Unknown Source) ... 72 more
As we can see, EclipseLink actually throws a o.e.p.e.DatabaseException
which is then caught by the container. But this is WRONG. EclipseLink should throw a PersistenceException
(from JPA) or one if its subclass but certainly not a provider specific exception. This is a bug and you should report it as such: https://glassfish.dev.java.net/servlets/ProjectIssues (in the entity-persistence subcomponent).
And you're absolutely right, you should NOT catch provider specific exceptions for the sake of portability. You should catch a JPA PersistenceException
or a subclass (and then maybe look at the wrapped SQLException
). You may have to (temporarily) in this particular case because of the EclipseLink bug, but this is a workaround.
I don't know how to detect the unique constraint violation in a portable way, the best I've come up with is just dealing with a PersistenceException. If someone can answer that I'd be interested too.
I can help with the log issue.
Inside of your persistence unit in your persistence.xml add the following:
<properties>
<property name="eclipselink.logging.level" value="SEVERE"/>
</properties>
That will get rid of some of the exceptions. You'll still see stack traces where the container is seeing exceptions at CMT commit time. You have to swallow these before the container sees them. You can do the following.
1) Create an application specific exception to indicate a persistence problem. I called mine DataStoreException.
2) Don't use a no-interface view bean. Add the DataStoreException to the throws clause of the method signature in the biz interface.
3) Add the following method to your EJB:
@AroundInvoke
public Object interceptor(InvocationContext ic) throws Exception {
Object o = null;
try {
o = ic.proceed();
if (!sessionContext.getRollbackOnly()) {
entityManager.flush();
}
} catch (PersistenceException ex) {
throw new DataStoreException(ex);
}
return o;
}
精彩评论