light refactoring to ForeignKeys

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-06-04 12:43:52 +02:00
parent b687120bad
commit 92f61c0956
2 changed files with 127 additions and 102 deletions

View File

@ -13,16 +13,17 @@ import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SelfDirtinessTracker; import org.hibernate.engine.spi.SelfDirtinessTracker;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer; import org.hibernate.proxy.LazyInitializer;
import org.hibernate.type.CompositeType; import org.hibernate.type.CompositeType;
import org.hibernate.type.EntityType; import org.hibernate.type.EntityType;
import org.hibernate.type.Type; import org.hibernate.type.Type;
import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY;
import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy; import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy;
import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker; import static org.hibernate.engine.internal.ManagedTypeHelper.processIfSelfDirtinessTracker;
import static org.hibernate.internal.util.StringHelper.qualify;
import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer;
/** /**
* Algorithms related to foreign key constraint transparency * Algorithms related to foreign key constraint transparency
@ -88,77 +89,83 @@ public final class ForeignKeys {
* @return {@code null} if the argument is an unsaved entity; otherwise return the argument. * @return {@code null} if the argument is an unsaved entity; otherwise return the argument.
*/ */
private Object nullifyTransientReferences(final Object value, final String propertyName, final Type type) { private Object nullifyTransientReferences(final Object value, final String propertyName, final Type type) {
final Object returnedValue; final Object returnedValue = nullifyTransient(value, propertyName, type);
if ( value == null ) {
returnedValue = null;
}
else if ( type.isEntityType() ) {
final EntityType entityType = (EntityType) type;
if ( entityType.isOneToOne() ) {
returnedValue = value;
}
else {
// If value is lazy, it may need to be initialized to
// determine if the value is nullifiable.
final Object possiblyInitializedValue = initializeIfNecessary( value, propertyName, entityType );
if ( possiblyInitializedValue == null ) {
// The uninitialized value was initialized to null
returnedValue = null;
}
else {
// If the value is not nullifiable, make sure that the
// possibly initialized value is returned.
returnedValue = isNullifiable( entityType.getAssociatedEntityName(), possiblyInitializedValue )
? null
: possiblyInitializedValue;
}
}
}
else if ( type.isAnyType() ) {
returnedValue = isNullifiable( null, value ) ? null : value;
}
else if ( type.isComponentType() ) {
final CompositeType compositeType = (CompositeType) type;
final Object[] subvalues = compositeType.getPropertyValues( value, session );
final Type[] subtypes = compositeType.getSubtypes();
final String[] subPropertyNames = compositeType.getPropertyNames();
boolean substitute = false;
for ( int i = 0; i < subvalues.length; i++ ) {
final Object replacement = nullifyTransientReferences(
subvalues[i],
StringHelper.qualify( propertyName, subPropertyNames[i] ),
subtypes[i]
);
if ( replacement != subvalues[i] ) {
substitute = true;
subvalues[i] = replacement;
}
}
if ( substitute ) {
// todo : need to account for entity mode on the CompositeType interface :(
compositeType.setPropertyValues( value, subvalues );
}
returnedValue = value;
}
else {
returnedValue = value;
}
// value != returnedValue if either: // value != returnedValue if either:
// 1) returnedValue was nullified (set to null); // 1) returnedValue was nullified (set to null);
// or 2) returnedValue was initialized, but not nullified. // or 2) returnedValue was initialized, but not nullified.
// When bytecode-enhancement is used for dirty-checking, the change should // When bytecode-enhancement is used for dirty-checking, the change should
// only be tracked when returnedValue was nullified (1)). // only be tracked when returnedValue was nullified (1).
if ( value != returnedValue && returnedValue == null ) { if ( value != returnedValue && returnedValue == null ) {
processIfSelfDirtinessTracker( self, SelfDirtinessTracker::$$_hibernate_trackChange, propertyName ); processIfSelfDirtinessTracker( self, SelfDirtinessTracker::$$_hibernate_trackChange, propertyName );
} }
return returnedValue; return returnedValue;
} }
private Object nullifyTransient(Object value, String propertyName, Type type) {
if ( value == null ) {
return null;
}
else if ( type.isEntityType() ) {
return nullifyEntityType( value, propertyName, (EntityType) type );
}
else if ( type.isAnyType() ) {
return isNullifiable( null, value) ? null : value;
}
else if ( type.isComponentType() ) {
return nullifyCompositeType( value, propertyName, (CompositeType) type );
}
else {
return value;
}
}
private Object nullifyCompositeType(Object value, String propertyName, CompositeType compositeType) {
final Object[] subvalues = compositeType.getPropertyValues(value, session );
final Type[] subtypes = compositeType.getSubtypes();
final String[] subPropertyNames = compositeType.getPropertyNames();
boolean substitute = false;
for ( int i = 0; i < subvalues.length; i++ ) {
final Object replacement = nullifyTransientReferences(
subvalues[i],
qualify( propertyName, subPropertyNames[i] ),
subtypes[i]
);
if ( replacement != subvalues[i] ) {
substitute = true;
subvalues[i] = replacement;
}
}
if ( substitute ) {
// todo : need to account for entity mode on the CompositeType interface :(
compositeType.setPropertyValues( value, subvalues );
}
return value;
}
private Object nullifyEntityType(Object value, String propertyName, EntityType entityType) {
if ( entityType.isOneToOne() ) {
return value;
}
else {
// If value is lazy, it may need to be initialized to
// determine if the value is nullifiable.
final Object possiblyInitializedValue = initializeIfNecessary(value, propertyName, entityType );
if ( possiblyInitializedValue == null ) {
// The uninitialized value was initialized to null
return null;
}
else {
// If the value is not nullifiable, make sure that the
// possibly initialized value is returned.
return isNullifiable(entityType.getAssociatedEntityName(), possiblyInitializedValue)
? null
: possiblyInitializedValue;
}
}
}
private Object initializeIfNecessary(final Object value, final String propertyName, final Type type) { private Object initializeIfNecessary(final Object value, final String propertyName, final Type type) {
if ( isDelete if ( initializationIsNecessary( value, type ) ) {
&& value == LazyPropertyInitializer.UNFETCHED_PROPERTY
&& type.isEntityType()
&& !session.getPersistenceContextInternal().isNullifiableEntityKeysEmpty() ) {
// IMPLEMENTATION NOTE: If cascade-remove was mapped for the attribute, // IMPLEMENTATION NOTE: If cascade-remove was mapped for the attribute,
// then value should have been initialized previously, when the remove operation was // then value should have been initialized previously, when the remove operation was
// cascaded to the property (because CascadingAction.DELETE.performOnLazyProperty() // cascaded to the property (because CascadingAction.DELETE.performOnLazyProperty()
@ -170,18 +177,26 @@ public final class ForeignKeys {
// the property is not nullified, then a constraint violation will result. // the property is not nullified, then a constraint violation will result.
// The only way to find out if the associated entity is nullifiable is // The only way to find out if the associated entity is nullifiable is
// to initialize it. // to initialize it.
// TODO: there may be ways to fine-tune when initialization is necessary return ( (LazyPropertyInitializer) persister )
// (e.g., only initialize when the associated entity type is a .initializeLazyProperty( propertyName, self, session );
// superclass or the same as the entity type of a nullifiable entity).
// It is unclear if a more complicated check would impact performance
// more than just initializing the associated entity.
return ( (LazyPropertyInitializer) persister ).initializeLazyProperty( propertyName, self, session );
} }
else { else {
return value; return value;
} }
} }
private boolean initializationIsNecessary(Object value, Type type) {
// TODO: there may be ways to fine-tune when initialization is necessary
// (e.g., only initialize when the associated entity type is a
// superclass or the same as the entity type of a nullifiable entity).
// It is unclear if a more complicated check would impact performance
// more than just initializing the associated entity.
return isDelete
&& value == UNFETCHED_PROPERTY
&& type.isEntityType()
&& !session.getPersistenceContextInternal().isNullifiableEntityKeysEmpty();
}
/** /**
* Determine if the object already exists in the database, * Determine if the object already exists in the database,
* using a "best guess" * using a "best guess"
@ -191,21 +206,20 @@ public final class ForeignKeys {
*/ */
private boolean isNullifiable(final String entityName, Object object) private boolean isNullifiable(final String entityName, Object object)
throws HibernateException { throws HibernateException {
if ( object == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { if ( object == UNFETCHED_PROPERTY ) {
// this is the best we can do... // this is the best we can do...
return false; return false;
} }
final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
final LazyInitializer lazyInitializer = extractLazyInitializer( object );
final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( object );
if ( lazyInitializer != null ) { if ( lazyInitializer != null ) {
// if it's an uninitialized proxy it can only be // if it's an uninitialized proxy it can only be
// transient if we did an unloaded-delete on the // transient if we did an unloaded-delete on the
// proxy itself, in which case there is no entry // proxy itself, in which case there is no entry
// for it, but its key has already been registered // for it, but its key has already been registered
// as nullifiable // as nullifiable
Object entity = lazyInitializer.getImplementation( session ); final Object entity = lazyInitializer.getImplementation( session );
if ( entity == null ) { if ( entity == null ) {
// an unloaded proxy might be scheduled for deletion // an unloaded proxy might be scheduled for deletion
return persistenceContext.containsDeletedUnloadedEntityKey( return persistenceContext.containsDeletedUnloadedEntityKey(
@ -227,7 +241,7 @@ public final class ForeignKeys {
// case we definitely need to nullify // case we definitely need to nullify
if ( object == self ) { if ( object == self ) {
return isEarlyInsert return isEarlyInsert
|| isDelete && session.getFactory().getJdbcServices().getDialect().hasSelfReferentialForeignKeyBug(); || isDelete && hasSelfReferentialForeignKeyBug();
} }
// See if the entity is already bound to this session, if not look at the // See if the entity is already bound to this session, if not look at the
@ -240,6 +254,11 @@ public final class ForeignKeys {
? isTransient( entityName, object, null, session ) ? isTransient( entityName, object, null, session )
: entityEntry.isNullifiable( isEarlyInsert, session ); : entityEntry.isNullifiable( isEarlyInsert, session );
} }
private boolean hasSelfReferentialForeignKeyBug() {
return session.getFactory().getJdbcServices().getDialect()
.hasSelfReferentialForeignKeyBug();
}
} }
/** /**
@ -255,7 +274,8 @@ public final class ForeignKeys {
* *
* @return {@code true} if the given entity is not transient (meaning it is either detached/persistent) * @return {@code true} if the given entity is not transient (meaning it is either detached/persistent)
*/ */
public static boolean isNotTransient(String entityName, Object entity, Boolean assumed, SharedSessionContractImplementor session) { public static boolean isNotTransient(
String entityName, Object entity, Boolean assumed, SharedSessionContractImplementor session) {
return isHibernateProxy( entity ) return isHibernateProxy( entity )
|| session.getPersistenceContextInternal().isEntryFor( entity ) || session.getPersistenceContextInternal().isEntryFor( entity )
// todo : shouldn't assumed be reversed here? // todo : shouldn't assumed be reversed here?
@ -275,37 +295,38 @@ public final class ForeignKeys {
* *
* @return {@code true} if the given entity is transient (unsaved) * @return {@code true} if the given entity is transient (unsaved)
*/ */
public static boolean isTransient(String entityName, Object entity, Boolean assumed, SharedSessionContractImplementor session) { public static boolean isTransient(
if ( entity == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { String entityName, Object entity, Boolean assumed, SharedSessionContractImplementor session) {
if ( entity == UNFETCHED_PROPERTY ) {
// an unfetched association can only point to // an unfetched association can only point to
// an entity that already exists in the db // an entity that already exists in the db
return false; return false;
} }
// let the interceptor inspect the instance to decide // let the interceptor inspect the instance to decide
Boolean isUnsaved = session.getInterceptor().isTransient( entity ); final Boolean isUnsavedAccordingToInterceptor = session.getInterceptor().isTransient( entity );
if ( isUnsaved != null ) { if ( isUnsavedAccordingToInterceptor != null ) {
return isUnsaved; return isUnsavedAccordingToInterceptor;
} }
// let the persister inspect the instance to decide // let the persister inspect the instance to decide
final EntityPersister persister = session.getEntityPersister( entityName, entity ); final EntityPersister persister = session.getEntityPersister( entityName, entity );
isUnsaved = persister.isTransient( entity, session ); final Boolean isUnsavedAccordingToPersister = persister.isTransient( entity, session );
if ( isUnsaved != null ) { if ( isUnsavedAccordingToPersister != null ) {
return isUnsaved; return isUnsavedAccordingToPersister;
} }
// we use the assumed value, if there is one, to avoid hitting // we use the assumed value, if there is one, to
// the database // avoid hitting the database
if ( assumed != null ) { if ( assumed != null ) {
return assumed; return assumed;
} }
// hit the database, after checking the session cache for a snapshot // hit the database, after checking the session
final Object[] snapshot = session.getPersistenceContextInternal().getDatabaseSnapshot( // cache for a snapshot
persister.getIdentifier( entity, session ), final Object[] snapshot =
persister session.getPersistenceContextInternal()
); .getDatabaseSnapshot( persister.getIdentifier( entity, session ), persister );
return snapshot == null; return snapshot == null;
} }
@ -334,20 +355,26 @@ public final class ForeignKeys {
return null; return null;
} }
else { else {
Object id = session.getContextEntityIdentifier( object ); final Object id = session.getContextEntityIdentifier( object );
if ( id == null ) { if ( id == null ) {
// context-entity-identifier returns null explicitly if the entity // context-entity-identifier returns null explicitly if the entity
// is not associated with the persistence context; so make some // is not associated with the persistence context; so make some
// deeper checks... // deeper checks...
if ( isTransient( entityName, object, Boolean.FALSE, session ) ) { throwIfTransient( entityName, object, session );
throw new TransientObjectException( return session.getEntityPersister( entityName, object ).getIdentifier( object, session );
"object references an unsaved transient instance - save the transient instance before flushing: " +
(entityName == null ? session.guessEntityName( object ) : entityName)
);
}
id = session.getEntityPersister( entityName, object ).getIdentifier( object, session );
} }
return id; else {
return id;
}
}
}
private static void throwIfTransient(String entityName, Object object, SharedSessionContractImplementor session) {
if ( isTransient( entityName, object, Boolean.FALSE, session ) ) {
throw new TransientObjectException(
"object references an unsaved transient instance - save the transient instance before flushing: " +
(entityName == null ? session.guessEntityName(object) : entityName)
);
} }
} }
@ -402,10 +429,9 @@ public final class ForeignKeys {
SharedSessionContractImplementor session, SharedSessionContractImplementor session,
NonNullableTransientDependencies nonNullableTransientEntities) { NonNullableTransientDependencies nonNullableTransientEntities) {
if ( value == null ) { if ( value == null ) {
return; // do nothing
} }
else if ( type.isEntityType() ) {
if ( type.isEntityType() ) {
final EntityType entityType = (EntityType) type; final EntityType entityType = (EntityType) type;
if ( !isNullable if ( !isNullable
&& !entityType.isOneToOne() && !entityType.isOneToOne()

View File

@ -22,7 +22,6 @@ import org.hibernate.event.spi.PersistContext;
import org.hibernate.event.spi.RefreshContext; import org.hibernate.event.spi.RefreshContext;
import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.type.CollectionType; import org.hibernate.type.CollectionType;
import org.hibernate.type.EntityType; import org.hibernate.type.EntityType;
import org.hibernate.type.Type; import org.hibernate.type.Type;