org.hibernate.engine.internal.ForeignKeys Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of hibernate-core Show documentation
Show all versions of hibernate-core Show documentation
Hibernate's core ORM functionality
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or .
*/
package org.hibernate.engine.internal;
import java.io.Serializable;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.TransientObjectException;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.SelfDirtinessTracker;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.type.CompositeType;
import org.hibernate.type.EntityType;
import org.hibernate.type.Type;
/**
* Algorithms related to foreign key constraint transparency
*
* @author Gavin King
*/
public final class ForeignKeys {
/**
* Delegate for handling nullifying ("null"ing-out) non-cascaded associations
*/
public static class Nullifier {
private final boolean isDelete;
private final boolean isEarlyInsert;
private final SharedSessionContractImplementor session;
private final Object self;
private final EntityPersister persister;
/**
* Constructs a Nullifier
*
* @param self The entity
* @param isDelete Are we in the middle of a delete action?
* @param isEarlyInsert Is this an early insert (INSERT generated id strategy)?
* @param session The session
* @param persister The EntityPersister for {@code self}
*/
public Nullifier(
final Object self,
final boolean isDelete,
final boolean isEarlyInsert,
final SharedSessionContractImplementor session,
final EntityPersister persister) {
this.isDelete = isDelete;
this.isEarlyInsert = isEarlyInsert;
this.session = session;
this.persister = persister;
this.self = self;
}
/**
* Nullify all references to entities that have not yet been inserted in the database, where the foreign key
* points toward that entity.
*
* @param values The entity attribute values
*/
public void nullifyTransientReferences(final Object[] values) {
final String[] propertyNames = persister.getPropertyNames();
final Type[] types = persister.getPropertyTypes();
for ( int i = 0; i < types.length; i++ ) {
values[i] = nullifyTransientReferences( values[i], propertyNames[i], types[i] );
}
}
/**
* Return null if the argument is an "unsaved" entity (ie. one with no existing database row), or the
* input argument otherwise. This is how Hibernate avoids foreign key constraint violations.
*
* @param value An entity attribute value
* @param propertyName An entity attribute name
* @param type An entity attribute type
*
* @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) {
final Object returnedValue;
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 actype = (CompositeType) type;
final Object[] subvalues = actype.getPropertyValues( value, session );
final Type[] subtypes = actype.getSubtypes();
final String[] subPropertyNames = actype.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 :(
actype.setPropertyValues( value, subvalues, EntityMode.POJO );
}
returnedValue = value;
}
else {
returnedValue = value;
}
// value != returnedValue if either:
// 1) returnedValue was nullified (set to null);
// or 2) returnedValue was initialized, but not nullified.
// When bytecode-enhancement is used for dirty-checking, the change should
// only be tracked when returnedValue was nullified (1)).
if ( value != returnedValue && returnedValue == null && SelfDirtinessTracker.class.isInstance( self ) ) {
( (SelfDirtinessTracker) self ).$$_hibernate_trackChange( propertyName );
}
return returnedValue;
}
private Object initializeIfNecessary(
final Object value,
final String propertyName,
final Type type) {
if ( isDelete &&
value == LazyPropertyInitializer.UNFETCHED_PROPERTY &&
type.isEntityType() &&
!session.getPersistenceContextInternal().isNullifiableEntityKeysEmpty() ) {
// IMPLEMENTATION NOTE: If cascade-remove was mapped for the attribute,
// then value should have been initialized previously, when the remove operation was
// cascaded to the property (because CascadingAction.DELETE.performOnLazyProperty()
// returns true). This particular situation can only arise when cascade-remove is not
// mapped for the association.
// There is at least one nullifiable entity. We don't know if the lazy
// associated entity is one of the nullifiable entities. If it is, and
// the property is not nullified, then a constraint violation will result.
// The only way to find out if the associated entity is nullifiable is
// to initialize it.
// 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 ( (LazyPropertyInitializer) persister ).initializeLazyProperty( propertyName, self, session );
}
else {
return value;
}
}
/**
* Determine if the object already exists in the database,
* using a "best guess"
*
* @param entityName The name of the entity
* @param object The entity instance
*/
private boolean isNullifiable(final String entityName, Object object)
throws HibernateException {
if ( object == LazyPropertyInitializer.UNFETCHED_PROPERTY ) {
// this is the best we can do...
return false;
}
if ( object instanceof HibernateProxy ) {
// if its an uninitialized proxy it can't be transient
final LazyInitializer li = ( (HibernateProxy) object ).getHibernateLazyInitializer();
if ( li.getImplementation( session ) == null ) {
return false;
// ie. we never have to null out a reference to
// an uninitialized proxy
}
else {
//unwrap it
object = li.getImplementation( session );
}
}
// if it was a reference to self, don't need to nullify
// unless we are using native id generation, in which
// case we definitely need to nullify
if ( object == self ) {
return isEarlyInsert
|| ( isDelete && session.getFactory().getDialect().hasSelfReferentialForeignKeyBug() );
}
// See if the entity is already bound to this session, if not look at the
// entity identifier and assume that the entity is persistent if the
// id is not "unsaved" (that is, we rely on foreign keys to keep
// database integrity)
final EntityEntry entityEntry = session.getPersistenceContextInternal().getEntry( object );
if ( entityEntry == null ) {
return isTransient( entityName, object, null, session );
}
else {
return entityEntry.isNullifiable( isEarlyInsert, session );
}
}
}
/**
* Is this instance persistent or detached?
*
* If assumed is non-null, don't hit the database to make the determination, instead assume that
* value; the client code must be prepared to "recover" in the case that this assumed result is incorrect.
*
* @param entityName The name of the entity
* @param entity The entity instance
* @param assumed The assumed return value, if avoiding database hit is desired
* @param session The session
*
* @return {@code true} if the given entity is not transient (meaning it is either detached/persistent)
*/
@SuppressWarnings("SimplifiableIfStatement")
public static boolean isNotTransient(String entityName, Object entity, Boolean assumed, SharedSessionContractImplementor session) {
if ( entity instanceof HibernateProxy ) {
return true;
}
if ( session.getPersistenceContextInternal().isEntryFor( entity ) ) {
return true;
}
// todo : shouldn't assumed be reversed here?
return !isTransient( entityName, entity, assumed, session );
}
/**
* Is this instance, which we know is not persistent, actually transient?
*
* If assumed is non-null, don't hit the database to make the determination, instead assume that
* value; the client code must be prepared to "recover" in the case that this assumed result is incorrect.
*
* @param entityName The name of the entity
* @param entity The entity instance
* @param assumed The assumed return value, if avoiding database hit is desired
* @param session The session
*
* @return {@code true} if the given entity is transient (unsaved)
*/
public static boolean isTransient(String entityName, Object entity, Boolean assumed, SharedSessionContractImplementor session) {
if ( entity == LazyPropertyInitializer.UNFETCHED_PROPERTY ) {
// an unfetched association can only point to
// an entity that already exists in the db
return false;
}
// let the interceptor inspect the instance to decide
Boolean isUnsaved = session.getInterceptor().isTransient( entity );
if ( isUnsaved != null ) {
return isUnsaved;
}
// let the persister inspect the instance to decide
final EntityPersister persister = session.getEntityPersister( entityName, entity );
isUnsaved = persister.isTransient( entity, session );
if ( isUnsaved != null ) {
return isUnsaved;
}
// we use the assumed value, if there is one, to avoid hitting
// the database
if ( assumed != null ) {
return assumed;
}
// hit the database, after checking the session cache for a snapshot
final Object[] snapshot = session.getPersistenceContextInternal().getDatabaseSnapshot(
persister.getIdentifier( entity, session ),
persister
);
return snapshot == null;
}
/**
* Return the identifier of the persistent or transient object, or throw
* an exception if the instance is "unsaved"
*
* Used by OneToOneType and ManyToOneType to determine what id value should
* be used for an object that may or may not be associated with the session.
* This does a "best guess" using any/all info available to use (not just the
* EntityEntry).
*
* @param entityName The name of the entity
* @param object The entity instance
* @param session The session
*
* @return The identifier
*
* @throws TransientObjectException if the entity is transient (does not yet have an identifier)
*/
public static Serializable getEntityIdentifierIfNotUnsaved(
final String entityName,
final Object object,
final SharedSessionContractImplementor session) throws TransientObjectException {
if ( object == null ) {
return null;
}
else {
Serializable id = session.getContextEntityIdentifier( object );
if ( id == null ) {
// context-entity-identifier returns null explicitly if the entity
// is not associated with the persistence context; so make some
// deeper checks...
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)
);
}
id = session.getEntityPersister( entityName, object ).getIdentifier( object, session );
}
return id;
}
}
/**
* Find all non-nullable references to entities that have not yet
* been inserted in the database, where the foreign key
* is a reference to an unsaved transient entity. .
*
* @param entityName - the entity name
* @param entity - the entity instance
* @param values - insertable properties of the object (including backrefs),
* possibly with substitutions
* @param isEarlyInsert - true if the entity needs to be executed as soon as possible
* (e.g., to generate an ID)
* @param session - the session
*
* @return the transient unsaved entity dependencies that are non-nullable,
* or null if there are none.
*/
public static NonNullableTransientDependencies findNonNullableTransientEntities(
String entityName,
Object entity,
Object[] values,
boolean isEarlyInsert,
SharedSessionContractImplementor session) {
final EntityPersister persister = session.getEntityPersister( entityName, entity );
final Nullifier nullifier = new Nullifier( entity, false, isEarlyInsert, session, persister );
final String[] propertyNames = persister.getPropertyNames();
final Type[] types = persister.getPropertyTypes();
final boolean[] nullability = persister.getPropertyNullability();
final NonNullableTransientDependencies nonNullableTransientEntities = new NonNullableTransientDependencies();
for ( int i = 0; i < types.length; i++ ) {
collectNonNullableTransientEntities(
nullifier,
values[i],
propertyNames[i],
types[i],
nullability[i],
session,
nonNullableTransientEntities
);
}
return nonNullableTransientEntities.isEmpty() ? null : nonNullableTransientEntities;
}
private static void collectNonNullableTransientEntities(
Nullifier nullifier,
Object value,
String propertyName,
Type type,
boolean isNullable,
SharedSessionContractImplementor session,
NonNullableTransientDependencies nonNullableTransientEntities) {
if ( value == null ) {
return;
}
if ( type.isEntityType() ) {
final EntityType entityType = (EntityType) type;
if ( !isNullable
&& !entityType.isOneToOne()
&& nullifier.isNullifiable( entityType.getAssociatedEntityName(), value ) ) {
nonNullableTransientEntities.add( propertyName, value );
}
}
else if ( type.isAnyType() ) {
if ( !isNullable && nullifier.isNullifiable( null, value ) ) {
nonNullableTransientEntities.add( propertyName, value );
}
}
else if ( type.isComponentType() ) {
final CompositeType actype = (CompositeType) type;
final boolean[] subValueNullability = actype.getPropertyNullability();
if ( subValueNullability != null ) {
final String[] subPropertyNames = actype.getPropertyNames();
final Object[] subvalues = actype.getPropertyValues( value, session );
final Type[] subtypes = actype.getSubtypes();
for ( int j = 0; j < subvalues.length; j++ ) {
collectNonNullableTransientEntities(
nullifier,
subvalues[j],
subPropertyNames[j],
subtypes[j],
subValueNullability[j],
session,
nonNullableTransientEntities
);
}
}
}
}
/**
* Disallow instantiation
*/
private ForeignKeys() {
}
}