package org.hibernate.test.jpa.lock; import org.hibernate.LockMode; import org.hibernate.Session; import org.hibernate.Transaction; import org.junit.Test; import org.hibernate.testing.DialectChecks; import org.hibernate.testing.RequiresDialectFeature; import org.hibernate.test.jpa.AbstractJPATest; import org.hibernate.test.jpa.Item; import org.hibernate.test.jpa.MyEntity; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Tests specifically relating to section 3.3.5.3 [Lock Modes] of the * JPA persistence specification (as of the <i>Proposed Final Draft</i>). * * @author Steve Ebersole */ @RequiresDialectFeature( DialectChecks.DoesReadCommittedNotCauseWritersToBlockReadersCheck.class ) public class JPALockTest extends AbstractJPATest { /** * Test the equivalent of EJB3 LockModeType.READ * <p/> * From the spec: * <p/> * If transaction T1 calls lock(entity, LockModeType.READ) on a versioned object, the entity * manager must ensure that neither of the following phenomena can occur:<ul> * <li>P1 (Dirty read): Transaction T1 modifies a row. Another transaction T2 then reads that row and * obtains the modified value, before T1 has committed or rolled back. Transaction T2 eventually * commits successfully; it does not matter whether T1 commits or rolls back and whether it does * so before or after T2 commits. * <li>P2 (Non-repeatable read): Transaction T1 reads a row. Another transaction T2 then modifies or * deletes that row, before T1 has committed. Both transactions eventually commit successfully. * <p/> * This will generally be achieved by the entity manager acquiring a lock on the underlying database row. * Any such lock may be obtained immediately (so long as it is retained until commit completes), or the * lock may be deferred until commit time (although even then it must be retained until the commit completes). * Any implementation that supports repeatable reads in a way that prevents the above phenomena * is permissible. * <p/> * The persistence implementation is not required to support calling lock(entity, LockMode-Type.READ) * on a non-versioned object. When it cannot support such a lock call, it must throw the * PersistenceException. When supported, whether for versioned or non-versioned objects, LockMode-Type.READ * must always prevent the phenomena P1 and P2. Applications that call lock(entity, LockModeType.READ) * on non-versioned objects will not be portable. * <p/> * EJB3 LockModeType.READ actually maps to the Hibernate LockMode.OPTIMISTIC */ @Test public void testLockModeTypeRead() { if ( !readCommittedIsolationMaintained( "ejb3 lock tests" ) ) { return; } final String initialName = "lock test"; // set up some test data Session s1 = sessionFactory().openSession(); Transaction t1 = s1.beginTransaction(); Item item = new Item(); item.setName( initialName ); s1.save( item ); t1.commit(); s1.close(); Long itemId = item.getId(); // do the isolated update s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); item = (Item) s1.get( Item.class, itemId ); s1.lock( item, LockMode.UPGRADE ); item.setName( "updated" ); s1.flush(); Session s2 = sessionFactory().openSession(); Transaction t2 = s2.beginTransaction(); Item item2 = (Item) s2.get( Item.class, itemId ); assertEquals( "isolation not maintained", initialName, item2.getName() ); t1.commit(); s1.close(); item2 = (Item) s2.get( Item.class, itemId ); assertEquals( "repeatable read not maintained", initialName, item2.getName() ); t2.commit(); s2.close(); s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); s1.delete( item ); t1.commit(); s1.close(); } /** * Test the equivalent of EJB3 LockModeType.WRITE * <p/> * From the spec: * <p/> * If transaction T1 calls lock(entity, LockModeType.WRITE) on a versioned object, the entity * manager must avoid the phenomena P1 and P2 (as with LockModeType.READ) and must also force * an update (increment) to the entity's version column. A forced version update may be performed immediately, * or may be deferred until a flush or commit. If an entity is removed before a deferred version * update was to have been applied, the forced version update is omitted, since the underlying database * row no longer exists. * <p/> * The persistence implementation is not required to support calling lock(entity, LockMode-Type.WRITE) * on a non-versioned object. When it cannot support a such lock call, it must throw the * PersistenceException. When supported, whether for versioned or non-versioned objects, LockMode-Type.WRITE * must always prevent the phenomena P1 and P2. For non-versioned objects, whether or * not LockModeType.WRITE has any additional behaviour is vendor-specific. Applications that call * lock(entity, LockModeType.WRITE) on non-versioned objects will not be portable. * <p/> * Due to the requirement that LockModeType.WRITE needs to force a version increment, * a new Hibernate LockMode was added to support this behavior: {@link org.hibernate.LockMode#FORCE}. */ @Test public void testLockModeTypeWrite() { if ( !readCommittedIsolationMaintained( "ejb3 lock tests" ) ) { return; } final String initialName = "lock test"; // set up some test data Session s1 = sessionFactory().openSession(); Transaction t1 = s1.beginTransaction(); Item item = new Item(); item.setName( initialName ); s1.save( item ); MyEntity myEntity = new MyEntity(); myEntity.setName( "Test" ); s1.save( myEntity ); t1.commit(); s1.close(); Long itemId = item.getId(); long initialVersion = item.getVersion(); s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); item = (Item) s1.get( Item.class, itemId ); s1.lock( item, LockMode.FORCE ); assertEquals( "no forced version increment", initialVersion + 1, item.getVersion() ); myEntity = (MyEntity) s1.get( MyEntity.class, myEntity.getId() ); s1.lock( myEntity, LockMode.FORCE ); assertTrue( "LockMode.FORCE on a un-versioned entity should degrade nicely to UPGRADE", true ); s1.lock( item, LockMode.FORCE ); assertEquals( "subsequent LockMode.FORCE did not no-op", initialVersion + 1, item.getVersion() ); Session s2 = sessionFactory().openSession(); Transaction t2 = s2.beginTransaction(); Item item2 = (Item) s2.get( Item.class, itemId ); assertEquals( "isolation not maintained", initialName, item2.getName() ); item.setName( "updated-1" ); s1.flush(); // currently an unfortunate side effect... assertEquals( initialVersion + 2, item.getVersion() ); t1.commit(); s1.close(); item2.setName( "updated" ); try { t2.commit(); fail( "optimistic lock should have failed" ); } catch (Throwable ignore) { // expected behavior t2.rollback(); } finally { s2.close(); } s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); s1.delete( item ); s1.delete( myEntity ); t1.commit(); s1.close(); } }