Skip to content

Commit 6ac27cb

Browse files
committed
HHH-19336 - Proper implementation for JPA extended locking scope
HHH-19459 - LockScope, FollowOnLocking HHH-19501 - Session#lock w/ pessimistic locks for scopes HHH-19502 - Disallow SKIP_LOCKED with Session#lock HHH-19503 - Track a Dialect's level of support for locking joined tables
1 parent d6ae560 commit 6ac27cb

File tree

5 files changed

+142
-31
lines changed

5 files changed

+142
-31
lines changed

hibernate-core/src/main/java/org/hibernate/Locking.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import jakarta.persistence.PessimisticLockScope;
1010
import jakarta.persistence.RefreshOption;
1111

12+
import java.util.Locale;
13+
1214
/**
1315
* Support for various aspects of pessimistic locking.
1416
*
@@ -94,6 +96,13 @@ public static Scope fromJpaScope(PessimisticLockScope scope) {
9496
// null, NORMAL
9597
return ROOT_ONLY;
9698
}
99+
100+
public static Scope interpret(String name) {
101+
if ( name == null ) {
102+
return null;
103+
}
104+
return valueOf( name.toUpperCase( Locale.ROOT ) );
105+
}
97106
}
98107

99108
/**
@@ -134,6 +143,14 @@ enum FollowOn implements FindOption, LockOption, RefreshOption {
134143
*/
135144
FORCE;
136145

146+
public static FollowOn interpret(String name) {
147+
if ( name == null ) {
148+
return null;
149+
}
150+
151+
return valueOf( name.toUpperCase( Locale.ROOT ) );
152+
}
153+
137154
/**
138155
* Interprets the follow-on strategy into the legacy boolean values.
139156
*

hibernate-core/src/main/java/org/hibernate/internal/LockOptionsHelper.java

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@
44
*/
55
package org.hibernate.internal;
66

7-
import java.util.Map;
8-
import java.util.function.Supplier;
97
import jakarta.persistence.PersistenceException;
108
import jakarta.persistence.PessimisticLockScope;
11-
9+
import jakarta.persistence.Timeout;
1210
import org.hibernate.LockOptions;
11+
import org.hibernate.Locking;
12+
import org.hibernate.internal.log.DeprecationLogger;
13+
14+
import java.util.Map;
15+
import java.util.function.Supplier;
1316

14-
import static jakarta.persistence.PessimisticLockScope.EXTENDED;
15-
import static jakarta.persistence.PessimisticLockScope.NORMAL;
1617
import static org.hibernate.cfg.AvailableSettings.JAKARTA_LOCK_SCOPE;
1718
import static org.hibernate.cfg.AvailableSettings.JAKARTA_LOCK_TIMEOUT;
1819
import static org.hibernate.cfg.AvailableSettings.JPA_LOCK_SCOPE;
1920
import static org.hibernate.cfg.AvailableSettings.JPA_LOCK_TIMEOUT;
21+
import static org.hibernate.internal.util.config.ConfigurationHelper.getBoolean;
22+
import static org.hibernate.jpa.HibernateHints.HINT_FOLLOW_ON_LOCKING;
23+
import static org.hibernate.jpa.HibernateHints.HINT_FOLLOW_ON_STRATEGY;
2024

2125
public final class LockOptionsHelper {
2226

@@ -35,44 +39,80 @@ private LockOptionsHelper() {
3539
public static void applyPropertiesToLockOptions(Map<String, Object> props, Supplier<LockOptions> lockOptions) {
3640
applyScope( props, lockOptions );
3741
applyTimeout( props, lockOptions );
42+
applyFollowOn( props, lockOptions );
3843
}
3944

4045
private static void applyScope(Map<String, Object> props, Supplier<LockOptions> lockOptions) {
41-
String lockScopeHint = JPA_LOCK_SCOPE;
42-
Object lockScope = props.get( lockScopeHint );
43-
if ( lockScope == null ) {
44-
lockScopeHint = JAKARTA_LOCK_SCOPE;
45-
lockScope = props.get( lockScopeHint );
46+
String lockScopeHint = JAKARTA_LOCK_SCOPE;
47+
Object value = props.get( lockScopeHint );
48+
if ( value == null ) {
49+
lockScopeHint = JPA_LOCK_SCOPE;
50+
value = props.get( lockScopeHint );
4651
}
4752

48-
if ( lockScope instanceof String string ) {
49-
lockOptions.get().setLockScope( EXTENDED.name().equalsIgnoreCase( string ) ? EXTENDED : NORMAL );
53+
if ( value instanceof Locking.Scope scope ) {
54+
lockOptions.get().setScope( scope );
5055
}
51-
else if ( lockScope instanceof PessimisticLockScope pessimisticLockScope ) {
56+
else if ( value instanceof PessimisticLockScope pessimisticLockScope ) {
5257
lockOptions.get().setLockScope( pessimisticLockScope );
5358
}
54-
else if ( lockScope != null ) {
55-
throw new PersistenceException( "Unable to parse " + lockScopeHint + ": " + lockScope );
59+
else if ( value instanceof String string ) {
60+
final Locking.Scope scope = Locking.Scope.interpret( string );
61+
if ( scope != null ) {
62+
lockOptions.get().setScope( scope );
63+
}
64+
}
65+
else if ( value != null ) {
66+
throw new PersistenceException( "Unable to interpret " + lockScopeHint + ": " + value );
5667
}
5768
}
5869

5970
private static void applyTimeout(Map<String, Object> props, Supplier<LockOptions> lockOptions) {
60-
String timeoutHint = JPA_LOCK_TIMEOUT;
71+
String timeoutHint = JAKARTA_LOCK_TIMEOUT;
6172
Object lockTimeout = props.get( timeoutHint );
6273
if (lockTimeout == null) {
63-
timeoutHint = JAKARTA_LOCK_TIMEOUT;
74+
timeoutHint = JPA_LOCK_TIMEOUT;
6475
lockTimeout = props.get( timeoutHint );
76+
if ( lockTimeout != null ) {
77+
DeprecationLogger.DEPRECATION_LOGGER.deprecatedHint( JPA_LOCK_TIMEOUT, JAKARTA_LOCK_TIMEOUT );
78+
}
6579
}
6680

67-
if ( lockTimeout instanceof String string ) {
81+
if ( lockTimeout instanceof Timeout timeout ) {
82+
lockOptions.get().setTimeout( timeout );
83+
}
84+
else if ( lockTimeout instanceof String string ) {
6885
lockOptions.get().setTimeOut( Integer.parseInt( string ) );
6986
}
7087
else if ( lockTimeout instanceof Number number ) {
7188
int timeout = number.intValue();
7289
lockOptions.get().setTimeOut( timeout );
7390
}
7491
else if ( lockTimeout != null ) {
75-
throw new PersistenceException( "Unable to parse " + timeoutHint + ": " + lockTimeout );
92+
throw new PersistenceException( "Unable to interpret " + timeoutHint + ": " + lockTimeout );
93+
}
94+
}
95+
96+
private static void applyFollowOn(Map<String, Object> props, Supplier<LockOptions> lockOptions) {
97+
final Object strategyValue = props.get( HINT_FOLLOW_ON_STRATEGY );
98+
if ( strategyValue != null ) {
99+
if ( strategyValue instanceof Locking.FollowOn strategy ) {
100+
lockOptions.get().setFollowOnStrategy( strategy );
101+
}
102+
else if ( strategyValue instanceof String name ) {
103+
lockOptions.get().setFollowOnStrategy( Locking.FollowOn.interpret( name ) );
104+
}
105+
else {
106+
throw new PersistenceException( "Unable to interpret " + HINT_FOLLOW_ON_STRATEGY + ": " + strategyValue );
107+
}
108+
}
109+
else {
110+
// accounts for manually specifying null...
111+
if ( props.containsKey( HINT_FOLLOW_ON_LOCKING ) ) {
112+
final Locking.FollowOn strategyFromLegacy = Locking.FollowOn.fromLegacyValue( getBoolean( HINT_FOLLOW_ON_LOCKING, props ) );
113+
DeprecationLogger.DEPRECATION_LOGGER.deprecatedHint( HINT_FOLLOW_ON_LOCKING, HINT_FOLLOW_ON_STRATEGY );
114+
lockOptions.get().setFollowOnStrategy( strategyFromLegacy );
115+
}
76116
}
77117
}
78118

hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -805,11 +805,18 @@ protected SingleIdEntityLoader<?> buildSingleIdEntityLoader() {
805805
return new SingleIdEntityLoaderProvidedQueryImpl<>( this, memento );
806806
}
807807
else {
808-
return buildSingleIdEntityLoader( new LoadQueryInfluencers( factory ) );
808+
return buildSingleIdEntityLoader( new LoadQueryInfluencers( factory ), null );
809809
}
810810
}
811811

812-
private SingleIdEntityLoader<?> buildSingleIdEntityLoader(LoadQueryInfluencers loadQueryInfluencers) {
812+
private SingleIdEntityLoader<?> buildSingleIdEntityLoader(
813+
LoadQueryInfluencers loadQueryInfluencers,
814+
LockOptions lockOptions) {
815+
if ( lockOptions != null ) {
816+
if ( lockOptions.getLockMode().isPessimistic() && lockOptions.getScope() != Locking.Scope.ROOT_ONLY ) {
817+
return new SingleIdEntityLoaderStandardImpl<>( this, loadQueryInfluencers );
818+
}
819+
}
813820
if ( loadQueryInfluencers.effectivelyBatchLoadable( this ) ) {
814821
final int batchSize = loadQueryInfluencers.effectiveBatchSize( this );
815822
return factory.getServiceRegistry().requireService( BatchLoaderFactory.class )
@@ -1614,7 +1621,7 @@ private Object initLazyProperties(
16141621
ex.getSQLException(),
16151622
"could not initialize lazy properties: "
16161623
+ infoString( this, id, getFactory() ),
1617-
lazySelect.getJdbcSelect().getSqlString()
1624+
ex.getSQL()
16181625
);
16191626
}
16201627
}
@@ -1641,7 +1648,7 @@ private Object initLazyProperty(
16411648
ex.getSQLException(),
16421649
"could not initialize lazy properties: "
16431650
+ infoString( this, id, getFactory() ),
1644-
lazyLoanPlan.getJdbcSelect().getSqlString()
1651+
ex.getSQL()
16451652
);
16461653
}
16471654
}
@@ -3485,9 +3492,10 @@ protected SingleIdEntityLoader<?> determineLoaderToUse(SharedSessionContractImpl
34853492
else {
34863493
final LoadQueryInfluencers influencers = session.getLoadQueryInfluencers();
34873494
final boolean needsUniqueLoader = isAffectedByInfluencers( influencers, true )
3488-
|| lockOptions.getScope() != Locking.Scope.ROOT_ONLY;
3495+
|| lockOptions.getScope() != Locking.Scope.ROOT_ONLY
3496+
|| lockOptions.getFollowOnStrategy() != Locking.FollowOn.ALLOW;
34893497
return needsUniqueLoader
3490-
? buildSingleIdEntityLoader( influencers )
3498+
? buildSingleIdEntityLoader( influencers, lockOptions )
34913499
: getSingleIdLoader();
34923500
}
34933501
}

hibernate-core/src/test/java/org/hibernate/orm/test/locking/options/FollowOnLockingTests.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import jakarta.persistence.LockModeType;
88
import org.hibernate.Locking;
9+
import org.hibernate.jpa.HibernateHints;
910
import org.hibernate.testing.jdbc.SQLStatementInspector;
1011
import org.hibernate.testing.orm.junit.DomainModel;
1112
import org.hibernate.testing.orm.junit.SessionFactory;
@@ -14,6 +15,8 @@
1415
import org.junit.jupiter.api.BeforeEach;
1516
import org.junit.jupiter.api.Test;
1617

18+
import java.util.Map;
19+
1720
import static org.assertj.core.api.Assertions.assertThat;
1821

1922
/**
@@ -58,4 +61,46 @@ void testFindWithForced(SessionFactoryScope factoryScope) {
5861
Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), Helper.Table.BOOKS );
5962
} );
6063
}
64+
65+
@Test
66+
void testFindWithForcedAsHint(SessionFactoryScope factoryScope) {
67+
final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector();
68+
69+
factoryScope.inTransaction( (session) -> {
70+
sqlCollector.clear();
71+
final Map<String, Object> hints = Map.of( HibernateHints.HINT_FOLLOW_ON_STRATEGY, Locking.FollowOn.FORCE );
72+
session.find( Book.class, 1, LockModeType.PESSIMISTIC_WRITE, hints );
73+
74+
assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 );
75+
Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), Helper.Table.BOOKS );
76+
} );
77+
}
78+
79+
@Test
80+
void testFindWithForcedAsHintName(SessionFactoryScope factoryScope) {
81+
final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector();
82+
83+
factoryScope.inTransaction( (session) -> {
84+
sqlCollector.clear();
85+
final Map<String, Object> hints = Map.of( HibernateHints.HINT_FOLLOW_ON_STRATEGY, Locking.FollowOn.FORCE.name() );
86+
session.find( Book.class, 1, LockModeType.PESSIMISTIC_WRITE, hints );
87+
88+
assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 );
89+
Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), Helper.Table.BOOKS );
90+
} );
91+
}
92+
93+
@Test
94+
void testFindWithForcedAsLegacyHint(SessionFactoryScope factoryScope) {
95+
final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector();
96+
97+
factoryScope.inTransaction( (session) -> {
98+
sqlCollector.clear();
99+
final Map<String, Object> hints = Map.of( HibernateHints.HINT_FOLLOW_ON_LOCKING, true );
100+
session.find( Book.class, 1, LockModeType.PESSIMISTIC_WRITE, hints );
101+
102+
assertThat( sqlCollector.getSqlQueries() ).hasSize( 2 );
103+
Helper.checkSql( sqlCollector.getSqlQueries().get( 1 ), session.getDialect(), Helper.Table.BOOKS );
104+
} );
105+
}
61106
}

hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,13 @@ public boolean apply(Dialect dialect) {
273273
}
274274
}
275275

276+
public static class SupportsLockingJoins implements DialectFeatureCheck {
277+
public boolean apply(Dialect dialect) {
278+
return dialect.getOuterJoinLockingLevel() == OuterJoinLockingLevel.FULL
279+
|| dialect.getOuterJoinLockingLevel() == OuterJoinLockingLevel.IDENTIFIED;
280+
}
281+
}
282+
276283
public static class DoubleQuoteQuoting implements DialectFeatureCheck {
277284
@Override
278285
public boolean apply(Dialect dialect) {
@@ -452,12 +459,6 @@ public boolean apply(Dialect dialect) {
452459
}
453460
}
454461

455-
public static class SupportsLockingJoins implements DialectFeatureCheck {
456-
public boolean apply(Dialect dialect) {
457-
return dialect.getOuterJoinLockingLevel() == OuterJoinLockingLevel.FULL;
458-
}
459-
}
460-
461462
public static class CurrentTimestampHasMicrosecondPrecision implements DialectFeatureCheck {
462463
public boolean apply(Dialect dialect) {
463464
return !dialect.currentTimestamp().contains( "6" );

0 commit comments

Comments
 (0)