8
votes

I have a question. I use Spring+Hibernate and I can't handle exceptions in methods marked as @Transactional. Previously, when I used Spring JDBC everything worked just fine.

DAO classes are marked with @Repository.
Here is snipper of my old code.
in service:

@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, rollbackFor = {Exception.class})
        public boolean bookTickets(Integer userId, List<Integer> ticketsId) throws TicketsServiceException {
            Integer currId = null;
            try {
                for (Integer ticketId : ticketsId) {
                    currId = ticketId;
                    Ticket ticket = ticketDAO.getTicketById(ticketId);
                    bookingDAO.bookTicket(ticket, userId);
                }
            } catch (DuplicateKeyException e) {
                throw new TicketsServiceException(String.format(MESSAGE_TICKET_ALREADY_BOOKED, currId));
            } catch (EmptyResultDataAccessException e) {
            throw new TicketsServiceException(String.format(MESSAGE_NO_SUCH_TICKET, currId));
            }
            return true;   


in dao:

@Transactional(propagation = Propagation.MANDATORY)
public void bookTicket(Ticket ticket, final int userId) {
    MapSqlParameterSource map = new MapSqlParameterSource();
    map.addValue(Constants.TABLE_BOOKING.FIELD_TICKET_ID, ticket.getId());
    map.addValue(Constants.TABLE_BOOKING.FIELD_USER_ID, userId);

    int rowsAffected = template.update(QUERY_INSERT_BOOKING, map);
    if (LOG.isTraceEnabled()) {
        LOG.trace("Affected " + rowsAffected + " rows.");
    }
}

Now I'm moving my DAO from Spring JDBC onto hibernate 3. That's what I have now.
in service:

@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, rollbackFor = {Exception.class})
public boolean bookTickets(Integer userId, List<Integer> ticketsId) throws TicketsServiceException {
    Integer currId = null;
    try {
        for (Integer ticketId : ticketsId) {
            currId = ticketId;
            Ticket ticket = ticketDAO.getTicketById(ticketId);
            bookingDAO.bookTicket(new Booking(ticketId, userId));
        }
    } catch (Exception e) {
        throw new TicketsServiceException(String.format(MESSAGE_THERE_IS_NO_SUCH_TICKET_OR_ALREADY_BOOKED, currId));
    }
    return true;
}

in dao:

@Override
public void bookTicket(Booking booking) {
    getHibernateTemplate().save(booking);
}

The problem is that I can't handle exceptions inside try-catch block in service method. They are throwing directly into my controller.

I think the problem is in difference between transaction managers org.springframework.jdbc.datasource.DataSourceTransactionManager in Spring JDBC and org.springframework.orm.hibernate3.HibernateTransactionManager in hibernate, but I dont have any ideas how to overcome this problem, so I need your help.



AFTER UPDATE. ADDED EXAMPLE OF EXCEPTION.

2013-10-21 14:51:17 ERROR BookingController:89 - Unhandled exception
org.springframework.dao.DataIntegrityViolationException: Could not execute JDBC batch update; SQL [insert into booking (ticket_id, user_id, booking_id) values (?, ?, ?)]; constraint ["CONSTRAINT_INDEX_2 ON PUBLIC.BOOKING(TICKET_ID) VALUES ( /* key:1 */ 2, 1, 1)"; SQL statement:
insert into booking (ticket_id, user_id, booking_id) values (?, ?, ?) [23505-173]]; nested exception is org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
    at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:643)
    at org.springframework.orm.hibernate3.HibernateTransactionManager.convertHibernateAccessException(HibernateTransactionManager.java:793)
    at org.springframework.orm.hibernate3.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:664)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:755)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:724)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:475)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:270)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:631)
    at cdp.tarasenko.springmvc.task3.service.TicketsService$$EnhancerByCGLIB$$fabdc899.bookTickets(<generated>)
    at cdp.tarasenko.springmvc.task3.controller.BookingController.bookTickets(BookingController.java:54)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:219)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:745)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:686)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:925)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:856)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:936)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:838)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:755)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:812)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:848)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:594)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:486)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:119)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:524)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:233)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1065)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:413)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:192)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:999)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:117)
    at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:250)
    at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:149)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:111)
    at org.eclipse.jetty.server.Server.handle(Server.java:351)
    at org.eclipse.jetty.server.AbstractHttpConnection.handleRequest(AbstractHttpConnection.java:454)
    at org.eclipse.jetty.server.AbstractHttpConnection.content(AbstractHttpConnection.java:900)
    at org.eclipse.jetty.server.AbstractHttpConnection$RequestHandler.content(AbstractHttpConnection.java:954)
    at org.eclipse.jetty.http.HttpParser.parseNext(HttpParser.java:857)
    at org.eclipse.jetty.http.HttpParser.parseAvailable(HttpParser.java:235)
    at org.eclipse.jetty.server.AsyncHttpConnection.handle(AsyncHttpConnection.java:77)
    at org.eclipse.jetty.io.nio.SelectChannelEndPoint.handle(SelectChannelEndPoint.java:609)
    at org.eclipse.jetty.io.nio.SelectChannelEndPoint$1.run(SelectChannelEndPoint.java:45)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:599)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:534)
    at java.lang.Thread.run(Thread.java:662)
Caused by: org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
    at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:71)
    at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:253)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:237)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:141)
    at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:298)
    at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
    at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000)
    at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:338)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:106)
    at org.springframework.orm.hibernate3.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:656)
    ... 51 more
Caused by: org.h2.jdbc.JdbcBatchUpdateException: Нарушение уникального индекса или первичного ключа: "CONSTRAINT_INDEX_2 ON PUBLIC.BOOKING(TICKET_ID) VALUES ( /* key:1 */ 2, 1, 1)"
Unique index or primary key violation: "CONSTRAINT_INDEX_2 ON PUBLIC.BOOKING(TICKET_ID) VALUES ( /* key:1 */ 2, 1, 1)"; SQL statement:
insert into booking (ticket_id, user_id, booking_id) values (?, ?, ?) [23505-173]
    at org.h2.jdbc.JdbcPreparedStatement.executeBatch(JdbcPreparedStatement.java:1167)
    at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:48)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:246)
    ... 59 more

Is there any ways to handle exceptions inside service layer?
Thank you.

3
Please post the stack trace of the exception that gets thrown. - Sotirios Delimanolis
Is it really matters? The main point is that I can't handle exceptions I need to. But I updated my post with example of exception when app tries to insert value which violate unique column. - Oleksandr Tarasenko
Try adding a finally block to the try-catch in the service method. What happens? - Sotirios Delimanolis
@Sotirios Delimanolis, I will try. But this behavior looks pretty strange as for me. - Oleksandr Tarasenko
@Sotirios Delimanolis code in final block executes. However code in catch block - not. Looks like hibernate transaction manager throws exception only after (at)Transaction methods(and current transaction) ends. Do you have any ideas how to make it throw exception inside method? - Oleksandr Tarasenko

3 Answers

2
votes

Take a look of hibernate FlushMode

Default HiberanateTemplate FlushMode is FlusMode.AUTO, so session synchronization will occur on commit or before some queries to prevent stale state.

You could change HibernateTemplate flush mode to FlushMode.ALWAYS, inefficient and not recomended (but will work as you expect now), or call Session.flush() at some points.

see HiberanateTemplate.setFlushMode() and Session.flush()

1
votes

You don't get DB exceptions like constraint violations before committing the TX and TX gets committed in transaction manager. The best solution would be handling TX manager exceptions using an afterThrowing aspect (AOP). Alternatively, you can call your @Transactional methods inside another non-transactional method in your service and have it catch the exceptions. Personally, I don't like the later as you will have to create a wrapper method for all your services.

1
votes

Or you can invoke your transactional code in explicit transaction mode.

E.g.

//No @transactional here
public void service() {

    PlatformTransactionManager txManager= ...;
    TransactionStatus tx = txManager.getTransaction(new DefaultTransactionDefinition());
    try{
        .... do something
        txManager.commit(tx);
    }catch(DataAccessException ex){
        txManager.rollback(tx);
        ... handle error
    }
}