Skip to content

Commit cfbb96f

Browse files
committed
HHH-19602 - Adjust JdbcOperation to allow more-than-one statement
HHH-19513 - Follow-on locking does not lock element-collection tables
1 parent 7ce134f commit cfbb96f

File tree

9 files changed

+291
-207
lines changed

9 files changed

+291
-207
lines changed

hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/SqlAstBasedLockingStrategy.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.hibernate.LockOptions;
99
import org.hibernate.Locking;
1010
import org.hibernate.StaleObjectStateException;
11+
import org.hibernate.collection.spi.PersistentCollection;
1112
import org.hibernate.dialect.lock.LockingStrategy;
1213
import org.hibernate.dialect.lock.LockingStrategyException;
1314
import org.hibernate.dialect.lock.PessimisticEntityLockException;
@@ -23,6 +24,7 @@
2324
import org.hibernate.metamodel.mapping.SelectableMapping;
2425
import org.hibernate.persister.entity.EntityPersister;
2526
import org.hibernate.query.sqm.ComparisonOperator;
27+
import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper;
2628
import org.hibernate.spi.NavigablePath;
2729
import org.hibernate.sql.ast.spi.SimpleFromClauseAccessImpl;
2830
import org.hibernate.sql.ast.spi.SqlAliasBaseManager;
@@ -34,6 +36,7 @@
3436
import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl;
3537
import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl;
3638
import org.hibernate.sql.exec.internal.JdbcParameterImpl;
39+
import org.hibernate.sql.exec.internal.lock.LockingHelper;
3740
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
3841
import org.hibernate.sql.exec.spi.JdbcSelect;
3942
import org.hibernate.sql.exec.spi.JdbcSelectExecutor;
@@ -176,6 +179,19 @@ public void lock(
176179
1,
177180
SingleResultConsumer.instance()
178181
);
182+
183+
if ( lockOptions.getScope() == Locking.Scope.INCLUDE_COLLECTIONS ) {
184+
SqmMutationStrategyHelper.visitCollectionTables( entityToLock, (attribute) -> {
185+
final PersistentCollection<?> collectionToLock = (PersistentCollection<?>) attribute.getValue( object );
186+
LockingHelper.lockCollectionTable(
187+
attribute,
188+
lockMode,
189+
lockOptions.getTimeout(),
190+
collectionToLock,
191+
lockingExecutionContext
192+
);
193+
} );
194+
}
179195
}
180196
catch (LockTimeoutException e) {
181197
throw new PessimisticEntityLockException(

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.hibernate.dialect.Dialect;
1515
import org.hibernate.dialect.DmlTargetColumnQualifierSupport;
1616
import org.hibernate.dialect.SelectItemReferenceStrategy;
17-
import org.hibernate.dialect.lock.PessimisticLockStyle;
1817
import org.hibernate.dialect.lock.spi.LockTimeoutType;
1918
import org.hibernate.dialect.lock.spi.LockingSupport;
2019
import org.hibernate.engine.jdbc.Size;
@@ -909,16 +908,8 @@ protected JdbcSelect translateSelect(SelectStatement selectStatement) {
909908
if ( lockStrategy == LockStrategy.FOLLOW_ON ) {
910909
FollowOnLockingAction.apply( lockOptions, lockingTarget, lockingClauseStrategy, builder );
911910
}
912-
else {
913-
if ( lockOptions.getScope() == Locking.Scope.INCLUDE_COLLECTIONS ) {
914-
if ( dialect.getLockingSupport().getMetadata().getPessimisticLockStyle() == PessimisticLockStyle.TABLE_HINT ) {
915-
// we have a case where include-collection scope was requested.
916-
// generally this would be handled by follow-on locking process, but because
917-
// the Dialect supports "table hint locking" we will always skip follow-on
918-
// locking. therefore, we need to handle the collection-table locking explicitly.
919-
CollectionLockingAction.apply( lockOptions, lockingTarget, builder );
920-
}
921-
}
911+
else if ( lockOptions.getScope() == Locking.Scope.INCLUDE_COLLECTIONS ) {
912+
CollectionLockingAction.apply( lockOptions, lockingTarget, builder );
922913
}
923914

924915
return builder.build();

hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/CollectionLockingAction.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import java.sql.Connection;
3131
import java.util.ArrayList;
32+
import java.util.IdentityHashMap;
3233
import java.util.List;
3334
import java.util.Map;
3435

@@ -148,6 +149,17 @@ private static LoadedValuesCollectorImpl resolveLoadedValuesCollector(FromClause
148149
}
149150
}
150151

152+
private static Map<EntityMappingType, List<EntityKey>> segmentLoadedValues(LoadedValuesCollector loadedValuesCollector) {
153+
final Map<EntityMappingType, List<EntityKey>> map = new IdentityHashMap<>();
154+
LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedRootEntities(), map );
155+
LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedNonRootEntities(), map );
156+
if ( map.isEmpty() ) {
157+
// NOTE: this may happen with Session#lock routed through SqlAstBasedLockingStrategy.
158+
// however, we cannot tell that is the code path from here.
159+
}
160+
return map;
161+
}
162+
151163
private static class LoadedValuesCollectorImpl implements LoadedValuesCollector {
152164
private final List<NavigablePath> rootPaths;
153165

hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/FollowOnLockingAction.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.ArrayList;
4040
import java.util.Collection;
4141
import java.util.HashMap;
42+
import java.util.IdentityHashMap;
4243
import java.util.List;
4344
import java.util.Locale;
4445
import java.util.Map;
@@ -209,11 +210,14 @@ private QueryOptions buildLockingOptions(ExecutionContext executionContext) {
209210
return lockingQueryOptions;
210211
}
211212

212-
/**
213-
* Collect loaded entities by type
214-
*/
215213
private Map<EntityMappingType, List<EntityKey>> segmentLoadedValues() {
216-
return LockingHelper.segmentLoadedValues( loadedValuesCollector );
214+
final Map<EntityMappingType, List<EntityKey>> map = new IdentityHashMap<>();
215+
LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedRootEntities(), map );
216+
LockingHelper.segmentLoadedValues( loadedValuesCollector.getCollectedNonRootEntities(), map );
217+
if ( map.isEmpty() ) {
218+
throw new AssertionFailure( "Expecting some values" );
219+
}
220+
return map;
217221
}
218222

219223
private Map<String, TableLock> prepareTableLocks(

hibernate-core/src/main/java/org/hibernate/sql/exec/internal/lock/LockingHelper.java

Lines changed: 149 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
package org.hibernate.sql.exec.internal.lock;
66

77
import jakarta.persistence.Timeout;
8-
import org.hibernate.AssertionFailure;
98
import org.hibernate.LockMode;
109
import org.hibernate.ScrollMode;
10+
import org.hibernate.Session;
1111
import org.hibernate.collection.spi.PersistentCollection;
1212
import org.hibernate.engine.jdbc.spi.JdbcServices;
1313
import org.hibernate.engine.spi.EntityEntry;
@@ -22,14 +22,19 @@
2222
import org.hibernate.metamodel.mapping.ValuedModelPart;
2323
import org.hibernate.query.internal.QueryOptionsImpl;
2424
import org.hibernate.query.spi.QueryOptions;
25+
import org.hibernate.query.sqm.ComparisonOperator;
2526
import org.hibernate.spi.NavigablePath;
2627
import org.hibernate.sql.ast.SqlAstTranslator;
2728
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
2829
import org.hibernate.sql.ast.spi.LockingClauseStrategy;
2930
import org.hibernate.sql.ast.tree.expression.ColumnReference;
31+
import org.hibernate.sql.ast.tree.expression.JdbcParameter;
32+
import org.hibernate.sql.ast.tree.expression.SqlTuple;
3033
import org.hibernate.sql.ast.tree.from.NamedTableReference;
3134
import org.hibernate.sql.ast.tree.from.TableGroup;
3235
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
36+
import org.hibernate.sql.ast.tree.from.TableReference;
37+
import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate;
3338
import org.hibernate.sql.ast.tree.predicate.InListPredicate;
3439
import org.hibernate.sql.ast.tree.select.QuerySpec;
3540
import org.hibernate.sql.ast.tree.select.SelectStatement;
@@ -48,7 +53,6 @@
4853
import java.util.ArrayList;
4954
import java.util.Collection;
5055
import java.util.HashMap;
51-
import java.util.IdentityHashMap;
5256
import java.util.LinkedHashSet;
5357
import java.util.List;
5458
import java.util.Map;
@@ -61,6 +65,106 @@
6165
* @author Steve Ebersole
6266
*/
6367
public class LockingHelper {
68+
/**
69+
* Lock a collection-table for a single entity.
70+
*
71+
* @param attributeMapping The plural attribute whose table needs locked.
72+
* @param lockMode The lock mode to apply
73+
* @param lockTimeout A lock timeout to apply, if one.
74+
* @param collectionToLock The collection to lock.
75+
*
76+
* @see Session#lock
77+
*/
78+
public static void lockCollectionTable(
79+
PluralAttributeMapping attributeMapping,
80+
LockMode lockMode,
81+
Timeout lockTimeout,
82+
PersistentCollection<?> collectionToLock,
83+
ExecutionContext executionContext) {
84+
final SharedSessionContractImplementor session = executionContext.getSession();
85+
86+
final ForeignKeyDescriptor keyDescriptor = attributeMapping.getKeyDescriptor();
87+
final String keyTableName = keyDescriptor.getKeyTable();
88+
89+
if ( SQL_EXEC_LOGGER.isDebugEnabled() ) {
90+
SQL_EXEC_LOGGER.debugf( "Collection locking for collection table `%s` - %s", keyTableName, attributeMapping.getRootPathName() );
91+
}
92+
93+
final QuerySpec querySpec = new QuerySpec( true );
94+
95+
final NamedTableReference tableReference = new NamedTableReference( keyTableName, "tbl" );
96+
final LockingTableGroup tableGroup = new LockingTableGroup(
97+
tableReference,
98+
keyTableName,
99+
attributeMapping,
100+
keyDescriptor.getKeySide().getModelPart()
101+
);
102+
103+
querySpec.getFromClause().addRoot( tableGroup );
104+
105+
final ValuedModelPart keyPart = keyDescriptor.getKeyPart();
106+
final ColumnReference columnReference = new ColumnReference( tableReference, keyPart.getSelectable( 0 ) );
107+
108+
// NOTE: We add the key column to the selection list, but never create a DomainResult
109+
// as we won't read the value back. Ideally, we would read the "value column(s)" and
110+
// update the collection state accordingly much like is done for entity state -
111+
// however, the concern is minor, so for simplicity we do not.
112+
final SqlSelectionImpl sqlSelection = new SqlSelectionImpl( columnReference, 0 );
113+
querySpec.getSelectClause().addSqlSelection( sqlSelection );
114+
115+
final JdbcParameterBindingsImpl parameterBindings = new JdbcParameterBindingsImpl( keyDescriptor.getJdbcTypeCount() );
116+
117+
final ComparisonPredicate restriction;
118+
if ( keyDescriptor.getJdbcTypeCount() == 1 ) {
119+
final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( keyPart.getSelectable( 0 ).getJdbcMapping() );
120+
keyDescriptor.breakDownJdbcValues(
121+
collectionToLock.getKey(),
122+
(valueIndex, value, jdbcValueMapping) -> {
123+
parameterBindings.addBinding(
124+
jdbcParameter,
125+
new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value )
126+
);
127+
},
128+
session
129+
);
130+
restriction = new ComparisonPredicate( columnReference, ComparisonOperator.EQUAL, jdbcParameter );
131+
}
132+
else {
133+
final List<ColumnReference> columnReferences = new ArrayList<>( keyDescriptor.getJdbcTypeCount() );
134+
final List<JdbcParameter> jdbcParameters = new ArrayList<>( keyDescriptor.getJdbcTypeCount() );
135+
keyDescriptor.breakDownJdbcValues(
136+
collectionToLock.getKey(),
137+
(valueIndex, value, jdbcValueMapping) -> {
138+
columnReferences.add( new ColumnReference( tableReference, jdbcValueMapping ) );
139+
140+
final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( jdbcValueMapping.getJdbcMapping() );
141+
jdbcParameters.add( jdbcParameter );
142+
parameterBindings.addBinding(
143+
jdbcParameter,
144+
new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value )
145+
);
146+
},
147+
session
148+
);
149+
final SqlTuple columns = new SqlTuple( columnReferences, keyDescriptor );
150+
final SqlTuple parameters = new SqlTuple( jdbcParameters, keyDescriptor );
151+
restriction = new ComparisonPredicate( columns, ComparisonOperator.EQUAL, parameters );
152+
}
153+
querySpec.applyPredicate( restriction );
154+
155+
final QueryOptionsImpl lockingQueryOptions = new QueryOptionsImpl();
156+
lockingQueryOptions.getLockOptions().setLockMode( lockMode );
157+
lockingQueryOptions.getLockOptions().setTimeout( lockTimeout );
158+
final ExecutionContext lockingExecutionContext = new BaseExecutionContext( executionContext.getSession() ) {
159+
@Override
160+
public QueryOptions getQueryOptions() {
161+
return lockingQueryOptions;
162+
}
163+
};
164+
165+
performLocking( querySpec, parameterBindings, lockingExecutionContext );
166+
}
167+
64168
/**
65169
* Lock a collection-table.
66170
*
@@ -104,13 +208,12 @@ public static void lockCollectionTable(
104208
final SqlSelectionImpl sqlSelection = new SqlSelectionImpl( columnReference, 0 );
105209
querySpec.getSelectClause().addSqlSelection( sqlSelection );
106210

107-
final InListPredicate restriction = new InListPredicate( columnReference );
108-
querySpec.applyPredicate( restriction );
109-
110211
final int expectedParamCount = ownerDetailsMap.size() * keyDescriptor.getJdbcTypeCount();
111212
final JdbcParameterBindingsImpl parameterBindings = new JdbcParameterBindingsImpl( expectedParamCount );
112213

214+
final InListPredicate restriction;
113215
if ( keyDescriptor.getJdbcTypeCount() == 1 ) {
216+
restriction = new InListPredicate( columnReference );
114217
applySimpleCollectionKeyTableLockRestrictions(
115218
attributeMapping,
116219
keyDescriptor,
@@ -121,15 +224,16 @@ public static void lockCollectionTable(
121224
);
122225
}
123226
else {
124-
applyCompositeCollectionKeyTableLockRestrictions(
227+
restriction = applyCompositeCollectionKeyTableLockRestrictions(
125228
attributeMapping,
126229
keyDescriptor,
127-
restriction,
230+
tableReference,
128231
parameterBindings,
129232
ownerDetailsMap,
130233
executionContext.getSession()
131234
);
132235
}
236+
querySpec.applyPredicate( restriction );
133237

134238
final QueryOptionsImpl lockingQueryOptions = new QueryOptionsImpl();
135239
lockingQueryOptions.getLockOptions().setLockMode( lockMode );
@@ -172,14 +276,48 @@ private static void applySimpleCollectionKeyTableLockRestrictions(
172276
} );
173277
}
174278

175-
private static void applyCompositeCollectionKeyTableLockRestrictions(
279+
private static InListPredicate applyCompositeCollectionKeyTableLockRestrictions(
176280
PluralAttributeMapping attributeMapping,
177281
ForeignKeyDescriptor keyDescriptor,
178-
InListPredicate restriction,
282+
TableReference tableReference,
179283
JdbcParameterBindingsImpl parameterBindings,
180284
Map<Object, EntityDetails> ownerDetailsMap,
181285
SharedSessionContractImplementor session) {
182-
throw new UnsupportedOperationException( "Not implemented yet" );
286+
if ( !session.getDialect().supportsRowValueConstructorSyntaxInInList() ) {
287+
// for now...
288+
throw new UnsupportedOperationException(
289+
"Follow-on collection-table locking with composite keys is not supported for Dialects"
290+
+ " which do not support tuples (row constructor syntax) as part of an in-list"
291+
);
292+
}
293+
294+
final List<ColumnReference> columnReferences = new ArrayList<>( keyDescriptor.getJdbcTypeCount() );
295+
keyDescriptor.forEachSelectable( (selectionIndex, selectableMapping) -> {
296+
columnReferences.add( new ColumnReference( tableReference, selectableMapping ) );
297+
} );
298+
final InListPredicate inListPredicate = new InListPredicate( new SqlTuple( columnReferences, keyDescriptor ) );
299+
300+
ownerDetailsMap.forEach( (o, entityDetails) -> {
301+
final PersistentCollection<?> collectionInstance = (PersistentCollection<?>) entityDetails.entry().getLoadedState()[attributeMapping.getStateArrayPosition()];
302+
final Object collectionKeyValue = collectionInstance.getKey();
303+
304+
final List<JdbcParameter> jdbcParameters = new ArrayList<>( keyDescriptor.getJdbcTypeCount() );
305+
keyDescriptor.breakDownJdbcValues(
306+
collectionKeyValue,
307+
(valueIndex, value, jdbcValueMapping) -> {
308+
final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( jdbcValueMapping.getJdbcMapping() );
309+
jdbcParameters.add( jdbcParameter );
310+
parameterBindings.addBinding(
311+
jdbcParameter,
312+
new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value )
313+
);
314+
},
315+
session
316+
);
317+
inListPredicate.addExpression( new SqlTuple( jdbcParameters, keyDescriptor ) );
318+
} );
319+
320+
return inListPredicate;
183321
}
184322

185323
private static void performLocking(
@@ -262,20 +400,7 @@ public static Collection<NavigablePath> extractPathsToLock(LockingClauseStrategy
262400
return paths;
263401
}
264402

265-
/**
266-
* Collect loaded entities by type.
267-
*/
268-
public static Map<EntityMappingType, List<EntityKey>> segmentLoadedValues(LoadedValuesCollector loadedValuesCollector) {
269-
final Map<EntityMappingType, List<EntityKey>> map = new IdentityHashMap<>();
270-
segmentLoadedValues( loadedValuesCollector.getCollectedRootEntities(), map );
271-
segmentLoadedValues( loadedValuesCollector.getCollectedNonRootEntities(), map );
272-
if ( map.isEmpty() ) {
273-
throw new AssertionFailure( "Expecting some values" );
274-
}
275-
return map;
276-
}
277-
278-
private static void segmentLoadedValues(List<LoadedValuesCollector.LoadedEntityRegistration> registrations, Map<EntityMappingType, List<EntityKey>> map) {
403+
public static void segmentLoadedValues(List<LoadedValuesCollector.LoadedEntityRegistration> registrations, Map<EntityMappingType, List<EntityKey>> map) {
279404
if ( registrations == null ) {
280405
return;
281406
}

0 commit comments

Comments
 (0)