Skip to content

Commit e8af3e0

Browse files
committed
HHH-19336 - Proper implementation for JPA extended locking scope
HHH-19459 - LockScope, FollowOnLocking
1 parent 3e0e1e2 commit e8af3e0

File tree

6 files changed

+126
-75
lines changed

6 files changed

+126
-75
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,12 @@ public LockOptions setLockMode(LockMode lockMode) {
195195
if ( immutable ) {
196196
throw new UnsupportedOperationException("immutable global instance of LockOptions");
197197
}
198+
if ( lockMode == LockMode.UPGRADE_NOWAIT ) {
199+
timeout = Timeouts.NO_WAIT_MILLI;
200+
}
201+
else if ( lockMode == LockMode.UPGRADE_SKIPLOCKED ) {
202+
timeout = Timeouts.SKIP_LOCKED_MILLI;
203+
}
198204
this.lockMode = lockMode;
199205
return this;
200206
}

hibernate-core/src/main/java/org/hibernate/sql/ast/internal/NoOpForUpdateClauseStrategy.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.hibernate.sql.ast.spi.ForUpdateClauseStrategy;
88
import org.hibernate.sql.ast.spi.SqlAppender;
99
import org.hibernate.sql.ast.tree.from.TableGroup;
10+
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
1011

1112
/**
1213
* ForUpdateClauseStrategy implementation for cases when a dialect
@@ -19,10 +20,20 @@ public class NoOpForUpdateClauseStrategy implements ForUpdateClauseStrategy {
1920
public static final NoOpForUpdateClauseStrategy NO_OP_STRATEGY = new NoOpForUpdateClauseStrategy();
2021

2122
@Override
22-
public void register(TableGroup tableGroup, boolean isRoot) {
23+
public void registerRoot(TableGroup root) {
2324
// nothing to do
2425
}
2526

27+
@Override
28+
public void registerJoin(TableGroupJoin join) {
29+
// nothing to do
30+
}
31+
32+
@Override
33+
public boolean containsOuterJoins() {
34+
return false;
35+
}
36+
2637
@Override
2738
public void render(SqlAppender sqlAppender) {
2839
// nothing to do

hibernate-core/src/main/java/org/hibernate/sql/ast/internal/PessimisticLockKind.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public enum PessimisticLockKind {
2121
public static PessimisticLockKind interpret(LockMode lockMode) {
2222
return switch ( lockMode ) {
2323
case PESSIMISTIC_READ -> SHARE;
24-
case PESSIMISTIC_WRITE, PESSIMISTIC_FORCE_INCREMENT -> UPDATE;
24+
case PESSIMISTIC_WRITE, PESSIMISTIC_FORCE_INCREMENT, UPGRADE_NOWAIT, UPGRADE_SKIPLOCKED -> UPDATE;
2525
default -> NONE;
2626
};
2727
}

hibernate-core/src/main/java/org/hibernate/sql/ast/internal/StandardForUpdateClauseStrategy.java

Lines changed: 80 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import org.hibernate.LockMode;
99
import org.hibernate.LockOptions;
1010
import org.hibernate.Locking;
11-
import org.hibernate.Timeouts;
1211
import org.hibernate.dialect.Dialect;
1312
import org.hibernate.dialect.RowLockStrategy;
1413
import org.hibernate.internal.util.collections.CollectionHelper;
@@ -17,14 +16,18 @@
1716
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
1817
import org.hibernate.metamodel.mapping.internal.BasicValuedCollectionPart;
1918
import org.hibernate.persister.entity.EntityPersister;
19+
import org.hibernate.persister.entity.JoinedSubclassEntityPersister;
20+
import org.hibernate.sql.ast.SqlAstJoinType;
2021
import org.hibernate.sql.ast.spi.ForUpdateClauseStrategy;
2122
import org.hibernate.sql.ast.spi.SqlAppender;
2223
import org.hibernate.sql.ast.tree.from.TableGroup;
24+
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
2325
import org.hibernate.sql.ast.tree.from.TableReferenceJoin;
2426
import org.hibernate.sql.ast.tree.select.QuerySpec;
2527

2628
import java.util.ArrayList;
2729
import java.util.Collections;
30+
import java.util.HashSet;
2831
import java.util.LinkedHashSet;
2932
import java.util.List;
3033
import java.util.Set;
@@ -41,7 +44,8 @@ public class StandardForUpdateClauseStrategy implements ForUpdateClauseStrategy
4144
private final Locking.Scope lockingScope;
4245
private final int timeout;
4346

44-
private Set<TableGroup> tableGroupsToLock;
47+
private Set<TableGroup> rootsToLock;
48+
private Set<TableGroupJoin> joinsToLock;
4549

4650
public StandardForUpdateClauseStrategy(
4751
Dialect dialect,
@@ -61,114 +65,136 @@ public StandardForUpdateClauseStrategy(
6165
}
6266

6367
@Override
64-
public void register(TableGroup tableGroup, boolean isRoot) {
68+
public void registerRoot(TableGroup root) {
6569
if ( rowLockStrategy == RowLockStrategy.NONE ) {
6670
// no need to collect these
6771
return;
6872
}
6973

70-
if ( isRoot ) {
71-
assert tableGroup.isRealTableGroup();
74+
if ( rootsToLock == null ) {
75+
rootsToLock = new HashSet<>();
76+
}
77+
rootsToLock.add( root );
78+
}
7279

73-
// we always want to lock tables which are part of the roots
74-
trackTableGroup( tableGroup );
80+
@Override
81+
public void registerJoin(TableGroupJoin join) {
82+
if ( rowLockStrategy == RowLockStrategy.NONE ) {
83+
// no need to collect these
84+
return;
7585
}
7686
else if ( lockingScope == Locking.Scope.INCLUDE_COLLECTIONS ) {
7787
// if the TableGroup is an owned (aka, non-inverse) collection,
7888
// and we are to lock collections, track it
79-
if ( tableGroup.getModelPart() instanceof PluralAttributeMapping attrMapping ) {
89+
if ( join.getJoinedGroup().getModelPart() instanceof PluralAttributeMapping attrMapping ) {
8090
if ( !attrMapping.getCollectionDescriptor().isInverse() ) {
8191
// owned collection
8292
if ( attrMapping.getElementDescriptor() instanceof BasicValuedCollectionPart ) {
8393
// an element-collection
84-
trackTableGroup( tableGroup );
94+
trackJoin( join );
8595
}
8696
}
8797
}
8898
}
8999
else if ( lockingScope == Locking.Scope.INCLUDE_FETCHES ) {
90-
if ( tableGroup.isFetched() ) {
91-
trackTableGroup( tableGroup );
100+
if ( join.getJoinedGroup().isFetched() ) {
101+
trackJoin( join );
92102
}
93103
}
94104
}
95105

96-
private void trackTableGroup(TableGroup tableGroup) {
97-
if ( tableGroupsToLock == null ) {
98-
tableGroupsToLock = new LinkedHashSet<>();
106+
private void trackJoin(TableGroupJoin join) {
107+
if ( joinsToLock == null ) {
108+
joinsToLock = new LinkedHashSet<>();
99109
}
100-
tableGroupsToLock.add( tableGroup );
110+
joinsToLock.add( join );
101111
}
102112

113+
@Override
114+
public boolean containsOuterJoins() {
115+
for ( TableGroup tableGroup : rootsToLock ) {
116+
if ( tableGroup.getModelPart() instanceof JoinedSubclassEntityPersister ) {
117+
// inherently has outer joins
118+
return true;
119+
}
120+
}
121+
122+
if ( joinsToLock == null ) {
123+
return false;
124+
}
125+
for ( TableGroupJoin tableGroupJoin : joinsToLock ) {
126+
final TableGroup joinedGroup = tableGroupJoin.getJoinedGroup();
127+
if ( tableGroupJoin.isInitialized()
128+
&& tableGroupJoin.getJoinType() != SqlAstJoinType.INNER
129+
&& !joinedGroup.isVirtual() ) {
130+
return true;
131+
}
132+
if ( joinedGroup.getModelPart() instanceof JoinedSubclassEntityPersister ) {
133+
// inherently has outer joins
134+
return true;
135+
}
136+
}
137+
138+
return false;
139+
}
103140

104141
@Override
105142
public void render(SqlAppender sqlAppender) {
106-
renderLockTerm( dialect, lockKind, timeout, sqlAppender );
107-
renderRowLocking( rowLockStrategy, tableGroupsToLock, sqlAppender );
143+
renderLockFragment( dialect, lockKind, timeout, rowLockStrategy, sqlAppender );
108144
renderResultSetOptions( dialect, sqlAppender );
109-
renderLockedRowHandling( timeout, sqlAppender );
110145
}
111146

112-
protected void renderLockTerm(
147+
protected void renderLockFragment(
113148
Dialect dialect,
114149
PessimisticLockKind lockKind,
115150
int timeout,
151+
RowLockStrategy rowLockStrategy,
116152
SqlAppender sqlAppender) {
117-
final String term = lockKind == PessimisticLockKind.SHARE
118-
? dialect.getReadLockString( timeout )
119-
: dialect.getWriteLockString( timeout );
120-
sqlAppender.append( term );
121-
}
122-
123-
protected void renderRowLocking(RowLockStrategy rowLockStrategy, Set<TableGroup> tableGroupsToLock, SqlAppender sqlAppender) {
153+
final String fragment;
124154
if ( rowLockStrategy == RowLockStrategy.NONE ) {
125-
return;
155+
fragment = lockKind == PessimisticLockKind.SHARE
156+
? dialect.getReadLockString( timeout )
157+
: dialect.getWriteLockString( timeout );
126158
}
159+
else {
160+
final String lockItemsFragment = collectLockItems();
161+
fragment = lockKind == PessimisticLockKind.SHARE
162+
? dialect.getReadLockString( lockItemsFragment, timeout )
163+
: dialect.getWriteLockString( lockItemsFragment, timeout );
164+
}
165+
sqlAppender.append( fragment );
166+
}
127167

128-
assert tableGroupsToLock != null && !tableGroupsToLock.isEmpty();
129-
130-
sqlAppender.append( " of " );
131-
168+
private String collectLockItems() {
132169
final List<String> lockItems = new ArrayList<>();
133-
for ( TableGroup tableGroup : tableGroupsToLock ) {
134-
collectLockItems( tableGroup, lockItems );
170+
for ( TableGroup root : rootsToLock ) {
171+
collectLockItems( root, lockItems );
172+
}
173+
if ( joinsToLock != null ) {
174+
for ( TableGroupJoin join : joinsToLock ) {
175+
collectLockItems( join.getJoinedGroup(), lockItems );
176+
}
135177
}
136178

179+
final StringBuilder buffer = new StringBuilder();
137180
boolean first = true;
138181
for ( String lockItem : lockItems ) {
139182
if ( first ) {
140183
first = false;
141184
}
142185
else {
143-
sqlAppender.appendSql( ',' );
186+
buffer.append( ',' );
144187
}
145-
sqlAppender.appendSql( lockItem );
188+
buffer.append( lockItem );
146189
}
190+
191+
return buffer.toString();
147192
}
148193

149194
protected void renderResultSetOptions(Dialect dialect, SqlAppender sqlAppender) {
150195
// hook for Derby
151196
}
152197

153-
protected void renderLockedRowHandling(int timeout, SqlAppender sqlAppender) {
154-
if ( timeout == Timeouts.NO_WAIT_MILLI ) {
155-
if ( dialect.supportsNoWait() ) {
156-
sqlAppender.append( dialect.getForUpdateNowaitString() );
157-
}
158-
}
159-
else if ( timeout == Timeouts.SKIP_LOCKED_MILLI ) {
160-
if ( dialect.supportsSkipLocked() ) {
161-
sqlAppender.append( dialect.getForUpdateSkipLockedString() );
162-
}
163-
}
164-
else if ( timeout > 0 ) {
165-
if ( dialect.supportsLockTimeouts() ) {
166-
sqlAppender.append( " wait " );
167-
sqlAppender.append( Integer.toString( Timeouts.getTimeoutInSeconds( timeout ) ) );
168-
}
169-
}
170-
}
171-
172198
private void collectLockItems(TableGroup tableGroup, List<String> lockItems) {
173199
if ( rowLockStrategy == RowLockStrategy.TABLE ) {
174200
addTableAliases( tableGroup, lockItems );

hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,17 +1735,9 @@ else if ( followOnStrategy == Locking.FollowOn.IGNORE ) {
17351735
}
17361736

17371737
if ( !dialect.supportsOuterJoinForUpdate() ) {
1738-
// if we have any outer joins, we need to use follow-on locking if allowed
1739-
final Boolean hasOuterJoins = querySpec.getFromClause().queryTableGroupJoins( tableGroupJoin -> {
1740-
final TableGroup group = tableGroupJoin.getJoinedGroup();
1741-
if ( tableGroupJoin.isInitialized()
1742-
&& tableGroupJoin.getJoinType() != SqlAstJoinType.INNER
1743-
&& !group.isVirtual() ) {
1744-
return true;
1745-
}
1746-
return null;
1747-
} );
1748-
if ( hasOuterJoins == Boolean.TRUE ) {
1738+
if ( forUpdateClauseStrategy != null && forUpdateClauseStrategy.containsOuterJoins() ) {
1739+
// we have any outer joins to lock, but the dialect does not support locking outer joins
1740+
// -we need to use follow-on locking if allowed
17491741
if ( followOnStrategy == Locking.FollowOn.DISALLOW ) {
17501742
throw new IllegalQueryOperationException( "Locking with OUTER joins is not supported" );
17511743
}
@@ -5684,10 +5676,12 @@ else if ( root.isInitialized() ) {
56845676

56855677
protected void renderRootTableGroup(TableGroup tableGroup, List<TableGroupJoin> tableGroupJoinCollector) {
56865678
final LockMode effectiveLockMode = getEffectiveLockMode( tableGroup.getSourceAlias() );
5687-
final boolean usesLockHint = renderPrimaryTableReference( tableGroup, effectiveLockMode );
5688-
if ( !usesLockHint ) {
5689-
forUpdateClauseStrategy.register( tableGroup, true );
5679+
renderPrimaryTableReference( tableGroup, effectiveLockMode );
5680+
5681+
if ( forUpdateClauseStrategy != null ) {
5682+
forUpdateClauseStrategy.registerRoot( tableGroup );
56905683
}
5684+
56915685
if ( tableGroup.isLateral() && !dialect.supportsLateral() ) {
56925686
addAdditionalWherePredicate( determineLateralEmulationPredicate( tableGroup ) );
56935687
}
@@ -5831,10 +5825,6 @@ else if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.NO_TABLE_GROUP_
58315825
registerAffectedTable( querySpaces[i] );
58325826
}
58335827
}
5834-
5835-
if ( !usesLockHint ) {
5836-
forUpdateClauseStrategy.register( tableGroup, false );
5837-
}
58385828
}
58395829

58405830
protected boolean needsLocking(QuerySpec querySpec) {
@@ -6224,6 +6214,9 @@ protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List<TableGro
62246214
else {
62256215
renderTableGroup( tableGroupJoin.getJoinedGroup(), null, tableGroupJoinCollector );
62266216
}
6217+
if ( forUpdateClauseStrategy != null ) {
6218+
forUpdateClauseStrategy.registerJoin( tableGroupJoin );
6219+
}
62276220
}
62286221

62296222
protected Predicate determineLateralEmulationPredicate(TableGroup tableGroup) {

hibernate-core/src/main/java/org/hibernate/sql/ast/spi/ForUpdateClauseStrategy.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package org.hibernate.sql.ast.spi;
66

77
import org.hibernate.sql.ast.tree.from.TableGroup;
8+
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
89

910
/**
1011
* Strategy for dealing with locking via a SQL {@code FOR UPDATE (OF)}
@@ -17,12 +18,26 @@
1718
* Some dialects support an additional {@code FOR SHARE (OF)} clause
1819
* as well to acquire non-exclusive locks. That is also handled here,
1920
* varied by the requested {@linkplain org.hibernate.LockMode LockMode}.
21+
* <p/>
22+
* Operates in 2 "phases"-<ol>
23+
* <li>
24+
* collect tables which are to be locked (based on {@linkplain org.hibernate.Locking.Scope},
25+
* and other things)
26+
* </li>
27+
* <li>
28+
* render the appropriate locking fragment
29+
* </li>
30+
* </ol>
2031
*
2132
* @see org.hibernate.dialect.Dialect#getForUpdateClauseStrategy
2233
*
2334
* @author Steve Ebersole
2435
*/
2536
public interface ForUpdateClauseStrategy {
26-
void register(TableGroup tableGroup, boolean isRoot);
37+
void registerRoot(TableGroup root);
38+
void registerJoin(TableGroupJoin join);
39+
40+
boolean containsOuterJoins();
41+
2742
void render(SqlAppender sqlAppender);
2843
}

0 commit comments

Comments
 (0)