I want to use the optimistic locking feature present in hibernate. To do this I configured the mapping for my table as follows:
<hibernate-mapping package="org.sandbox.optimisticlocking.dao.entity"> <class name="EmployeeEntity" table="employee"> <id name="id" column="employee_id"> <generator class="identity"/> </id> <version name="version" column="version" type="java.lang.Integer" generated="always"/> <property name="firstName" column="first_name" type="java.lang.String"/>
<set name="projects" table="employee_to_project" inverse="true"> <key column="employee_id"/> <many-to-many column="project_id" class="ProjectEntity"/> </set> </class> </hibernate-mapping>
I used generated="always" and created the following trigger in db:
CREATE OR REPLACE FUNCTION public.tab_employee_update_version() RETURNS trigger LANGUAGE plpgsql AS $body$ BEGIN NEW.version = coalesce(OLD.version,0) + 1; RETURN NEW; END; $body$ /
Then I run the following code:
public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("optimisticlocking-context.xml");
final EmployeeService employeeService = ctx.getBean("employeeService", EmployeeService.class);
// INSERT final Integer employeeId = employeeService.save(createEmployee());
// SELECT final Employee employee = employeeService.getById(employeeId);
// UPDATE employeeService.update(employee);
and get the HibernateOptimisticLockingFailureException despite the fact that employee record was not changed by another transaction meantime:
Exception in thread "main" org.springframework.orm.hibernate4.HibernateOptimisticLockingFailureException: Object of class [org.sandbox.optimisticlocking.dao.entity.EmployeeEntity] with identifier [1]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : org.sandbox.optimisticlocking.dao.entity.EmployeeEntity#1 at org.springframework.orm.hibernate4.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:202) at org.springframework.orm.hibernate4.HibernateTransactionManager.convertHibernateAccessException(HibernateTransactionManager.java:730) at org.springframework.orm.hibernate4.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:592) at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:761) at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:730) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:485) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:291) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207) at com.sun.proxy.$Proxy15.update(Unknown Source) at org.sandbox.optimisticlocking.App.main(App.java:22) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134) Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : org.sandbox.optimisticlocking.dao.entity.EmployeeEntity#1 at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:2541) at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3285) at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3183) at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3525) at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:159) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:465) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:351) at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:350) at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:56) at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1258) at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:425) at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.beforeTransactionCommit(JdbcTransaction.java:101) at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.commit(AbstractTransactionImpl.java:177) at org.springframework.orm.hibernate4.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:584) ... 14 more
Transaction boundaries are on service level (the EmployeeService class is marked with the @Transactional annotation). I debugged the code and found out, that hibernate automatically increases the version during the update:
Hibernate: insert into employee (employee_id, first_name) values (null, ?) 14:35:24,724 TRACE main sql.BasicBinder:81 - binding parameter [1] as [VARCHAR] - [Piotr] Hibernate: select employeeen_.version as version2_0_ from employee employeeen_ where employeeen_.employee_id=? 14:35:24,730 TRACE main sql.BasicBinder:81 - binding parameter [1] as [INTEGER] - [1] 14:35:24,732 TRACE main sql.BasicExtractor:78 - extracted value ([version2_0_] : [INTEGER]) - [0]
Hibernate: select employeeen0_.employee_id as employee1_0_0_, employeeen0_.version as version2_0_0_, employeeen0_.first_name as first_na3_0_0_ from employee employeeen0_ where employeeen0_.employee_id=? 14:35:24,745 TRACE main sql.BasicBinder:81 - binding parameter [1] as [INTEGER] - [1] 14:35:24,749 TRACE main sql.BasicExtractor:78 - extracted value ([version2_0_0_] : [INTEGER]) - [0] 14:35:24,750 TRACE main sql.BasicExtractor:78 - extracted value ([first_na3_0_0_] : [VARCHAR]) - [Piotr] 14:35:24,756 TRACE main type.CollectionType:783 - Created collection wrapper: org.sandbox.optimisticlocking.dao.entity.EmployeeEntity.projects#1 Hibernate: select projects0_.employee_id as employee1_0_0_, projects0_.project_id as project_2_1_0_, projectent1_.project_id as project_1_2_1_, projectent1_.name as name2_2_1_ from employee_to_project projects0_ inner join project projectent1_ on projects0_.project_id=projectent1_.project_id where projects0_.employee_id=? 14:35:24,762 TRACE main sql.BasicBinder:81 - binding parameter [1] as [INTEGER] - [1]
Hibernate: update employee set first_name=? where employee_id=? and version=? 14:35:24,773 TRACE main sql.BasicBinder:81 - binding parameter [1] as [VARCHAR] - [Piotr] 14:35:24,773 TRACE main sql.BasicBinder:81 - binding parameter [2] as [INTEGER] - [1] 14:35:24,773 TRACE main sql.BasicBinder:81 - binding parameter [3] as [INTEGER] - [1]
Additionally, I found out that if I select and update a record in the same transaction, hibernate does not increase the version and everything works fine. It works fine also after removing the 'set' element from the mapping file:
<set name="projects" table="employee_to_project" inverse="true"> <key column="employee_id"/> <many-to-many column="project_id" class="ProjectEntity"/> </set>
I am attaching an application, which exposes the error. To reproduce the bug run the App.java class. |