Using Spring & JPA & Hibernate I use method to persist some entity with Exception handling like this one:
#Repository
public class UserDAOImpl implements UserDAO {
#PersistenceContext
EntityManager em;
#Override
public void createUserRole(String role) throws RoleAlreadyExistsException {
try {
UserRole userRole = new UserRole(role);
em.persist(userRole);
} catch (Exception e) {
throw new RoleAlreadyExistsException();
}
}
}
My service:
#Service("userService")
public class UserService
#Transactional
public void createUserRole(String role) throws RoleAlreadyExistsException {
userDao.createUserRole(role);
}
}
later I implement some logic:
try{
userService.createUserRole(role.name());
} catch (AuthorityEntityAlreadyExistsException e){}
but it doesn't catch exception which report about duplicate key :
SEVERE: Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [PRIMARY]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:259)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:225)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:521)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:761)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:730)
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:59)
at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:109)
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:95)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:485)
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 'ROLE_ADMIN' for key 'PRIMARY'
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
So, wasn't thrown none of my RoleAlreadyExistsException, catch block was missed. Exception was thrown during transaction commit in the end of service method. How to catch that exception ??? Or how to implement this logic in Spring in another way??
1) DAOs shouldn't catch exceptions. Let the exception generated by Hibernate or Spring be thrown and leave it alone. Especially if your exception-conversion is extremely broad and loses information (throws away the original stacktrace).
2) Don't rely on constraint violations to tell you there's a pre-existing entry. You can query for it before telling Hibernate to perform the insert. As long as the query is in the same transaction as the insert, and the isolation level is repeatable-read or better, the query result will be reliable.
EntityManager.persist didn't necessary execute a sql statements : The new entity is registered in a cache, and send to the database when needed.
If your constraints are checked by the database, you'll see an error when this entity is send to the database, in the best case when the transaction commit : it's the behavior you see in your trace.
You can force your JPA implementation to send his cache to the database by invoking EntityManager.flush()
public void createUserRole(String role) throws RoleAlreadyExistsException {
try {
UserRole userRole = new UserRole(role);
em.persist(userRole);
em.flush();
} catch (Exception e) {
throw new RoleAlreadyExistsException();
}
}
however, calling flush too much can be a performance lost, you should call it only when necessary. It can maybe be more simple to separate DAO ans service : try cach DataIntegrityException in UserService and don't call flush in the DAO
use #transactional annotation for the DAO or service or controller layer for the method you want to roll back when ever SQLexception is caught during commit.
i.e #transactional
public void method(#param somevalue)
{
// Database saving or updating function
}
if you want specific message to be shown during exception
use the below if you want to have global handler for particular expception class
#ExceptionHandler(SQLException.class)
public String exceptionMessage(){ //return exception message}
Related
I have a Hibernate listener on DB table. When this listener gets triggered, I want to query another table and update some data. However, when I try to do so, I get an error like:
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:571)
...
Caused by: java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
My code looks something like:
#Slf4j
#RequiredArgsConstructor
#Component
public class MyTableHibernateListener implements PostInsertEventListener, PreUpdateEventListener, PostUpdateEventListener {
private final JPQLQueryFactory queryFactory;
#Override
public boolean onPreUpdate(PreUpdateEvent event) {
try {
...
// Some working logic to set some data within the current MyTable.
// Try to query another table below
AnotherTable row = queryFactory.select(QAnotherTable.anotherTable)
.from(QAnotherTable.anotherTable)
.where(...)
.fetchOne();
...
log.info("Success");
return false;
} catch (Exception e) {
log.error("Failure", e);
throw e;
}
}
}
"Success" is logged and no failure is logged, so it looks like the error occurs outside the listener method. I'm also not yet making any DB changes in the other table, so it looks like even querying another table isn't allowed. Can someone help me understand what might be the issue here and what the recommended workaround may be?
You need to wrap your query in a BeforeTransactionCompletionProcess (I assume you want the update to run in same transaction) and register it into Hibernate action queue.
#Override
public boolean onPreUpdate(PreUpdateEvent event) {
event.getSession().getActionQueue().registerProcess(new BeforeTransactionCompletionProcess() {
#Override
public void doBeforeTransactionCompletion(SessionImplementor session) {
//your query
}
});
...
}
I created a service method that creates user accounts. If creation fails because the given e-mail-address is already in our database, I want to send the user an e-mail saying they are already registered:
#Transactional(noRollbackFor=DuplicateEmailException.class)
void registerUser(User user) {
try {
userRepository.create(user);
catch(DuplicateEmailException e) {
User registeredUser = userRepository.findByEmail(user.getEmail());
mailService.sendAlreadyRegisteredEmail(registeredUser);
}
}
This does not work. Although I marked the DuplicateEmailExcepetion as "no rollback", the second SQL query (findByEmail) still fails because the transaction was aborted.
What am I doing wrong?
There is no #Transactional annotation on the repository.
That's not a problem with Spring / JDBC or your code, the problem is with the underlying database. For example, when you are using the Postgres if any statement fails in a transaction all the subsequent statements will fail with current transaction is aborted.
For example executing the following statements on your Postgres:
> start a transaction
> DROP SEQUENCE BLA_BLA_BLA;
> Error while executing the query; ERROR: sequence "BLA_BLA_BLA" does not exist"
> SELECT * FROM USERS;
> ERROR: current transaction is aborted, commands ignored until end of transaction block
Still the SELECT and subsequent statements are expected to succeed against MySQL, Oracle and SQL Server
Why don't you change the logic as following :
void registerUser(User user) {
User existingUser = userRepository.findByEmail(user.getEmail())
if(existingUser == null){
userRepository.create(user);
}else{
mailService.sendAlreadyRegisteredEmail(existingUser)
}
}
This would ensure that only non-existing users to be inserted into the database.
#Transactional annotation is placed at incorrectly. Spring creates a AOP advisor around the method where #Transactional annotation is defined. So, in this case, pointcut will be created around registedUser method. But, registerUser method doesn't throw DuplicateEmailException. Hence, no rollback rules are evaluated.
You need to define the #Transactional rule around UserRepository.createUser method. This will ensure that Transaction pointcut created by spring doesn't rollback because of DuplicateEmailException.
public class UserRepository {
#Transactional(noRollbackFor=DuplicateEmailException.class)
public User createUser(){
//if user exist, throw DuplicateEmailException
}
}
void registerUser(User user) {
try {
userRepository.create(user);
catch(DuplicateEmailException e) {
User registeredUser = userRepository.findByEmail(user.getEmail());
mailService.sendAlreadyRegisteredEmail(registeredUser);
}
}
You could wrap the call to the userRepository in a try catch block. Or you could look first if the user exists, and abourt the creation of a new one.
This is my connection detail in JBoss standalone.xml
<connection-url>
jdbc:oracle:thin:#(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(ADDRESS=(PROTOCOL=TCP)(HOST=xx.1xx.119.1xx)(PORT=1521))(LOAD_BALANCE=on)(FAILOVER=on))(CONNECT_DATA=(SERVICE_NAME=XE)))
</connection-url>
I want to handle a corner case of failover where post getting EntityManager object during a call of persist(), the connection is lost. Failover option is not switching to next database in the same transaction, it switches to active connection in the next transaction. I attempted something like this: (Catch Exception and get updated bean object)
public EntityManager getEntityManager() {
try {
entityManager = getEntityManagerDao(Constant.JNDI_NFVD_ASSURANCE_ENTITY_MANAGER);
} catch (NamingException e) {
LOGGER.severe("Data could not be persisted.");
throw new PersistenceException();
}
return entityManager.getEntityManager();
}
/**
* Inserts record in database. In case multiple connections/databases exist, one more attempt will be made to
* insert record.
*
* #param entry
*/
public void persist(Object entry) {
try {
getEntityManager().persist(entry);
} catch (PersistenceException pe) {
LOGGER.info("Could not persist data. Trying new DB connection.");
getEntityManager().persist(entry);
}
}
private static Object getJNDIObject(String path) throws NamingException {
Object jndiObject = null;
InitialContext initialContext = new InitialContext();
jndiObject = initialContext.lookup(path);
return jndiObject;
}
private static AssuranceEntityManager getEntityManagerDao(String path) throws NamingException {
return (AssuranceEntityManager) getJNDIObject(path);
}
But this one also is not helping. After catching the exception, getting a new bean with JNDI lookup does not contain an updated new connection and an exception is thrown. This results in loss of data of that transaction.
Please suggest how to handle this corner case of "Connection lost post getting EntityManager and before persisting."
I think it's quite impossible what you want to achieve. The thing is that if internal DB transction is aborted then the JTA transaction is in abort state and you can't continue with it.
I expect it's kind of similar to this case
#Stateless
public class TableCreator {
#Resource
DataSource datasource;
#TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void create() {
try(Connection connection = datasource.getConnection()) {
Statement st = connection.createStatement();
st.execute("CREATE TABLE user (id INTEGER NOT NULL, name VARCHAR(255))");
} catch (SQLException sqle) {
// ignore this as table already exists
}
}
}
#Stateless
public class Inserter {
#EJB
private TableCreator creator;
public void call() {
creator.create();
UserEntity entity = new UserEntity(1, "EAP QE");
em.persist(entity);
}
}
In case that table user exists and you would use annotation #TransactionAttribute(TransactionAttributeType.REQUIRED) then the create call will be part of the same jta global transaction as call of persist. As in such case the transaction was aborted the persist call would fail with exception like (postgresql case)
Caused by: org.postgresql.util.PSQLException: ERROR: current transaction is aborted, commands ignored until end of transaction block
I mean if Oracle jdbc driver is not able to to handle connection fail transparently to JBoss app server and throws the exception upwards then I think that the only possible solution is to repeat the whole update action.
I try to execute this code
#Transactional
#Controller
public class MyController{
....
#RequestMapping(..)
public String MyMethod(...)
{
....
try {
ao_history_repository.save(new AoHistory(..));
}
catch (DataIntegrityViolationException e) {
System.out.println("history already exist");
}
....
model.addAttribute("...", my_respository.findAoToDetail(id) );
return "...";
}
But when i got duplicate entry Exception i catch it but after i got a other Exception
org.hibernate.AssertionFailure: null id in persistence.AoHistory entry
(don't flush the Session after an exception occurs)
I know that When a ConstraintViolationException is thrown it invalidates the current session but how can i reopen a new session and a new transaction ?
As you write, you need a new transaction. From your code snippet it looks like the simplest thing would be to move #Transactional from the controller to the repository classes. As an alternative, you could add a service layer and move #Transactional there.
A different approach would be to pre-check the entity object before trying to save it in the entity manager, so that exception is never thrown.
I got session bean which is managed by containter. Recently I run into problems, where exception is thrown:
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)
it is because some other process has updated row (and version field has changed). Now when it is thrown, I catch OptimisticLockException and want to re-run failed operation (and I want to put WRITE lock this time to be sure it won't fail again), I do it this way:
T ctj = new T();
C ca = entityManager.find(C.class, id);
Double newBalance = Operations.add(ca.getAccountBalance(), amount);
ca.setAccountBalance(newBalance);
entityManager.persist(ca);
ctj.setBalanceAfterTransaction(newBalance);
entityManager.persist(ctj);
try {
flushRegisterTransactionUpdateAccountBalance();
} catch(OptimisticLockException ex) {
retryBalanceUpdate(ca, ctj, amount);
}
and the methods I call in above:
#TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
private void retryBalanceUpdate(C ca, T ctj, Double amount) {
entityManager.refresh(ca);
entityManager.lock(ca, LockModeType.WRITE);
Double newBalance = Operations.add(ca.getAccountBalance(), amount);
ca.setAccountBalance(newBalance);
entityManager.persist(ca);
ctj.setBalanceAfterTransaction(newBalance);
entityManager.persist(ctj);
flushRegisterTransactionUpdateAccountBalance();
}
#TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
private void flushRegisterTransactionUpdateAccountBalance() {
entityManager.flush();
}
Those 2 methods I have crated, because I was hoping the whole (parent) transaction will not fail because of exception thrown by flushRegisterTransactionUpdateAccountBalance().
Unfortunately it fails, when I call in catch block method retryBalanceUpdate, the first line of it's body (entityManager.refresh(ca)) throws:
[TxPolicy] javax.ejb.EJBTransactionRolledbackException: EntityManager must be access within a transaction
[MyBean] No transaction
javax.persistence.TransactionRequiredException: EntityManager must be access within a transaction
Does anybode know how I could achieve what I explained? I am using EJB 3.0, entityManager object is initiated by class-level annotation:
#PersistenceContext(unitName="MyPersistenceUnit") private EntityManager entityManager;
The class it self is stateless session bean with transaction attribute SUPPORTS
OptimisticLockException :
Thrown by the persistence provider when an optimistic locking conflict occurs. This exception may be thrown as
part of an API call, a flush or at commit time. The current
transaction, if one is active, will be marked for rollback.
You can create a custom exception with annotation #ApplicationException(rollback=false). In flushRegisterTransactionUpdateAccountBalance you have to catch OptimisticLockException & re-throw custom exception.
Can refer the below the sample code.
try {
flushRegisterTransactionUpdateAccountBalance();
} catch(XApplicationException ex) {
retryBalanceUpdate(ca, ctj, amount);
}
In below code, handling the exception & then re-throwing custom exception which is marked as rollback = false.
#TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
private void flushRegisterTransactionUpdateAccountBalance() throws XApplicationException{
try {
entityManager.flush();
} catch(OptimisticLockException ex) {
throw new XApplicationException(ex.getMessage());
}
}