/* * Hibernate, Relational Persistence for Idiomatic Java * * Copyright (c) 2011, Red Hat Inc. or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat Inc. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package org.hibernate.test.jpa.lock; import java.math.BigDecimal; import org.hibernate.LockMode; import org.hibernate.Session; import org.hibernate.StaleObjectStateException; import org.hibernate.Transaction; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.exception.SQLGrammarException; 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.Part; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Test that the Hibernate Session complies with REPEATABLE_READ isolation * semantics. * * @author Steve Ebersole */ @RequiresDialectFeature( DialectChecks.DoesReadCommittedNotCauseWritersToBlockReadersCheck.class ) public class RepeatableReadTest extends AbstractJPATest { @Test public void testStaleVersionedInstanceFoundInQueryResult() { String check = "EJB3 Specification"; Session s1 = sessionFactory().openSession(); Transaction t1 = s1.beginTransaction(); Item item = new Item( check ); s1.save( item ); t1.commit(); s1.close(); Long itemId = item.getId(); long initialVersion = item.getVersion(); // Now, open a new Session and re-load the item... s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); item = ( Item ) s1.get( Item.class, itemId ); // now that the item is associated with the persistence-context of that session, // open a new session and modify it "behind the back" of the first session Session s2 = sessionFactory().openSession(); Transaction t2 = s2.beginTransaction(); Item item2 = ( Item ) s2.get( Item.class, itemId ); item2.setName( "EJB3 Persistence Spec" ); t2.commit(); s2.close(); // at this point, s1 now contains stale data, so try an hql query which // returns said item and make sure we get the previously associated state // (i.e., the old name and the old version) item2 = ( Item ) s1.createQuery( "select i from Item i" ).list().get( 0 ); assertTrue( item == item2 ); assertEquals( "encountered non-repeatable read", check, item2.getName() ); assertEquals( "encountered non-repeatable read", initialVersion, item2.getVersion() ); t1.commit(); s1.close(); // clean up s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); s1.createQuery( "delete Item" ).executeUpdate(); t1.commit(); s1.close(); } @Test public void testStaleVersionedInstanceFoundOnLock() { if ( ! readCommittedIsolationMaintained( "repeatable read tests" ) ) { return; } String check = "EJB3 Specification"; Session s1 = sessionFactory().openSession(); Transaction t1 = s1.beginTransaction(); Item item = new Item( check ); s1.save( item ); t1.commit(); s1.close(); Long itemId = item.getId(); long initialVersion = item.getVersion(); // Now, open a new Session and re-load the item... s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); item = ( Item ) s1.get( Item.class, itemId ); // now that the item is associated with the persistence-context of that session, // open a new session and modify it "behind the back" of the first session Session s2 = sessionFactory().openSession(); Transaction t2 = s2.beginTransaction(); Item item2 = ( Item ) s2.get( Item.class, itemId ); item2.setName( "EJB3 Persistence Spec" ); t2.commit(); s2.close(); // at this point, s1 now contains stale data, so acquire a READ lock // and make sure we get the already associated state (i.e., the old // name and the old version) s1.lock( item, LockMode.READ ); item2 = ( Item ) s1.get( Item.class, itemId ); assertTrue( item == item2 ); assertEquals( "encountered non-repeatable read", check, item2.getName() ); assertEquals( "encountered non-repeatable read", initialVersion, item2.getVersion() ); // attempt to acquire an UPGRADE lock; this should fail try { s1.lock( item, LockMode.UPGRADE ); fail( "expected UPGRADE lock failure" ); } catch( StaleObjectStateException expected ) { // this is the expected behavior } catch( SQLGrammarException t ) { if ( getDialect() instanceof SQLServerDialect ) { // sql-server (using snapshot isolation) reports this as a grammar exception /:) // // not to mention that it seems to "lose track" of the transaction in this scenario... t1.rollback(); t1 = s1.beginTransaction(); } else { throw t; } } t1.commit(); s1.close(); // clean up s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); s1.createQuery( "delete Item" ).executeUpdate(); t1.commit(); s1.close(); } @Test public void testStaleNonVersionedInstanceFoundInQueryResult() { String check = "Lock Modes"; Session s1 = sessionFactory().openSession(); Transaction t1 = s1.beginTransaction(); Part part = new Part( new Item( "EJB3 Specification" ), check, "3.3.5.3", new BigDecimal( 0.0 ) ); s1.save( part ); t1.commit(); s1.close(); Long partId = part.getId(); // Now, open a new Session and re-load the part... s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); part = ( Part ) s1.get( Part.class, partId ); // now that the item is associated with the persistence-context of that session, // open a new session and modify it "behind the back" of the first session Session s2 = sessionFactory().openSession(); Transaction t2 = s2.beginTransaction(); Part part2 = ( Part ) s2.get( Part.class, partId ); part2.setName( "Lock Mode Types" ); t2.commit(); s2.close(); // at this point, s1 now contains stale data, so try an hql query which // returns said part and make sure we get the previously associated state // (i.e., the old name) part2 = ( Part ) s1.createQuery( "select p from Part p" ).list().get( 0 ); assertTrue( part == part2 ); assertEquals( "encountered non-repeatable read", check, part2.getName() ); t1.commit(); s1.close(); // clean up s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); s1.delete( part2 ); s1.delete( part2.getItem() ); t1.commit(); s1.close(); } @Test public void testStaleNonVersionedInstanceFoundOnLock() { if ( ! readCommittedIsolationMaintained( "repeatable read tests" ) ) { return; } String check = "Lock Modes"; Session s1 = sessionFactory().openSession(); Transaction t1 = s1.beginTransaction(); Part part = new Part( new Item( "EJB3 Specification" ), check, "3.3.5.3", new BigDecimal( 0.0 ) ); s1.save( part ); t1.commit(); s1.close(); Long partId = part.getId(); // Now, open a new Session and re-load the part... s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); part = ( Part ) s1.get( Part.class, partId ); // now that the item is associated with the persistence-context of that session, // open a new session and modify it "behind the back" of the first session Session s2 = sessionFactory().openSession(); Transaction t2 = s2.beginTransaction(); Part part2 = ( Part ) s2.get( Part.class, partId ); part2.setName( "Lock Mode Types" ); t2.commit(); s2.close(); // at this point, s1 now contains stale data, so acquire a READ lock // and make sure we get the already associated state (i.e., the old // name and the old version) s1.lock( part, LockMode.READ ); part2 = ( Part ) s1.get( Part.class, partId ); assertTrue( part == part2 ); assertEquals( "encountered non-repeatable read", check, part2.getName() ); // then acquire an UPGRADE lock; this should fail try { s1.lock( part, LockMode.UPGRADE ); } catch( Throwable t ) { // SQLServer, for example, immediately throws an exception here... t1.rollback(); t1 = s1.beginTransaction(); } part2 = ( Part ) s1.get( Part.class, partId ); assertTrue( part == part2 ); assertEquals( "encountered non-repeatable read", check, part2.getName() ); t1.commit(); s1.close(); // clean up s1 = sessionFactory().openSession(); t1 = s1.beginTransaction(); s1.delete( part ); s1.delete( part.getItem() ); t1.commit(); s1.close(); } }