Skip to content

Commit 7422c18

Browse files
committed
HHH-18169 disallow refresh/lock for detached instance
Signed-off-by: Gavin King <[email protected]>
1 parent 1d12dc0 commit 7422c18

File tree

29 files changed

+149
-309
lines changed

29 files changed

+149
-309
lines changed

documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -933,44 +933,6 @@ And finally, serialization will make the deserialized form be detached (the orig
933933

934934
Detached data can still be manipulated, however, the persistence context will no longer automatically know about these modifications, and the application will need to intervene to make the changes persistent again.
935935

936-
[[pc-detach-reattach]]
937-
==== Reattaching detached data
938-
939-
Reattachment is the process of taking an incoming entity instance that is in the detached state and re-associating it with the current persistence context.
940-
941-
[IMPORTANT]
942-
====
943-
Jakarta Persistence does not support reattaching detached data. This is only available through Hibernate `org.hibernate.Session`.
944-
====
945-
946-
[[pc-detach-reattach-lock-example]]
947-
.Reattaching a detached entity using `lock`
948-
====
949-
[source, java, indent=0]
950-
----
951-
include::{example-dir-pc}/PersistenceContextTest.java[tags=pc-detach-reattach-lock-example]
952-
----
953-
====
954-
955-
[[pc-detach-reattach-saveOrUpdate-example]]
956-
.Reattaching a detached entity using `saveOrUpdate`
957-
====
958-
[source, java, indent=0]
959-
----
960-
include::{example-dir-pc}/PersistenceContextTest.java[tags=pc-detach-reattach-saveOrUpdate-example]
961-
----
962-
====
963-
964-
[NOTE]
965-
====
966-
The method name `update` is a bit misleading here.
967-
It does not mean that an `SQL` `UPDATE` is immediately performed.
968-
It does, however, mean that an `SQL` `UPDATE` will be performed when the persistence context is flushed since Hibernate does not know its previous state against which to compare for changes.
969-
If the entity is mapped with `select-before-update`, Hibernate will pull the current state from the database and see if an update is needed.
970-
====
971-
972-
Provided the entity is detached, `update` and `saveOrUpdate` operate exactly the same.
973-
974936
[[pc-merge]]
975937
==== Merging detached data
976938

@@ -1274,17 +1236,6 @@ include::{example-dir-pc}/CascadeDetachTest.java[tags=pc-cascade-detach-example]
12741236
Although unintuitively, `CascadeType.LOCK` does not propagate a lock request from a parent entity to its children.
12751237
Such a use case requires the use of the `PessimisticLockScope.EXTENDED` value of the `jakarta.persistence.lock.scope` property.
12761238

1277-
However, `CascadeType.LOCK` allows us to reattach a parent entity along with its children to the currently running Persistence Context.
1278-
1279-
[[pc-cascade-lock-example]]
1280-
.`CascadeType.LOCK` example
1281-
====
1282-
[source, java, indent=0]
1283-
----
1284-
include::{example-dir-pc}/CascadeLockTest.java[tags=pc-cascade-lock-example]
1285-
----
1286-
====
1287-
12881239
[[pc-cascade-refresh]]
12891240
==== `CascadeType.REFRESH`
12901241

hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, Boo
337337
this.allowRefreshDetachedEntity = configurationService.getSetting(
338338
ALLOW_REFRESH_DETACHED_ENTITY,
339339
BOOLEAN,
340-
true
340+
false
341341
);
342342

343343
this.flushBeforeCompletionEnabled = configurationService.getSetting( FLUSH_BEFORE_COMPLETION, BOOLEAN, true );

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

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ private void fireLock(Object object, LockOptions options) {
659659

660660
private void fireLock(LockEvent event) {
661661
checkOpen();
662+
checkEntityManaged( event.getEntityName(), event.getObject() );
662663
pulseTransactionCoordinator();
663664
fastSessionServices.eventListenerGroup_LOCK
664665
.fireEventOnEachListener( event, LockEventListener::onLock );
@@ -1163,18 +1164,7 @@ public void refresh(String entityName, Object object, RefreshContext refreshedAl
11631164

11641165
private void fireRefresh(final RefreshEvent event) {
11651166
try {
1166-
if ( !getSessionFactory().getSessionFactoryOptions().isAllowRefreshDetachedEntity() ) {
1167-
if ( event.getEntityName() != null ) {
1168-
if ( !contains( event.getEntityName(), event.getObject() ) ) {
1169-
throw new IllegalArgumentException( "Entity not managed" );
1170-
}
1171-
}
1172-
else {
1173-
if ( !contains( event.getObject() ) ) {
1174-
throw new IllegalArgumentException( "Entity not managed" );
1175-
}
1176-
}
1177-
}
1167+
checkEntityManaged( event.getEntityName(), event.getObject() );
11781168
pulseTransactionCoordinator();
11791169
fastSessionServices.eventListenerGroup_REFRESH
11801170
.fireEventOnEachListener( event, RefreshEventListener::onRefresh );
@@ -1195,6 +1185,7 @@ private void fireRefresh(final RefreshEvent event) {
11951185

11961186
private void fireRefresh(final RefreshContext refreshedAlready, final RefreshEvent event) {
11971187
try {
1188+
checkEntityManaged( event.getEntityName(), event.getObject() );
11981189
pulseTransactionCoordinator();
11991190
fastSessionServices.eventListenerGroup_REFRESH
12001191
.fireEventOnEachListener( event, refreshedAlready, RefreshEventListener::onRefresh );
@@ -1207,6 +1198,18 @@ private void fireRefresh(final RefreshContext refreshedAlready, final RefreshEve
12071198
}
12081199
}
12091200

1201+
private void checkEntityManaged(String entityName, Object entity) {
1202+
if ( !getSessionFactory().getSessionFactoryOptions().isAllowRefreshDetachedEntity() ) {
1203+
if ( !managed( entityName, entity ) ) {
1204+
throw new IllegalArgumentException(
1205+
"Given entity is not associated with the persistence context" );
1206+
}
1207+
}
1208+
}
1209+
1210+
private boolean managed(String entityName, Object entity) {
1211+
return entityName == null ? contains( entity ) : contains( entityName, entity );
1212+
}
12101213

12111214
// replicate() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12121215

hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@
8686
import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY;
8787
import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable;
8888
import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable;
89-
import static org.hibernate.internal.log.LoggingHelper.toLoggableString;
9089
import static org.hibernate.metamodel.mapping.ForeignKeyDescriptor.Nature.TARGET;
9190
import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer;
9291

@@ -163,12 +162,9 @@ public EntityInitializerData(EntityInitializerImpl initializer, RowProcessingSta
163162
lockMode = rowProcessingState.determineEffectiveLockMode( initializer.sourceAlias );
164163
if ( initializer.isResultInitializer() ) {
165164
uniqueKeyAttributePath = rowProcessingState.getEntityUniqueKeyAttributePath();
166-
if ( uniqueKeyAttributePath != null ) {
167-
uniqueKeyPropertyTypes = initializer.getParentEntityAttributeTypes( uniqueKeyAttributePath );
168-
}
169-
else {
170-
uniqueKeyPropertyTypes = null;
171-
}
165+
uniqueKeyPropertyTypes = uniqueKeyAttributePath != null
166+
? initializer.getParentEntityAttributeTypes( uniqueKeyAttributePath )
167+
: null;
172168
canUseEmbeddedIdentifierInstanceAsEntity = rowProcessingState.getEntityId() != null
173169
&& initializer.couldUseEmbeddedIdentifierInstanceAsEntity;
174170
}
@@ -178,13 +174,10 @@ public EntityInitializerData(EntityInitializerImpl initializer, RowProcessingSta
178174
canUseEmbeddedIdentifierInstanceAsEntity = false;
179175
}
180176
hasCallbackActions = rowProcessingState.hasCallbackActions();
181-
if ( initializer.discriminatorAssembler == null
182-
|| rowProcessingState.isQueryCacheHit() && entityDescriptor.useShallowQueryCacheLayout() && !entityDescriptor.storeDiscriminatorInShallowQueryCacheLayout() ) {
183-
defaultConcreteDescriptor = entityDescriptor;
184-
}
185-
else {
186-
defaultConcreteDescriptor = null;
187-
}
177+
defaultConcreteDescriptor =
178+
hasConcreteDescriptor( rowProcessingState, initializer.discriminatorAssembler, entityDescriptor )
179+
? entityDescriptor
180+
: null;
188181
}
189182

190183
/*
@@ -206,6 +199,16 @@ public EntityInitializerData(EntityInitializerData original) {
206199
}
207200
}
208201

202+
private static boolean hasConcreteDescriptor(
203+
RowProcessingState rowProcessingState,
204+
BasicResultAssembler<?> discriminatorAssembler,
205+
EntityPersister entityDescriptor) {
206+
return discriminatorAssembler == null
207+
|| rowProcessingState.isQueryCacheHit()
208+
&& entityDescriptor.useShallowQueryCacheLayout()
209+
&& !entityDescriptor.storeDiscriminatorInShallowQueryCacheLayout();
210+
}
211+
209212
public EntityInitializerImpl(
210213
EntityResultGraphNode resultDescriptor,
211214
String sourceAlias,
@@ -229,8 +232,9 @@ public EntityInitializerImpl(
229232
: entityDescriptor.getRootEntityDescriptor().getEntityPersister();
230233
keyTypeForEqualsHashCode = entityDescriptor.getIdentifierType().getTypeForEqualsHashCode();
231234
// The id can only be the entity instance if this is a non-aggregated id that has no containing class
232-
couldUseEmbeddedIdentifierInstanceAsEntity = entityDescriptor.getIdentifierMapping() instanceof CompositeIdentifierMapping
233-
&& !( (CompositeIdentifierMapping) entityDescriptor.getIdentifierMapping() ).hasContainingClass();
235+
couldUseEmbeddedIdentifierInstanceAsEntity =
236+
entityDescriptor.getIdentifierMapping() instanceof CompositeIdentifierMapping
237+
&& !( (CompositeIdentifierMapping) entityDescriptor.getIdentifierMapping() ).hasContainingClass();
234238

235239
this.navigablePath = resultDescriptor.getNavigablePath();
236240
this.sourceAlias = sourceAlias;
@@ -258,7 +262,8 @@ public EntityInitializerImpl(
258262
hasKeyManyToOne = initializer != null && initializer.isLazyCapable();
259263
}
260264

261-
assert entityDescriptor.hasSubclasses() == (discriminatorFetch != null) : "Discriminator should only be fetched if the entity has subclasses";
265+
assert entityDescriptor.hasSubclasses() == (discriminatorFetch != null)
266+
: "Discriminator should only be fetched if the entity has subclasses";
262267
discriminatorAssembler = discriminatorFetch != null
263268
? (BasicResultAssembler<?>) discriminatorFetch.createAssembler( this, creationState )
264269
: null;
@@ -286,12 +291,16 @@ public EntityInitializerImpl(
286291
final BitSet[] lazySets = new BitSet[subMappingTypes.size() + 1];
287292
final BitSet[] maybeLazySets = new BitSet[subMappingTypes.size() + 1];
288293
final MutabilityPlan[][] updatableAttributeMutabilityPlans = new MutabilityPlan[subMappingTypes.size() + 1][];
289-
assemblers[rootEntityDescriptor.getSubclassId()] = new DomainResultAssembler[rootEntityDescriptor.getNumberOfFetchables()];
290-
updatableAttributeMutabilityPlans[rootEntityDescriptor.getSubclassId()] = new MutabilityPlan[rootEntityDescriptor.getNumberOfAttributeMappings()];
294+
assemblers[rootEntityDescriptor.getSubclassId()] =
295+
new DomainResultAssembler[rootEntityDescriptor.getNumberOfFetchables()];
296+
updatableAttributeMutabilityPlans[rootEntityDescriptor.getSubclassId()] =
297+
new MutabilityPlan[rootEntityDescriptor.getNumberOfAttributeMappings()];
291298

292299
for ( EntityMappingType subMappingType : subMappingTypes ) {
293-
assemblers[subMappingType.getSubclassId()] = new DomainResultAssembler[subMappingType.getNumberOfFetchables()];
294-
updatableAttributeMutabilityPlans[subMappingType.getSubclassId()] = new MutabilityPlan[subMappingType.getNumberOfAttributeMappings()];
300+
assemblers[subMappingType.getSubclassId()] =
301+
new DomainResultAssembler[subMappingType.getNumberOfFetchables()];
302+
updatableAttributeMutabilityPlans[subMappingType.getSubclassId()] =
303+
new MutabilityPlan[subMappingType.getNumberOfAttributeMappings()];
295304
}
296305

297306
boolean hasLazySubInitializers = false;
@@ -345,7 +354,8 @@ public EntityInitializerImpl(
345354
}
346355
for ( EntityMappingType subMappingType : declaringType.getSubMappingTypes() ) {
347356
assemblers[subMappingType.getSubclassId()][stateArrayPosition] = stateAssembler;
348-
updatableAttributeMutabilityPlans[subMappingType.getSubclassId()][stateArrayPosition] = updatableAttributeMutabilityPlans[subclassId][stateArrayPosition];
357+
updatableAttributeMutabilityPlans[subMappingType.getSubclassId()][stateArrayPosition] =
358+
updatableAttributeMutabilityPlans[subclassId][stateArrayPosition];
349359
if ( subInitializer != null ) {
350360
if ( subInitializers[subMappingType.getSubclassId()] == null ) {
351361
subInitializers[subMappingType.getSubclassId()] = new Initializer<?>[size];
@@ -355,12 +365,14 @@ public EntityInitializerImpl(
355365
maybeLazySets[subMappingType.getSubclassId()] = new BitSet(size);
356366
}
357367
subInitializers[subMappingType.getSubclassId()][stateArrayPosition] = subInitializer;
358-
eagerSubInitializers[subMappingType.getSubclassId()][stateArrayPosition] = eagerSubInitializers[subclassId][stateArrayPosition];
359-
collectionContainingSubInitializers[subMappingType.getSubclassId()][stateArrayPosition] = collectionContainingSubInitializers[subclassId][stateArrayPosition];
360-
if (lazySets[subclassId].get( stateArrayPosition ) ) {
368+
eagerSubInitializers[subMappingType.getSubclassId()][stateArrayPosition] =
369+
eagerSubInitializers[subclassId][stateArrayPosition];
370+
collectionContainingSubInitializers[subMappingType.getSubclassId()][stateArrayPosition] =
371+
collectionContainingSubInitializers[subclassId][stateArrayPosition];
372+
if ( lazySets[subclassId].get( stateArrayPosition ) ) {
361373
lazySets[subMappingType.getSubclassId()].set( stateArrayPosition );
362374
}
363-
if (maybeLazySets[subclassId].get( stateArrayPosition ) ) {
375+
if ( maybeLazySets[subclassId].get( stateArrayPosition ) ) {
364376
maybeLazySets[subMappingType.getSubclassId()].set( stateArrayPosition );
365377
}
366378
}
@@ -411,9 +423,10 @@ public EntityInitializerImpl(
411423

412424
this.assemblers = assemblers;
413425
this.subInitializers = subInitializers;
414-
this.subInitializersForResolveFromInitialized = rootEntityDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading()
415-
? subInitializers
416-
: eagerSubInitializers;
426+
this.subInitializersForResolveFromInitialized =
427+
rootEntityDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading()
428+
? subInitializers
429+
: eagerSubInitializers;
417430
this.collectionContainingSubInitializers = collectionContainingSubInitializers;
418431
this.lazySets = Arrays.stream( lazySets ).map( ImmutableBitSet::valueOf ).toArray( ImmutableBitSet[]::new );
419432
this.maybeLazySets = Arrays.stream( maybeLazySets )
@@ -531,7 +544,8 @@ protected void resolveKey(EntityInitializerData data, boolean entityKeyOnly) {
531544
}
532545
else {
533546
//noinspection unchecked
534-
final Initializer<InitializerData> initializer = (Initializer<InitializerData>) identifierAssembler.getInitializer();
547+
final Initializer<InitializerData> initializer =
548+
(Initializer<InitializerData>) identifierAssembler.getInitializer();
535549
if ( initializer != null ) {
536550
final InitializerData subData = initializer.getData( rowProcessingState );
537551
initializer.resolveKey( subData );
@@ -785,7 +799,8 @@ public boolean isResultInitializer() {
785799
}
786800

787801
private void deepCopy(EntityPersister containerDescriptor, Object[] source, Object[] target) {
788-
final MutabilityPlan<Object>[] updatableAttributeMutabilityPlan = updatableAttributeMutabilityPlans[containerDescriptor.getSubclassId()];
802+
final MutabilityPlan<Object>[] updatableAttributeMutabilityPlan =
803+
updatableAttributeMutabilityPlans[containerDescriptor.getSubclassId()];
789804
for ( int i = 0; i < updatableAttributeMutabilityPlan.length; i++ ) {
790805
final Object sourceValue = source[i];
791806
if ( updatableAttributeMutabilityPlan[i] != null
@@ -830,11 +845,7 @@ public Object getTargetInstance(EntityInitializerData data) {
830845
protected Type[] getParentEntityAttributeTypes(String attributeName) {
831846
Type[] types = parentEntityAttributeTypes.get( attributeName );
832847
if ( types == null ) {
833-
types = new Type[
834-
entityDescriptor.getRootEntityDescriptor()
835-
.getSubclassEntityNames()
836-
.size()
837-
];
848+
types = new Type[entityDescriptor.getRootEntityDescriptor().getSubclassEntityNames().size()];
838849
initializeAttributeType( types, entityDescriptor, attributeName );
839850
for ( EntityMappingType subMappingType : entityDescriptor.getSubMappingTypes() ) {
840851
initializeAttributeType( types, subMappingType.getEntityPersister(), attributeName );
@@ -855,12 +866,12 @@ protected void initializeAttributeType(Type[] attributeTypes, EntityPersister en
855866
@Nullable BasicResultAssembler<?> discriminatorAssembler,
856867
EntityPersister entityDescriptor)
857868
throws WrongClassException {
858-
if ( discriminatorAssembler == null
859-
|| rowProcessingState.isQueryCacheHit() && entityDescriptor.useShallowQueryCacheLayout() && !entityDescriptor.storeDiscriminatorInShallowQueryCacheLayout() ) {
869+
if ( hasConcreteDescriptor( rowProcessingState, discriminatorAssembler, entityDescriptor ) ) {
860870
return entityDescriptor;
861871
}
862872
else {
863-
assert entityDescriptor.hasSubclasses() : "Reading a discriminator from a result set should only happen if the entity has subclasses";
873+
assert entityDescriptor.hasSubclasses()
874+
: "Reading a discriminator from a result set should only happen if the entity has subclasses";
864875
final EntityDiscriminatorMapping discriminatorMapping = entityDescriptor.getDiscriminatorMapping();
865876
assert discriminatorMapping != null;
866877
final Object discriminator = discriminatorAssembler.extractRawValue( rowProcessingState );
@@ -888,9 +899,16 @@ protected void initializeAttributeType(Type[] attributeTypes, EntityPersister en
888899
}
889900

890901
protected boolean useEmbeddedIdentifierInstanceAsEntity(EntityInitializerData data) {
891-
return data.canUseEmbeddedIdentifierInstanceAsEntity
892-
&& ( data.concreteDescriptor = determineConcreteEntityDescriptor( data.getRowProcessingState(), discriminatorAssembler, entityDescriptor ) ) != null
902+
if ( data.canUseEmbeddedIdentifierInstanceAsEntity ) {
903+
data.concreteDescriptor =
904+
determineConcreteEntityDescriptor( data.getRowProcessingState(),
905+
discriminatorAssembler, entityDescriptor );
906+
return data.concreteDescriptor != null
893907
&& data.concreteDescriptor.isInstance( data.getRowProcessingState().getEntityId() );
908+
}
909+
else {
910+
return false;
911+
}
894912
}
895913

896914
@Override
@@ -918,7 +936,7 @@ public void resolveInstance(Object instance, EntityInitializerData data) {
918936
resolveKey( data );
919937
assert data.getState() == State.MISSING;
920938
assert referencedModelPart instanceof ToOneAttributeMapping
921-
&& ( (ToOneAttributeMapping) referencedModelPart ).getSideNature() == TARGET;
939+
&& ( (ToOneAttributeMapping) referencedModelPart ).getSideNature() == TARGET;
922940
return;
923941
}
924942
// If the entity initializer is null, we know the entity is fully initialized,
@@ -1132,7 +1150,7 @@ protected void upgradeLockMode(EntityInitializerData data) {
11321150

11331151
private boolean isProxyInstance(Object proxy) {
11341152
return proxy != null
1135-
&& ( proxy instanceof MapProxy || entityDescriptor.getJavaType().getJavaTypeClass().isInstance( proxy ) );
1153+
&& ( proxy instanceof MapProxy || entityDescriptor.getJavaType().getJavaTypeClass().isInstance( proxy ) );
11361154
}
11371155

11381156
private boolean isExistingEntityInitialized(Object existingEntity) {

hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/HHH_10708/UnexpectedDeleteTest2.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88

99
import static org.junit.jupiter.api.Assertions.assertFalse;
1010

11+
import org.hibernate.cfg.AvailableSettings;
1112
import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced;
1213
import org.hibernate.testing.orm.junit.DomainModel;
1314
import org.hibernate.testing.orm.junit.JiraKey;
15+
import org.hibernate.testing.orm.junit.ServiceRegistry;
1416
import org.hibernate.testing.orm.junit.SessionFactory;
1517
import org.hibernate.testing.orm.junit.SessionFactoryScope;
18+
import org.hibernate.testing.orm.junit.Setting;
1619
import org.junit.jupiter.api.BeforeEach;
1720
import org.junit.jupiter.api.Test;
1821

@@ -33,6 +36,7 @@
3336
)
3437
@SessionFactory
3538
@BytecodeEnhanced
39+
@ServiceRegistry(settings = @Setting(name = AvailableSettings.ALLOW_REFRESH_DETACHED_ENTITY, value = "true"))
3640
public class UnexpectedDeleteTest2 {
3741

3842
private Bar myBar;

0 commit comments

Comments
 (0)