Consider this example in which I have created two JPA entities and used Spring Data JPA repositories to perform simple CRUD -
import java.sql.Timestamp;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractValueObject {
@Id
@GeneratedValue
private Long id;
@Version
@Column(name = "time_stamp")
private Timestamp version;
public Long getId() {
return id;
}
@Override
public String toString() {
if (id == null) {
return "";
}
return id.toString();
}
}
@Entity
@Table(name = "demo")
public class Demo extends AbstractValueObject {
private String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "demo")
private List<Owner> owners;
public Demo() {
owners = new ArrayList<>();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Owner> getOwners() {
return Collections.unmodifiableList(owners);
}
public void addOwner(Owner owner) {
this.owners.add(owner);
owner.setDemo(this);
}
public void addAllOwners(List<Owner> owners) {
this.owners.addAll(owners);
for (Owner owner : owners) {
owner.setDemo(this);
}
}
public void update(Demo demo) {
this.setName(demo.getName());
this.owners.clear();
this.addAllOwners(demo.getOwners());
}
}
@Entity
@Table(name = "owner")
public class Owner extends AbstractValueObject {
private String attribute;
@ManyToOne(cascade = CascadeType.ALL, optional = false)
@JoinColumn(name = "demo_id", nullable = false)
private Demo demo;
public String getAttribute() {
return attribute;
}
public void setAttribute(String attribute) {
this.attribute = attribute;
}
public Demo getDemo() {
return demo;
}
public void setDemo(Demo demo) {
this.demo = demo;
}
}
After that, I have created a JPA repository for the Demo
entity, extending from JpaRepository
-
import org.springframework.data.jpa.repository.JpaRepository;
public interface DemoRepository extends JpaRepository<Demo, Long> {}
Corresponding service implementation -
import javax.annotation.Resource;
import org.springframework.transaction.annotation.Transactional;
public class DemoServiceImpl implements DemoService {
@Resource
private DemoRepository demoRepository;
@Override
@Transactional
public Demo create(Demo demo) {
return demoRepository.save(demo);
}
@Override
@Transactional
public Demo update(long id, Demo demo) {
Demo dbDemo = demoRepository.findOne(id);
if (dbDemo == null) {
return demo;
}
dbDemo.update(demo);
return dbDemo;
}
@Transactional
public void testRun() {
Owner owner = new Owner();
owner.setAttribute("attribute");
Demo demo = new Demo();
demo.setName("demo");
demo.addOwner(owner);
this.create(demo);
demo.setName("another");
this.update(demo.getId(), demo);
}
}
persistence.xml file -
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="jpa-optimistic-locking" transaction-type="RESOURCE_LOCAL">
</persistence-unit>
</persistence>
Spring app-context.xml -
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:component-scan base-package="com.keertimaan.example.jpaoptimisticlocking" />
<jpa:repositories base-package="com.keertimaan.example.jpaoptimisticlocking.repository" />
<bean id="demoService" class="com.keertimaan.example.jpaoptimisticlocking.service.DemoServiceImpl" />
<!-- JPA/Database/Transaction Configuration -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />
<property name="user" value="root" />
<property name="password" value="admin123" />
<property name="minPoolSize" value="1" />
<property name="maxPoolSize" value="2" />
<property name="acquireIncrement" value="1" />
<property name="maxStatements" value="5" />
<property name="idleConnectionTestPeriod" value="500" />
<property name="maxIdleTime" value="1000" />
<property name="loginTimeout" value="800" />
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="jpa-optimistic-locking" />
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
<property name="persistenceProvider">
<bean class="org.hibernate.jpa.HibernatePersistenceProvider" />
</property>
<property name="jpaProperties">
<props>
<prop key="hibernate.hbm2ddl.auto">validate</prop>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
</props>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
Now whenever I try to update an entity like this on Windows 7 -
public class App {
public static void main(String[] args) {
DemoService demoService = (DemoService) SpringHelper.INSTANCE.getBean("demoService");
demoService.testRun();
}
}
I get an exception like this -
Exception in thread "main" org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.keertimaan.example.jpaoptimisticlocking.domain.Demo] with identifier [4]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.keertimaan.example.jpaoptimisticlocking.domain.Demo#4] at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:228) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:155) at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:519) at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:757) at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:726) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:478) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:272) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207) at com.sun.proxy.$Proxy31.testRun(Unknown Source) at com.keertimaan.example.jpaoptimisticlocking.App.main(App.java:9) Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.keertimaan.example.jpaoptimisticlocking.domain.Demo#4] 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:463) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:349) 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:1222) 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.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:77) at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:515) ... 9 more
If I run the same example in Ubuntu, then I get no exception at all and my application completes successfully. Why is that?
I am using Windowsw 7 64-bit edition -
OS Name: Microsoft Windows 7 Enterprise
OS Version: 6.1.7601 Service Pack 1 Build 7601
and my Ubuntu version is 12.04.5 64-bit edition.
JDK used in Windows: jdk7 update 75
JDK used in Ubuntu: jdk7 update 51
MySQL Server version in Windows: 5.6.23-log MySQL Community Server (GPL)
MySQL Server version in Ubuntu: 5.5.41-0ubuntu0.12.04.1 (Ubuntu)
Knowing how the optimistic locking version property works is very important when using JPA and Hibernate, as it allows you to prevent the lost update anomaly when a given entity is being modified by multiple concurrent users.
JPA provides us with two different optimistic lock modes (and two aliases): OPTIMISTIC obtains an optimistic read lock for all entities containing a version attribute. OPTIMISTIC_FORCE_INCREMENT obtains an optimistic lock the same as OPTIMISTIC and additionally increments the version attribute value. READ is a synonym for OPTIMISTIC.
JPA specification defines three pessimistic lock modes which we're going to discuss: PESSIMISTIC_READ – allows us to obtain a shared lock and prevent the data from being updated or deleted. PESSIMISTIC_WRITE – allows us to obtain an exclusive lock and prevent the data from being read, updated or deleted.
To achieve that, we can use an optimistic locking mechanism provided by Java Persistence API. This way, multiple updates made on the same data at the same time do not interfere with each other. 2. Understanding Optimistic Locking In order to use optimistic locking, we need to have an entity including a property with @Version annotation.
I have a feeling that this is related to the timestamp precision of MySQL 5.6. MySQL 5.6.4 introduced microsecond precision, which will cause a version mismatch, and the locking will fail.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With