/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.test.locking;
import java.util.concurrent.CountDownLatch;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.dialect.SQLServerDialect;
import org.hibernate.dialect.SybaseASE15Dialect;
import org.hibernate.testing.SkipForDialect;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.transaction.TransactionUtil;
import org.hibernate.testing.util.ExceptionUtil;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
/**
* Make sure that directly specifying lock modes, even though deprecated, continues to work until removed.
*
* @author Steve Ebersole
*/
@TestForIssue( jiraKey = "HHH-5275")
@SkipForDialect(value=SybaseASE15Dialect.class, strictMatching=true,
comment = "skip this test on Sybase ASE 15.5, but run it on 15.7, see HHH-6820")
public class LockModeTest extends BaseCoreFunctionalTestCase {
private Long id;
private CountDownLatch endLatch = new CountDownLatch( 1 );
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { A.class };
}
@Override
public void prepareTest() throws Exception {
doInHibernate( this::sessionFactory, session -> {
id = (Long) session.save( new A( "it" ) );
} );
}
@Override
protected boolean isCleanupTestDataRequired(){return true;}
@Test
@SuppressWarnings( {"deprecation"})
public void testLoading() {
// open a session, begin a transaction and lock row
doInHibernate( this::sessionFactory, session -> {
A it = session.byId( A.class ).with( LockOptions.UPGRADE ).load( id );
// make sure we got it
assertNotNull( it );
// that initial transaction is still active and so the lock should still be held.
// Lets open another session/transaction and verify that we cannot update the row
nowAttemptToUpdateRow();
} );
}
@Test
public void testLegacyCriteria() {
// open a session, begin a transaction and lock row
doInHibernate( this::sessionFactory, session -> {
A it = (A) session.createCriteria( A.class )
.setLockMode( LockMode.PESSIMISTIC_WRITE )
.uniqueResult();
// make sure we got it
assertNotNull( it );
// that initial transaction is still active and so the lock should still be held.
// Lets open another session/transaction and verify that we cannot update the row
nowAttemptToUpdateRow();
} );
}
@Test
public void testLegacyCriteriaAliasSpecific() {
// open a session, begin a transaction and lock row
doInHibernate( this::sessionFactory, session -> {
A it = (A) session.createCriteria( A.class )
.setLockMode( "this", LockMode.PESSIMISTIC_WRITE )
.uniqueResult();
// make sure we got it
assertNotNull( it );
// that initial transaction is still active and so the lock should still be held.
// Lets open another session/transaction and verify that we cannot update the row
nowAttemptToUpdateRow();
} );
}
@Test
public void testQuery() {
// open a session, begin a transaction and lock row
doInHibernate( this::sessionFactory, session -> {
A it = (A) session.createQuery( "from A a" )
.setLockMode( "a", LockMode.PESSIMISTIC_WRITE )
.uniqueResult();
// make sure we got it
assertNotNull( it );
// that initial transaction is still active and so the lock should still be held.
// Lets open another session/transaction and verify that we cannot update the row
nowAttemptToUpdateRow();
} );
}
@Test
public void testQueryUsingLockOptions() {
// todo : need an association here to make sure the alias-specific lock modes are applied correctly
doInHibernate( this::sessionFactory, session -> {
session.createQuery( "from A a" )
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) )
.uniqueResult();
session.createQuery( "from A a" )
.setLockOptions( new LockOptions().setAliasSpecificLockMode( "a", LockMode.PESSIMISTIC_WRITE ) )
.uniqueResult();
} );
}
@Test
@TestForIssue(jiraKey = "HHH-2735")
public void testQueryLockModeNoneWithAlias() {
doInHibernate( this::sessionFactory, session -> {
// shouldn't throw an exception
session.createQuery( "SELECT a.value FROM A a where a.id = :id" )
.setLockMode( "a", LockMode.NONE )
.setParameter( "id", 1L )
.list();
} );
}
@Test
@TestForIssue(jiraKey = "HHH-2735")
public void testQueryLockModePessimisticWriteWithAlias() {
doInHibernate( this::sessionFactory, session -> {
// shouldn't throw an exception
session.createQuery( "SELECT MAX(a.id)+1 FROM A a where a.value = :value" )
.setLockMode( "a", LockMode.PESSIMISTIC_WRITE )
.setParameter( "value", "it" )
.list();
} );
}
private void nowAttemptToUpdateRow() {
// here we just need to open a new connection (database session and transaction) and make sure that
// we are not allowed to acquire exclusive locks to that row and/or write to that row. That may take
// one of two forms:
// 1) either the get-with-lock or the update fails immediately with a sql error
// 2) either the get-with-lock or the update blocks indefinitely (in real world, it would block
// until the txn in the calling method completed.
// To be able to cater to the second type, we run this block in a separate thread to be able to "time it out"
try {
executeSync( () -> {
doInHibernate( this::sessionFactory, _session -> {
TransactionUtil.setJdbcTimeout( _session );
try {
// load with write lock to deal with databases that block (wait indefinitely) direct attempts
// to write a locked row
A it = _session.get(
A.class,
id,
new LockOptions( LockMode.PESSIMISTIC_WRITE ).setTimeOut( LockOptions.NO_WAIT )
);
_session.createNativeQuery( updateStatement() )
.setParameter( "value", "changed" )
.setParameter( "id", it.getId() )
.executeUpdate();
fail( "Pessimistic lock not obtained/held" );
}
catch ( Exception e ) {
if ( !ExceptionUtil.isSqlLockTimeout( e) ) {
fail( "Unexpected error type testing pessimistic locking : " + e.getClass().getName() );
}
}
} );
} );
}
catch (Exception e) {
//MariaDB throws a time out nd closes the underlying connection
if( !ExceptionUtil.isConnectionClose(e)) {
fail("Unknown exception thrown: " + e.getMessage());
}
}
}
protected String updateStatement() {
if( SQLServerDialect.class.isAssignableFrom( DIALECT.getClass() ) ) {
return "UPDATE T_LOCK_A WITH(NOWAIT) SET a_value = :value where id = :id";
}
return "UPDATE T_LOCK_A SET a_value = :value where id = :id";
}
}