clean up locking strategies

- remove duplicated code
- handle UPGRADE_NOWAIT and UPGRADE_SKIPLOCKED as flavors of
  PessimisticWriteSelectLockingStrategy
- improve Javadoc

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-05-14 10:21:05 +02:00
parent 1b67ebee60
commit 54d34a445c
9 changed files with 148 additions and 274 deletions

View File

@ -808,14 +808,28 @@ public interface Session extends SharedSessionContract, EntityManager {
/**
* Obtain the specified lock level on the given managed instance associated
* with this session. This may be used to:
* with this session. This operation may be used to:
* <ul>
* <li>perform a version check with {@link LockMode#READ}, or
* <li>upgrade to a pessimistic lock with {@link LockMode#PESSIMISTIC_WRITE}).
* <li>perform a version check on an entity read from the second-level cache
* by requesting {@link LockMode#READ},
* <li>schedule a version check at transaction commit by requesting
* {@link LockMode#OPTIMISTIC},
* <li>schedule a version increment at transaction commit by requesting
* {@link LockMode#OPTIMISTIC_FORCE_INCREMENT}
* <li>upgrade to a pessimistic lock with {@link LockMode#PESSIMISTIC_READ}
* or {@link LockMode#PESSIMISTIC_WRITE}, or
* <li>immediately increment the version of the given instance by requesting
* {@link LockMode#PESSIMISTIC_FORCE_INCREMENT}.
* </ul>
* <p>
* If the requested lock mode is already held on the given entity, this
* operation has no effect.
* <p>
* This operation cascades to associated instances if the association is
* mapped with {@link org.hibernate.annotations.CascadeType#LOCK}.
* <p>
* The modes {@link LockMode#WRITE} and {@link LockMode#UPGRADE_SKIPLOCKED}
* are not legal arguments to {@code lock()}.
*
* @param object a persistent instance
* @param lockMode the lock level

View File

@ -34,7 +34,7 @@ public class EntityVerifyVersionProcess implements BeforeTransactionCompletionPr
@Override
public void doBeforeTransactionCompletion(SessionImplementor session) {
final EntityEntry entry = session.getPersistenceContext().getEntry( object );
// Don't check version for an entity that is not in the PersistenceContext;
// Don't check version for an entity that is not in the PersistenceContext
if ( entry != null ) {
final Object latestVersion = entry.getPersister().getCurrentVersion( entry.getId(), session );
if ( !entry.getVersion().equals( latestVersion ) ) {

View File

@ -2018,16 +2018,21 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun
switch ( lockMode ) {
case PESSIMISTIC_FORCE_INCREMENT:
return new PessimisticForceIncrementLockingStrategy( lockable, lockMode );
case UPGRADE_NOWAIT:
case UPGRADE_SKIPLOCKED:
case PESSIMISTIC_WRITE:
return new PessimisticWriteSelectLockingStrategy( lockable, lockMode );
case PESSIMISTIC_READ:
return new PessimisticReadSelectLockingStrategy( lockable, lockMode );
case OPTIMISTIC:
return new OptimisticLockingStrategy( lockable, lockMode );
case OPTIMISTIC_FORCE_INCREMENT:
return new OptimisticForceIncrementLockingStrategy( lockable, lockMode );
default:
case OPTIMISTIC:
return new OptimisticLockingStrategy( lockable, lockMode );
case READ:
return new SelectLockingStrategy( lockable, lockMode );
default:
// WRITE, NONE are not allowed here
throw new IllegalArgumentException( "Unsupported lock mode" );
}
}

View File

@ -6,13 +6,29 @@
*/
package org.hibernate.dialect.lock;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.StaleObjectStateException;
import org.hibernate.engine.jdbc.spi.JdbcCoordinator;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.spi.EventSource;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.sql.SimpleSelect;
import org.hibernate.stat.spi.StatisticsImplementor;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import static org.hibernate.pretty.MessageHelper.infoString;
/**
* Base {@link LockingStrategy} implementation to support implementations
* based on issuing {@code SQL} {@code SELECT} statements
* based on issuing SQL {@code SELECT} statements. For non-read locks,
* this is achieved via the dialect's native {@code SELECT ... FOR UPDATE}
* syntax.
*
* @author Steve Ebersole
*/
@ -35,20 +51,88 @@ public abstract class AbstractSelectLockingStrategy implements LockingStrategy {
return lockMode;
}
protected abstract String generateLockString(int lockTimeout);
protected String generateLockString(int lockTimeout) {
final SessionFactoryImplementor factory = lockable.getFactory();
final LockOptions lockOptions = new LockOptions( lockMode );
lockOptions.setTimeOut( lockTimeout );
final SimpleSelect select =
new SimpleSelect( factory )
.setLockOptions( lockOptions )
.setTableName( lockable.getRootTableName() )
.addColumn( lockable.getRootTableIdentifierColumnNames()[0] )
.addRestriction( lockable.getRootTableIdentifierColumnNames() );
if ( lockable.isVersioned() ) {
select.addRestriction( lockable.getVersionColumnName() );
}
if ( factory.getSessionFactoryOptions().isCommentsEnabled() ) {
select.setComment( lockMode + " lock " + lockable.getEntityName() );
}
return select.toStatementString();
}
@Override
public void lock(Object id, Object version, Object object, int timeout, EventSource session)
throws StaleObjectStateException, JDBCException {
final String sql = determineSql( timeout );
final SessionFactoryImplementor factory = session.getFactory();
final Lockable lockable = getLockable();
try {
final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator();
final PreparedStatement st = jdbcCoordinator.getStatementPreparer().prepareStatement( sql );
try {
lockable.getIdentifierType().nullSafeSet( st, id, 1, session );
if ( lockable.isVersioned() ) {
lockable.getVersionType().nullSafeSet(
st,
version,
lockable.getIdentifierType().getColumnSpan( factory ) + 1,
session
);
}
final ResultSet rs = jdbcCoordinator.getResultSetReturn().extract( st, sql );
try {
if ( !rs.next() ) {
final StatisticsImplementor statistics = factory.getStatistics();
if ( statistics.isStatisticsEnabled() ) {
statistics.optimisticFailure( lockable.getEntityName() );
}
throw new StaleObjectStateException( lockable.getEntityName(), id );
}
}
finally {
jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( rs, st );
}
}
finally {
jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( st );
jdbcCoordinator.afterStatementExecution();
}
}
catch ( SQLException sqle ) {
throw convertException( object, jdbcException( id, session, sqle, sql ) );
}
}
private JDBCException jdbcException(Object id, EventSource session, SQLException sqle, String sql) {
return session.getJdbcServices().getSqlExceptionHelper()
.convert( sqle, "could not lock: " + infoString( lockable, id, session.getFactory() ), sql );
}
protected HibernateException convertException(Object entity, JDBCException ex) {
return ex;
}
protected String determineSql(int timeout) {
if ( timeout == LockOptions.WAIT_FOREVER) {
return waitForeverSql;
}
else if ( timeout == LockOptions.NO_WAIT) {
return getNoWaitSql();
}
else if ( timeout == LockOptions.SKIP_LOCKED) {
return getSkipLockedSql();
}
else {
return generateLockString( timeout );
switch (timeout) {
case LockOptions.WAIT_FOREVER:
return waitForeverSql;
case LockOptions.NO_WAIT:
return getNoWaitSql();
case LockOptions.SKIP_LOCKED:
return getSkipLockedSql();
default:
return generateLockString( timeout );
}
}

View File

@ -46,7 +46,7 @@ public class OptimisticForceIncrementLockingStrategy implements LockingStrategy
if ( !lockable.isVersioned() ) {
throw new HibernateException( "[" + lockMode + "] not supported for non-versioned entities [" + lockable.getEntityName() + "]" );
}
final EntityEntry entry = session.getPersistenceContextInternal().getEntry( object );
// final EntityEntry entry = session.getPersistenceContextInternal().getEntry( object );
// Register the EntityIncrementVersionProcess action to run just prior to transaction commit.
session.getActionQueue().registerProcess( new EntityIncrementVersionProcess( object ) );
}

View File

@ -6,32 +6,18 @@
*/
package org.hibernate.dialect.lock;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.StaleObjectStateException;
import org.hibernate.engine.jdbc.spi.JdbcCoordinator;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.spi.EventSource;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.sql.SimpleSelect;
import org.hibernate.stat.spi.StatisticsImplementor;
/**
* A pessimistic locking strategy where a lock is obtained via a
* select statements.
* A pessimistic locking strategy where {@link LockMode#PESSIMISTIC_READ}
* is obtained via a select statement.
* <p>
* For non-read locks, this is achieved through the dialect's native
* {@code SELECT ... FOR UPDATE} syntax.
* <p>
* This strategy is valid for {@link LockMode#PESSIMISTIC_READ}.
* <p>
* This class is a clone of {@link SelectLockingStrategy}.
* Differs from {@link SelectLockingStrategy} in throwing
* {@link PessimisticEntityLockException}.
*
* @author Steve Ebersole
* @author Scott Marlow
@ -53,68 +39,7 @@ public class PessimisticReadSelectLockingStrategy extends AbstractSelectLockingS
}
@Override
public void lock(Object id, Object version, Object object, int timeout, EventSource session) {
final String sql = determineSql( timeout );
final SessionFactoryImplementor factory = session.getFactory();
try {
final Lockable lockable = getLockable();
try {
final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator();
final PreparedStatement st = jdbcCoordinator.getStatementPreparer().prepareStatement( sql );
try {
lockable.getIdentifierType().nullSafeSet( st, id, 1, session );
if ( lockable.isVersioned() ) {
lockable.getVersionType().nullSafeSet(
st,
version,
lockable.getIdentifierType().getColumnSpan( factory ) + 1,
session
);
}
final ResultSet rs = jdbcCoordinator.getResultSetReturn().extract( st, sql );
if ( !rs.next() ) {
final StatisticsImplementor statistics = factory.getStatistics();
if ( statistics.isStatisticsEnabled() ) {
statistics.optimisticFailure( lockable.getEntityName() );
}
throw new StaleObjectStateException( lockable.getEntityName(), id );
}
}
finally {
jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( st );
jdbcCoordinator.afterStatementExecution();
}
}
catch ( SQLException e ) {
throw session.getJdbcServices().getSqlExceptionHelper().convert(
e,
"could not lock: " + MessageHelper.infoString( lockable, id, session.getFactory() ),
sql
);
}
}
catch (JDBCException e) {
throw new PessimisticEntityLockException( object, "could not obtain pessimistic lock", e );
}
}
protected String generateLockString(int lockTimeout) {
final SessionFactoryImplementor factory = getLockable().getFactory();
final LockOptions lockOptions = new LockOptions( getLockMode() );
lockOptions.setTimeOut( lockTimeout );
final SimpleSelect select = new SimpleSelect( factory )
.setLockOptions( lockOptions )
.setTableName( getLockable().getRootTableName() )
.addColumn( getLockable().getRootTableIdentifierColumnNames()[0] )
.addRestriction( getLockable().getRootTableIdentifierColumnNames() );
if ( getLockable().isVersioned() ) {
select.addRestriction( getLockable().getVersionColumnName() );
}
if ( factory.getSessionFactoryOptions().isCommentsEnabled() ) {
select.setComment( getLockMode() + " lock " + getLockable().getEntityName() );
}
return select.toStatementString();
protected HibernateException convertException(Object entity, JDBCException ex) {
return new PessimisticEntityLockException( entity, "could not obtain pessimistic lock", ex );
}
}

View File

@ -6,32 +6,18 @@
*/
package org.hibernate.dialect.lock;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.StaleObjectStateException;
import org.hibernate.engine.jdbc.spi.JdbcCoordinator;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.spi.EventSource;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.sql.SimpleSelect;
import org.hibernate.stat.spi.StatisticsImplementor;
/**
* A pessimistic locking strategy where a lock is obtained via a
* select statement.
* A pessimistic locking strategy where {@link LockMode#PESSIMISTIC_WRITE}
* lock is obtained via a select statement.
* <p>
* For non-read locks, this is achieved through the dialect's native
* {@code SELECT ... FOR UPDATE} syntax.
* <p>
* This strategy is valid for {@link LockMode#PESSIMISTIC_WRITE}.
* <p>
* This class is a clone of {@link SelectLockingStrategy}.
* Differs from {@link SelectLockingStrategy} in throwing
* {@link PessimisticEntityLockException}.
*
* @see org.hibernate.dialect.Dialect#getForUpdateString(LockMode)
* @see org.hibernate.dialect.Dialect#appendLockHint(LockOptions, String)
@ -52,72 +38,7 @@ public class PessimisticWriteSelectLockingStrategy extends AbstractSelectLocking
}
@Override
public void lock(Object id, Object version, Object object, int timeout, EventSource session) {
final String sql = determineSql( timeout );
final SessionFactoryImplementor factory = session.getFactory();
try {
final Lockable lockable = getLockable();
try {
final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator();
final PreparedStatement st = jdbcCoordinator.getStatementPreparer().prepareStatement( sql );
try {
lockable.getIdentifierType().nullSafeSet( st, id, 1, session );
if ( lockable.isVersioned() ) {
lockable.getVersionType().nullSafeSet(
st,
version,
lockable.getIdentifierType().getColumnSpan( factory ) + 1,
session
);
}
final ResultSet rs = jdbcCoordinator.getResultSetReturn().extract( st, sql );
try {
if ( !rs.next() ) {
final StatisticsImplementor statistics = factory.getStatistics();
if ( statistics.isStatisticsEnabled() ) {
statistics.optimisticFailure( lockable.getEntityName() );
}
throw new StaleObjectStateException( lockable.getEntityName(), id );
}
}
finally {
jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( rs, st );
}
}
finally {
jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( st );
jdbcCoordinator.afterStatementExecution();
}
}
catch ( SQLException e ) {
throw session.getJdbcServices().getSqlExceptionHelper().convert(
e,
"could not lock: " + MessageHelper.infoString( lockable, id, session.getFactory() ),
sql
);
}
}
catch (JDBCException e) {
throw new PessimisticEntityLockException( object, "could not obtain pessimistic lock", e );
}
}
protected String generateLockString(int lockTimeout) {
final SessionFactoryImplementor factory = getLockable().getFactory();
final LockOptions lockOptions = new LockOptions( getLockMode() );
lockOptions.setTimeOut( lockTimeout );
final SimpleSelect select = new SimpleSelect( factory )
.setLockOptions( lockOptions )
.setTableName( getLockable().getRootTableName() )
.addColumn( getLockable().getRootTableIdentifierColumnNames()[0] )
.addRestriction( getLockable().getRootTableIdentifierColumnNames() );
if ( getLockable().isVersioned() ) {
select.addRestriction( getLockable().getVersionColumnName() );
}
if ( factory.getSessionFactoryOptions().isCommentsEnabled() ) {
select.setComment( getLockMode() + " lock " + getLockable().getEntityName() );
}
return select.toStatementString();
protected HibernateException convertException(Object entity, JDBCException ex) {
return new PessimisticEntityLockException( entity, "could not obtain pessimistic lock", ex );
}
}

View File

@ -6,27 +6,19 @@
*/
package org.hibernate.dialect.lock;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.StaleObjectStateException;
import org.hibernate.engine.jdbc.spi.JdbcCoordinator;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.spi.EventSource;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.sql.SimpleSelect;
import org.hibernate.stat.spi.StatisticsImplementor;
/**
* A locking strategy where a lock is obtained via a select statement.
/**
* A locking strategy where an optimistic lock is obtained via a select
* statement.
* <p>
* For non-read locks, this is achieved through the dialect's native
* {@code SELECT ... FOR UPDATE} syntax.
* Differs from {@link PessimisticWriteSelectLockingStrategy} and
* {@link PessimisticReadSelectLockingStrategy} in throwing
* {@link OptimisticEntityLockException}.
*
* @see org.hibernate.dialect.Dialect#getForUpdateString(LockMode)
* @see org.hibernate.dialect.Dialect#appendLockHint(LockOptions, String)
@ -46,73 +38,7 @@ public class SelectLockingStrategy extends AbstractSelectLockingStrategy {
}
@Override
public void lock(
Object id,
Object version,
Object object,
int timeout,
EventSource session) throws StaleObjectStateException, JDBCException {
final String sql = determineSql( timeout );
final SessionFactoryImplementor factory = session.getFactory();
final Lockable lockable = getLockable();
try {
final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator();
final PreparedStatement st = jdbcCoordinator.getStatementPreparer().prepareStatement( sql );
try {
lockable.getIdentifierType().nullSafeSet( st, id, 1, session );
if ( lockable.isVersioned() ) {
lockable.getVersionType().nullSafeSet(
st,
version,
lockable.getIdentifierType().getColumnSpan( factory ) + 1,
session
);
}
final ResultSet rs = jdbcCoordinator.getResultSetReturn().extract( st, sql );
try {
if ( !rs.next() ) {
final StatisticsImplementor statistics = factory.getStatistics();
if ( statistics.isStatisticsEnabled() ) {
statistics.optimisticFailure( lockable.getEntityName() );
}
throw new StaleObjectStateException( lockable.getEntityName(), id );
}
}
finally {
jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( rs, st );
}
}
finally {
jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( st );
jdbcCoordinator.afterStatementExecution();
}
}
catch ( SQLException sqle ) {
throw session.getJdbcServices().getSqlExceptionHelper().convert(
sqle,
"could not lock: " + MessageHelper.infoString( lockable, id, session.getFactory() ),
sql
);
}
}
protected String generateLockString(int timeout) {
final SessionFactoryImplementor factory = getLockable().getFactory();
final LockOptions lockOptions = new LockOptions( getLockMode() );
lockOptions.setTimeOut( timeout );
final SimpleSelect select = new SimpleSelect( factory )
.setLockOptions( lockOptions )
.setTableName( getLockable().getRootTableName() )
.addColumn( getLockable().getRootTableIdentifierColumnNames()[0] )
.addRestriction( getLockable().getRootTableIdentifierColumnNames() );
if ( getLockable().isVersioned() ) {
select.addRestriction( getLockable().getVersionColumnName() );
}
if ( factory.getSessionFactoryOptions().isCommentsEnabled() ) {
select.setComment( getLockMode() + " lock " + getLockable().getEntityName() );
}
return select.toStatementString();
protected HibernateException convertException(Object entity, JDBCException ex) {
return new OptimisticEntityLockException( entity, "could not obtain optimistic lock", ex );
}
}

View File

@ -51,8 +51,7 @@ public class LoaderHelper {
public static void upgradeLock(Object object, EntityEntry entry, LockOptions lockOptions, EventSource session) {
final LockMode requestedLockMode = lockOptions.getLockMode();
if ( requestedLockMode.greaterThan( entry.getLockMode() ) ) {
// The user requested a "greater" (i.e. more restrictive) form of
// pessimistic lock
// Request is for a more restrictive lock than the lock already held
if ( entry.getStatus().isDeletedOrGone()) {
throw new ObjectDeletedException(