Skip to content

Commit 6bc4324

Browse files
committed
Ensure fetched collection tables are locked when LockScope.INCLUDE_FETCHES is used
1 parent b9b2d06 commit 6bc4324

File tree

2 files changed

+200
-0
lines changed

2 files changed

+200
-0
lines changed

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.sql.Connection;
3838
import java.util.ArrayList;
3939
import java.util.Collection;
40+
import java.util.Collections;
4041
import java.util.HashMap;
4142
import java.util.IdentityHashMap;
4243
import java.util.List;
@@ -109,6 +110,8 @@ public void performPostAction(
109110
try {
110111
// collect registrations by entity type
111112
final Map<EntityMappingType, List<EntityKey>> entitySegments = segmentLoadedValues();
113+
final Map<EntityMappingType, Map<PluralAttributeMapping, List<CollectionKey>>> collectionSegments =
114+
lockScope == Locking.Scope.INCLUDE_FETCHES ? segmentLoadedCollections() : Collections.emptyMap();
112115

113116
// for each entity-type, prepare a locking select statement per table.
114117
// this is based on the attributes for "state array" ordering purposes -
@@ -166,6 +169,23 @@ public void performPostAction(
166169
);
167170
} );
168171
}
172+
else if ( lockScope == Locking.Scope.INCLUDE_FETCHES
173+
&& loadedValuesCollector.getCollectedCollections() != null
174+
&& !loadedValuesCollector.getCollectedCollections().isEmpty() ) {
175+
final Map<PluralAttributeMapping, List<CollectionKey>> attributeKeys =
176+
collectionSegments.get( entityMappingType );
177+
if ( attributeKeys != null ) {
178+
for ( var entry : attributeKeys.entrySet() ) {
179+
LockingHelper.lockCollectionTable(
180+
entry.getKey(),
181+
lockMode,
182+
lockTimeout,
183+
entry.getValue(),
184+
executionContext
185+
);
186+
}
187+
}
188+
}
169189

170190

171191
// at this point, we have all the individual locking selects ready to go - execute them
@@ -219,6 +239,12 @@ private Map<EntityMappingType, List<EntityKey>> segmentLoadedValues() {
219239
return map;
220240
}
221241

242+
private Map<EntityMappingType, Map<PluralAttributeMapping, List<CollectionKey>>> segmentLoadedCollections() {
243+
final Map<EntityMappingType, Map<PluralAttributeMapping, List<CollectionKey>>> map = new HashMap<>();
244+
LockingHelper.segmentLoadedCollections( loadedValuesCollector.getCollectedCollections(), map );
245+
return map;
246+
}
247+
222248
private Map<String, TableLock> prepareTableLocks(
223249
EntityMappingType entityMappingType,
224250
List<EntityKey> entityKeys,

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

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.hibernate.Session;
1111
import org.hibernate.collection.spi.PersistentCollection;
1212
import org.hibernate.engine.jdbc.spi.JdbcServices;
13+
import org.hibernate.engine.spi.CollectionKey;
1314
import org.hibernate.engine.spi.EntityEntry;
1415
import org.hibernate.engine.spi.EntityKey;
1516
import org.hibernate.engine.spi.PersistenceContext;
@@ -320,6 +321,158 @@ private static InListPredicate applyCompositeCollectionKeyTableLockRestrictions(
320321
return inListPredicate;
321322
}
322323

324+
/**
325+
* Lock a collection-table.
326+
*
327+
* @param attributeMapping The plural attribute whose table needs locked.
328+
* @param lockMode The lock mode to apply
329+
* @param lockTimeout A lock timeout to apply, if one.
330+
* @param collectionKeys Keys of collection-table rows that should be locked.
331+
*/
332+
public static void lockCollectionTable(
333+
PluralAttributeMapping attributeMapping,
334+
LockMode lockMode,
335+
Timeout lockTimeout,
336+
List<CollectionKey> collectionKeys,
337+
ExecutionContext executionContext) {
338+
final ForeignKeyDescriptor keyDescriptor = attributeMapping.getKeyDescriptor();
339+
final String keyTableName = keyDescriptor.getKeyTable();
340+
341+
if ( SQL_EXEC_LOGGER.isDebugEnabled() ) {
342+
SQL_EXEC_LOGGER.debugf( "Follow-on locking for collection table `%s` - %s", keyTableName, attributeMapping.getRootPathName() );
343+
}
344+
345+
final QuerySpec querySpec = new QuerySpec( true );
346+
347+
final NamedTableReference tableReference = new NamedTableReference( keyTableName, "tbl" );
348+
final LockingTableGroup tableGroup = new LockingTableGroup(
349+
tableReference,
350+
keyTableName,
351+
attributeMapping,
352+
keyDescriptor.getKeySide().getModelPart()
353+
);
354+
355+
querySpec.getFromClause().addRoot( tableGroup );
356+
357+
final ValuedModelPart keyPart = keyDescriptor.getKeyPart();
358+
final ColumnReference columnReference = new ColumnReference( tableReference, keyPart.getSelectable( 0 ) );
359+
360+
// NOTE: We add the key column to the selection list, but never create a DomainResult
361+
// as we won't read the value back. Ideally, we would read the "value column(s)" and
362+
// update the collection state accordingly much like is done for entity state -
363+
// however, the concern is minor, so for simplicity we do not.
364+
final SqlSelectionImpl sqlSelection = new SqlSelectionImpl( columnReference, 0 );
365+
querySpec.getSelectClause().addSqlSelection( sqlSelection );
366+
367+
final int expectedParamCount = collectionKeys.size() * keyDescriptor.getJdbcTypeCount();
368+
final JdbcParameterBindingsImpl parameterBindings = new JdbcParameterBindingsImpl( expectedParamCount );
369+
370+
final InListPredicate restriction;
371+
if ( keyDescriptor.getJdbcTypeCount() == 1 ) {
372+
restriction = new InListPredicate( columnReference );
373+
applySimpleCollectionKeyTableLockRestrictions(
374+
attributeMapping,
375+
keyDescriptor,
376+
restriction,
377+
parameterBindings,
378+
collectionKeys,
379+
executionContext.getSession()
380+
);
381+
}
382+
else {
383+
restriction = applyCompositeCollectionKeyTableLockRestrictions(
384+
attributeMapping,
385+
keyDescriptor,
386+
tableReference,
387+
parameterBindings,
388+
collectionKeys,
389+
executionContext.getSession()
390+
);
391+
}
392+
querySpec.applyPredicate( restriction );
393+
394+
final QueryOptionsImpl lockingQueryOptions = new QueryOptionsImpl();
395+
lockingQueryOptions.getLockOptions().setLockMode( lockMode );
396+
lockingQueryOptions.getLockOptions().setTimeout( lockTimeout );
397+
final ExecutionContext lockingExecutionContext = new BaseExecutionContext( executionContext.getSession() ) {
398+
@Override
399+
public QueryOptions getQueryOptions() {
400+
return lockingQueryOptions;
401+
}
402+
};
403+
404+
performLocking( querySpec, parameterBindings, lockingExecutionContext );
405+
}
406+
407+
private static void applySimpleCollectionKeyTableLockRestrictions(
408+
PluralAttributeMapping attributeMapping,
409+
ForeignKeyDescriptor keyDescriptor,
410+
InListPredicate restriction,
411+
JdbcParameterBindingsImpl parameterBindings,
412+
List<CollectionKey> collectionKeys,
413+
SharedSessionContractImplementor session) {
414+
for ( CollectionKey collectionKey : collectionKeys ) {
415+
final Object collectionKeyValue = collectionKey.getKey();
416+
keyDescriptor.breakDownJdbcValues(
417+
collectionKeyValue,
418+
(valueIndex, value, jdbcValueMapping) -> {
419+
final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl(
420+
jdbcValueMapping.getJdbcMapping() );
421+
restriction.addExpression( jdbcParameter );
422+
423+
parameterBindings.addBinding(
424+
jdbcParameter,
425+
new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value )
426+
);
427+
},
428+
session
429+
);
430+
}
431+
}
432+
433+
private static InListPredicate applyCompositeCollectionKeyTableLockRestrictions(
434+
PluralAttributeMapping attributeMapping,
435+
ForeignKeyDescriptor keyDescriptor,
436+
TableReference tableReference,
437+
JdbcParameterBindingsImpl parameterBindings,
438+
List<CollectionKey> collectionKeys,
439+
SharedSessionContractImplementor session) {
440+
if ( !session.getDialect().supportsRowValueConstructorSyntaxInInList() ) {
441+
// for now...
442+
throw new UnsupportedOperationException(
443+
"Follow-on collection-table locking with composite keys is not supported for Dialects"
444+
+ " which do not support tuples (row constructor syntax) as part of an in-list"
445+
);
446+
}
447+
448+
final List<ColumnReference> columnReferences = new ArrayList<>( keyDescriptor.getJdbcTypeCount() );
449+
keyDescriptor.forEachSelectable( (selectionIndex, selectableMapping) -> {
450+
columnReferences.add( new ColumnReference( tableReference, selectableMapping ) );
451+
} );
452+
final InListPredicate inListPredicate = new InListPredicate( new SqlTuple( columnReferences, keyDescriptor ) );
453+
454+
for ( CollectionKey collectionKey : collectionKeys ) {
455+
final Object collectionKeyValue = collectionKey.getKey();
456+
457+
final List<JdbcParameter> jdbcParameters = new ArrayList<>( keyDescriptor.getJdbcTypeCount() );
458+
keyDescriptor.breakDownJdbcValues(
459+
collectionKeyValue,
460+
(valueIndex, value, jdbcValueMapping) -> {
461+
final JdbcParameterImpl jdbcParameter = new JdbcParameterImpl( jdbcValueMapping.getJdbcMapping() );
462+
jdbcParameters.add( jdbcParameter );
463+
parameterBindings.addBinding(
464+
jdbcParameter,
465+
new JdbcParameterBindingImpl( jdbcValueMapping.getJdbcMapping(), value )
466+
);
467+
},
468+
session
469+
);
470+
inListPredicate.addExpression( new SqlTuple( jdbcParameters, keyDescriptor ) );
471+
}
472+
473+
return inListPredicate;
474+
}
475+
323476
private static void performLocking(
324477
QuerySpec querySpec,
325478
JdbcParameterBindings jdbcParameterBindings,
@@ -414,6 +567,27 @@ public static void segmentLoadedValues(List<LoadedValuesCollector.LoadedEntityRe
414567
} );
415568
}
416569

570+
public static void segmentLoadedCollections(List<LoadedValuesCollector.LoadedCollectionRegistration> registrations, Map<EntityMappingType, Map<PluralAttributeMapping, List<CollectionKey>>> map) {
571+
if ( registrations == null ) {
572+
return;
573+
}
574+
575+
registrations.forEach( (registration) -> {
576+
final PluralAttributeMapping pluralAttributeMapping = registration.collectionDescriptor();
577+
if ( pluralAttributeMapping.getSeparateCollectionTable() != null ) {
578+
final Map<PluralAttributeMapping, List<CollectionKey>> attributeKeys = map.computeIfAbsent(
579+
pluralAttributeMapping.findContainingEntityMapping(),
580+
entityMappingType -> new HashMap<>()
581+
);
582+
final List<CollectionKey> collectionKeys = attributeKeys.computeIfAbsent(
583+
pluralAttributeMapping,
584+
entityMappingType -> new ArrayList<>()
585+
);
586+
collectionKeys.add( registration.collectionKey() );
587+
}
588+
} );
589+
}
590+
417591
public static Map<Object, EntityDetails> resolveEntityKeys(List<EntityKey> entityKeys, ExecutionContext executionContext) {
418592
final Map<Object, EntityDetails> map = new HashMap<>();
419593
final PersistenceContext persistenceContext = executionContext.getSession().getPersistenceContext();

0 commit comments

Comments
 (0)