简体   繁体   中英

Spring doesn't close hibernate session after transaction rollback

I'm trying to catch a ConstraintViolationException in my service layer and rethrowing a user defined checked exception. I'm catching the exception in my controller and adding an error object to my BindingResult. I'm using declarative transaction management I've tried to make my DAO a Repository and added a PersistenceExceptionTranslationPostProcessor to catch a spring translated exception. I've also added a txAdvice to rollback on all throwables. My exception does get caught but I'm getting an error 500 with:

Hibernate: insert into user (email, password, first_name, last_name, userType) values (?, ?, ?, ?, 1)
[acme]: [WARN ] - 2013-Feb-05 11:12:43 -  SqlExceptionHelper:logExceptions(): SQL Error: 1062, SQLState: 23000
[acme]: [ERROR] - 2013-Feb-05 11:12:43 -  SqlExceptionHelper:logExceptions(): Duplicate entry 'admin' for key 'email_unique'
[acme]: [DEBUG] - 2013-Feb-05 11:12:43 -  HibernateTransactionManager:processCommit(): Initiating transaction commit
[acme]: [DEBUG] - 2013-Feb-05 11:12:43 -  HibernateTransactionManager:doCommit(): Committing Hibernate transaction on Session [SessionImpl(PersistenceContext[entityKeys=[],collectionKeys=[]];ActionQueue[insertions=[] updates=[] deletions=[] collectionCreations=[] collectionRemovals=[] collectionUpdates=[] unresolvedInsertDependencies=UnresolvedEntityInsertActions[]])]
[acme]: [ERROR] - 2013-Feb-05 11:12:43 -  AssertionFailure:<init>(): HHH000099: an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session): org.hibernate.AssertionFailure: null id in com.test.model.AdminUser entry (don't flush the Session after an exception occurs)
[acme]: [DEBUG] - 2013-Feb-05 11:12:43 -  HibernateTransactionManager:doRollbackOnCommitException(): Initiating transaction rollback after commit exception
org.hibernate.AssertionFailure: null id in com.test.model.AdminUser entry (don't flush the Session after an exception occurs)
    at org.hibernate.event.internal.DefaultFlushEntityEventListener.checkId(DefaultFlushEntityEventListener.java:79)
    at org.hibernate.event.internal.DefaultFlushEntityEventListener.getValues(DefaultFlushEntityEventListener.java:194)
    at org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:156)
    at org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:225)
    at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:99)
    at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
    at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1213)
    at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:402)
    at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.beforeTransactionCommit(JdbcTransaction.java:101)
    at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.commit(AbstractTransactionImpl.java:175)
    at org.springframework.orm.hibernate4.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:468)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)

My Controller:

@RequestMapping (method = RequestMethod.POST) 
    @Transactional
    public String registerAdmin(@Valid @ModelAttribute("user") AdminUser user, BindingResult bindingResult, ModelMap model)  {
        if (bindingResult.hasErrors()) {
            return "admin/admins/form";
        } 
        else if (!user.getPassword().equals(user.getConfirmPassword())) {
            bindingResult.addError(new ObjectError("user.confirmPassword", "Passwords don't match"));
            return "admin/admins/form";
        }
        else {
            user.setPassword(passwordEncoder.encodePassword(user.getPassword(), null));
            try {
                userService.save(user);
                return "redirect:/admin/admins";
            } catch(ApplicationException ce) {
                bindingResult.addError(new ObjectError("user.email", "Email already registered"));
                return "admin/admins/form";
            }

        }

    }

Part of my Spring config:

    <context:component-scan base-package="com.test.dao, com.test.service" />
     <context:property-placeholder location="/WEB-INF/spring.properties"/>  

    <import resource="springapp-security.xml"/>

    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/testdb?zeroDateTimeBehavior=convertToNull"/>
    <property name="username" value="test"/>
    <property name="password" value="test"/>
  </bean>

  <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="myDataSource"/>
    <property name="mappingLocations" value="classpath*:com/test/model/hbm/**/*.hbm.xml" />

    <property name="hibernateProperties">
      <value>
        hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
        hibernate.show_sql=true
      </value>
    </property>
  </bean>

 <tx:annotation-driven />
  <bean id="transactionManager"
            class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
  </bean>
  <tx:advice id="txAdvice">
  <tx:attributes>
  <tx:method name="*" rollback-for="Throwable" />
  </tx:attributes>
</tx:advice>

Service layer:

public class UserServiceImpl implements UserDetailsService, UserService {

    private UserDAO dao;


    @Override
    public void save(User c) throws ApplicationException {
        try {
            dao.save(c);
        } catch(DataIntegrityViolationException cve) {
            throw new ApplicationException("email already registered");
        }
    }

If I don't catch the runtime exception I don't get the hibernate exception (don't flush the session..)

You may want to remove the transaction annotation from your controller and add it to the service layer.

The service layer would look like below. If your service layer is throwing a checked exception you can add that to your annotation so that the insert is not even attempted to be committed.

public class UserServiceImpl implements UserDetailsService, UserService {

private UserDAO dao;


@Override
@Transactional(rollbackFor=ApplicationException.class)
public void save(User c) throws ApplicationException {
    try {
        dao.save(c);
    } catch(DataIntegrityViolationException cve) {
        throw new ApplicationException("email already registered");
    }
}

What is happening currently in your code is that the transaction is not being rolled back but has to rollback because it actually tried to commit the data but because a database constraint the transaction had to be rolled back. By forcing the rollback with the @Transactional(rollbackFor=ApplicationException.class) it will not allow the transaction to perform a commit but it will rollback and your app will still add the error to the BindingResult.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM