HHH-16759 When ComponentType is immutable, use instantiator instead of setting property values

This commit is contained in:
Christian Beikov 2023-07-26 16:37:26 +02:00
parent 130e05755a
commit 10baf4398a
14 changed files with 296 additions and 118 deletions

View File

@ -48,6 +48,7 @@ import org.hibernate.boot.model.internal.CreateKeySecondPass;
import org.hibernate.boot.model.internal.FkSecondPass;
import org.hibernate.boot.model.internal.IdGeneratorResolverSecondPass;
import org.hibernate.boot.model.internal.JPAIndexHolder;
import org.hibernate.boot.model.internal.OptionalDeterminationSecondPass;
import org.hibernate.boot.model.internal.QuerySecondPass;
import org.hibernate.boot.model.internal.SecondaryTableFromAnnotationSecondPass;
import org.hibernate.boot.model.internal.SecondaryTableSecondPass;
@ -1667,6 +1668,7 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
private ArrayList<ImplicitColumnNamingSecondPass> implicitColumnNamingSecondPassList;
private ArrayList<SecondPass> generalSecondPassList;
private ArrayList<OptionalDeterminationSecondPass> optionalDeterminationSecondPassList;
@Override
public void addSecondPass(SecondPass secondPass) {
@ -1705,6 +1707,9 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
else if ( secondPass instanceof ImplicitColumnNamingSecondPass ) {
addImplicitColumnNamingSecondPass( (ImplicitColumnNamingSecondPass) secondPass );
}
else if ( secondPass instanceof OptionalDeterminationSecondPass ) {
addOptionalDeterminationSecondPass( (OptionalDeterminationSecondPass) secondPass );
}
else {
// add to the general SecondPass list
if ( generalSecondPassList == null ) {
@ -1786,6 +1791,13 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
implicitColumnNamingSecondPassList.add( secondPass );
}
private void addOptionalDeterminationSecondPass(OptionalDeterminationSecondPass secondPass) {
if ( optionalDeterminationSecondPassList == null ) {
optionalDeterminationSecondPassList = new ArrayList<>();
}
optionalDeterminationSecondPassList.add( secondPass );
}
private boolean inSecondPass = false;
@ -1812,6 +1824,7 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector,
processSecondPasses( querySecondPassList );
processSecondPasses( generalSecondPassList );
processSecondPasses( optionalDeterminationSecondPassList );
processPropertyReferences();

View File

@ -119,6 +119,7 @@ public class AnyBinder {
}
binder.setAccessType( inferredData.getDefaultAccess() );
binder.setCascade( cascadeStrategy );
binder.setBuildingContext( context );
Property prop = binder.makeProperty();
//composite FK columns are in the same table, so it's OK
propertyHolder.addProperty( prop, columns, inferredData.getDeclaringClass() );

View File

@ -1276,6 +1276,7 @@ public abstract class CollectionBinder {
binder.setProperty( property );
binder.setInsertable( insertable );
binder.setUpdatable( updatable );
binder.setBuildingContext( buildingContext );
Property prop = binder.makeProperty();
//we don't care about the join stuffs because the column is on the association table.
if ( !declaringClassSet ) {

View File

@ -192,7 +192,7 @@ public class EmbeddableBinder {
entityBinder,
isComponentEmbedded,
isIdentifierMapper,
false,
context.getMetadataCollector().isInSecondPass(),
customInstantiatorImpl,
compositeUserTypeClass,
annotatedColumns,

View File

@ -112,6 +112,7 @@ public class OneToOneSecondPass implements SecondPass {
binder.setValue( value );
binder.setCascade( cascadeStrategy );
binder.setAccessType( inferredData.getDefaultAccess() );
binder.setBuildingContext( buildingContext );
final LazyGroup lazyGroupAnnotation = property.getAnnotation( LazyGroup.class );
if ( lazyGroupAnnotation != null ) {
@ -186,6 +187,7 @@ public class OneToOneSecondPass implements SecondPass {
manyToOne.setFetchMode( oneToOne.getFetchMode() );
manyToOne.setLazy( oneToOne.isLazy() );
manyToOne.setReferencedEntityName( oneToOne.getReferencedEntityName() );
manyToOne.setReferencedPropertyName( mappedBy );
manyToOne.setUnwrapProxy( oneToOne.isUnwrapProxy() );
manyToOne.markAsLogicalOneToOne();
property.setValue( manyToOne );

View File

@ -0,0 +1,13 @@
/*
* 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 http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.boot.model.internal;
import org.hibernate.boot.spi.SecondPass;
public interface OptionalDeterminationSecondPass extends SecondPass {
}

View File

@ -48,13 +48,16 @@ import org.hibernate.boot.model.IdentifierGeneratorDefinition;
import org.hibernate.boot.spi.AccessType;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.boot.spi.PropertyData;
import org.hibernate.boot.spi.SecondPass;
import org.hibernate.engine.OptimisticLockStyle;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.mapping.Collection;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.GeneratorCreator;
import org.hibernate.mapping.Join;
import org.hibernate.mapping.KeyValue;
import org.hibernate.mapping.MappedSuperclass;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.RootClass;
import org.hibernate.mapping.SimpleValue;
@ -448,13 +451,32 @@ public class PropertyBinder {
private void handleOptional(Property property) {
if ( this.property != null ) {
property.setOptional( !isId && isOptional( this.property ) && isNullable( property ) );
}
}
property.setOptional( !isId && isOptional( this.property ) );
if ( property.isOptional() ) {
final OptionalDeterminationSecondPass secondPass = persistentClasses -> {
// Defer determining whether a property and its columns are nullable,
// as handleOptional might be called when the value is not yet fully initialized
if ( property.getPersistentClass() != null ) {
for ( Join join : property.getPersistentClass().getJoins() ) {
if ( join.getProperties().contains( property ) ) {
// If this property is part of a join it is inherently optional
return;
}
}
}
private static boolean isNullable(Property property) {
final Value value = property.getValue();
return value instanceof org.hibernate.mapping.OneToMany || value.isNullable();
if ( !property.getValue().isNullable() ) {
property.setOptional( false );
}
};
// Always register this as second pass and never execute it directly,
// even if we are in a second pass already.
// If we are in a second pass, then we are currently processing the generalSecondPassList
// to which the following call will add the second pass to,
// so it will be executed within that second pass, just a bit later
buildingContext.getMetadataCollector().addSecondPass( secondPass );
}
}
}
private void handleNaturalId(Property property) {

View File

@ -143,4 +143,9 @@ public class ManyToOne extends ToOne {
public boolean isLogicalOneToOne() {
return isLogicalOneToOne;
}
@Override
public boolean isNullable() {
return getReferencedPropertyName() != null || super.isNullable();
}
}

View File

@ -9,11 +9,13 @@ package org.hibernate.metamodel.mapping.internal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.hibernate.AssertionFailure;
import org.hibernate.LockMode;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.cache.MutableCacheKeyBuilder;
@ -77,6 +79,7 @@ import org.hibernate.sql.ast.tree.from.TableGroupJoin;
import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer;
import org.hibernate.sql.ast.tree.from.TableGroupProducer;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.ast.tree.from.TableReferenceJoin;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
@ -145,6 +148,7 @@ public class ToOneAttributeMapping
private final Set<String> targetKeyPropertyNames;
private final Cardinality cardinality;
private final boolean hasJoinTable;
/*
Capture the other side's name of a possibly bidirectional association to allow resolving circular fetches.
It may be null if the referenced property is a non-entity.
@ -171,6 +175,7 @@ public class ToOneAttributeMapping
referencedPropertyName = original.referencedPropertyName;
targetKeyPropertyName = original.targetKeyPropertyName;
cardinality = original.cardinality;
hasJoinTable = original.hasJoinTable;
bidirectionalAttributePath = original.bidirectionalAttributePath;
declaringTableGroupProducer = original.declaringTableGroupProducer;
isKeyTableNullable = original.isKeyTableNullable;
@ -242,7 +247,10 @@ public class ToOneAttributeMapping
this.entityMappingType = entityMappingType;
this.navigableRole = navigableRole;
this.declaringTableGroupProducer = resolveDeclaringTableGroupProducer( declaringEntityPersister, navigableRole );
this.declaringTableGroupProducer = resolveDeclaringTableGroupProducer(
declaringEntityPersister,
navigableRole
);
if ( bootValue instanceof ManyToOne ) {
final ManyToOne manyToOne = (ManyToOne) bootValue;
this.notFoundAction = ( (ManyToOne) bootValue ).getNotFoundAction();
@ -260,6 +268,7 @@ public class ToOneAttributeMapping
? name
: bootValue.getPropertyName();
if ( cardinality == Cardinality.LOGICAL_ONE_TO_ONE ) {
boolean hasJoinTable = false;
// Handle join table cases
for ( Join join : entityBinding.getJoinClosure() ) {
if ( join.getPersistentClass().getEntityName().equals( entityBinding.getEntityName() )
@ -267,7 +276,10 @@ public class ToOneAttributeMapping
&& join.getTable() == manyToOne.getTable()
&& equal( join.getKey(), manyToOne ) ) {
//noinspection deprecation
bidirectionalAttributeName = SelectablePath.parse( join.getPropertyIterator().next().getName() );
bidirectionalAttributeName = SelectablePath.parse(
join.getPropertyIterator().next().getName()
);
hasJoinTable = true;
break;
}
}
@ -280,8 +292,10 @@ public class ToOneAttributeMapping
entityBinding.getPropertyClosure()
);
}
this.hasJoinTable = hasJoinTable;
}
else {
this.hasJoinTable = false;
bidirectionalAttributeName = findBidirectionalOneToManyAttributeName(
propertyPath,
declaringType,
@ -294,7 +308,12 @@ public class ToOneAttributeMapping
else {
// Only set the bidirectional attribute name if the referenced property can actually be circular i.e. an entity type
final Property property = entityBinding.getProperty( referencedPropertyName );
this.bidirectionalAttributePath = property != null && property.getValue() instanceof EntityType
this.hasJoinTable = cardinality == Cardinality.LOGICAL_ONE_TO_ONE
&& property != null
&& ( property.getValue() instanceof ManyToOne )
&& ( (ManyToOne) property.getValue() ).isLogicalOneToOne();
this.bidirectionalAttributePath = property != null && property.getValue()
.getType() instanceof EntityType
? SelectablePath.parse( referencedPropertyName )
: null;
}
@ -302,12 +321,20 @@ public class ToOneAttributeMapping
isKeyTableNullable = true;
}
else {
final String targetTableName = MappingModelCreationHelper.getTableIdentifierExpression( manyToOne.getTable(), declaringEntityPersister.getFactory() );
final String targetTableName = MappingModelCreationHelper.getTableIdentifierExpression(
manyToOne.getTable(),
declaringEntityPersister.getFactory()
);
if ( CollectionPart.Nature.fromNameExact( navigableRole.getParent().getLocalName() ) != null ) {
// * the to-one's parent is directly a collection element or index
// * therefore, its parent-parent should be the collection itself
final PluralAttributeMapping pluralAttribute = (PluralAttributeMapping) declaringEntityPersister.findByPath(
navigableRole.getParent().getParent().getFullPath().substring( declaringEntityPersister.getNavigableRole().getFullPath().length() + 1 ) );
navigableRole.getParent()
.getParent()
.getFullPath()
.substring( declaringEntityPersister.getNavigableRole()
.getFullPath()
.length() + 1 ) );
assert pluralAttribute != null;
final AbstractCollectionPersister persister = (AbstractCollectionPersister) pluralAttribute.getCollectionDescriptor();
@ -328,6 +355,7 @@ public class ToOneAttributeMapping
else {
assert bootValue instanceof OneToOne;
cardinality = Cardinality.ONE_TO_ONE;
hasJoinTable = false;
/*
The otherSidePropertyName value is used to determine bidirectionality based on the navigablePath string
@ -381,7 +409,7 @@ public class ToOneAttributeMapping
}
notFoundAction = null;
isKeyTableNullable = isNullable();
isOptional = ! bootValue.isConstrained();
isOptional = !bootValue.isConstrained();
isInternalLoadNullable = isNullable();
}
@ -436,67 +464,78 @@ public class ToOneAttributeMapping
}
this.targetKeyPropertyNames = targetKeyPropertyNames;
}
else if ( bootValue.isReferenceToPrimaryKey() ) {
this.targetKeyPropertyName = referencedPropertyName;
final Set<String> targetKeyPropertyNames = new HashSet<>( 2 );
addPrefixedPropertyNames(
targetKeyPropertyNames,
targetKeyPropertyName,
bootValue.getType(),
declaringEntityPersister.getFactory()
);
this.targetKeyPropertyNames = targetKeyPropertyNames;
}
else {
final PersistentClass entityBinding = bootValue.getBuildingContext().getMetadataCollector()
.getEntityBinding( entityMappingType.getEntityName() );
final Type propertyType = entityBinding.getRecursiveProperty( referencedPropertyName ).getType();
final CompositeType compositeType;
if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded()
&& compositeType.getPropertyNames().length == 1 ) {
final Set<String> targetKeyPropertyNames = new HashSet<>( 2 );
this.targetKeyPropertyName = compositeType.getPropertyNames()[0];
addPrefixedPropertyPaths(
if ( bootValue.isReferenceToPrimaryKey() ) {
this.targetKeyPropertyName = referencedPropertyName;
final Set<String> targetKeyPropertyNames = new HashSet<>( 3 );
addPrefixedPropertyNames(
targetKeyPropertyNames,
targetKeyPropertyName,
compositeType.getSubtypes()[0],
propertyType,
declaringEntityPersister.getFactory()
);
addPrefixedPropertyNames(
targetKeyPropertyNames,
EntityIdentifierMapping.ROLE_LOCAL_NAME,
propertyType,
null,
bootValue.getType(),
declaringEntityPersister.getFactory()
);
this.targetKeyPropertyNames = targetKeyPropertyNames;
}
else {
final Set<String> targetKeyPropertyNames = new HashSet<>( 2 );
this.targetKeyPropertyName = referencedPropertyName;
final String mapsIdAttributeName;
// If there is a "virtual property" for a non-PK join mapping, we try to see if the columns match the
// primary key columns and if so, we add the primary key property name as target key property
if ( ( mapsIdAttributeName = findMapsIdPropertyName( entityMappingType, referencedPropertyName ) ) != null ) {
final CompositeType compositeType;
if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded()
&& compositeType.getPropertyNames().length == 1 ) {
final Set<String> targetKeyPropertyNames = new HashSet<>( 2 );
this.targetKeyPropertyName = compositeType.getPropertyNames()[0];
addPrefixedPropertyPaths(
targetKeyPropertyNames,
mapsIdAttributeName,
entityMappingType.getEntityPersister().getIdentifierType(),
targetKeyPropertyName,
compositeType.getSubtypes()[0],
declaringEntityPersister.getFactory()
);
addPrefixedPropertyNames(
targetKeyPropertyNames,
EntityIdentifierMapping.ROLE_LOCAL_NAME,
propertyType,
declaringEntityPersister.getFactory()
);
this.targetKeyPropertyNames = targetKeyPropertyNames;
}
else {
final Set<String> targetKeyPropertyNames = new HashSet<>( 2 );
this.targetKeyPropertyName = referencedPropertyName;
final String mapsIdAttributeName;
// If there is a "virtual property" for a non-PK join mapping, we try to see if the columns match the
// primary key columns and if so, we add the primary key property name as target key property
if ( ( mapsIdAttributeName = findMapsIdPropertyName(
entityMappingType,
referencedPropertyName
) ) != null ) {
addPrefixedPropertyPaths(
targetKeyPropertyNames,
mapsIdAttributeName,
entityMappingType.getEntityPersister().getIdentifierType(),
declaringEntityPersister.getFactory()
);
}
addPrefixedPropertyNames(
targetKeyPropertyNames,
targetKeyPropertyName,
propertyType,
declaringEntityPersister.getFactory()
);
addPrefixedPropertyNames(
targetKeyPropertyNames,
ForeignKeyDescriptor.PART_NAME,
propertyType,
declaringEntityPersister.getFactory()
);
this.targetKeyPropertyNames = targetKeyPropertyNames;
}
addPrefixedPropertyNames(
targetKeyPropertyNames,
targetKeyPropertyName,
propertyType,
declaringEntityPersister.getFactory()
);
addPrefixedPropertyNames(
targetKeyPropertyNames,
ForeignKeyDescriptor.PART_NAME,
propertyType,
declaringEntityPersister.getFactory()
);
this.targetKeyPropertyNames = targetKeyPropertyNames;
}
}
}
@ -629,6 +668,7 @@ public class ToOneAttributeMapping
this.targetKeyPropertyName = original.targetKeyPropertyName;
this.targetKeyPropertyNames = original.targetKeyPropertyNames;
this.cardinality = original.cardinality;
this.hasJoinTable = original.hasJoinTable;
this.bidirectionalAttributePath = original.bidirectionalAttributePath;
this.declaringTableGroupProducer = declaringTableGroupProducer;
this.isInternalLoadNullable = original.isInternalLoadNullable;
@ -729,7 +769,7 @@ public class ToOneAttributeMapping
final String newFkPrefix;
if ( prefix == null ) {
newPrefix = propertyName;
newPkPrefix = propertyName + "." + EntityIdentifierMapping.ROLE_LOCAL_NAME;
newPkPrefix = EntityIdentifierMapping.ROLE_LOCAL_NAME;
newFkPrefix = ForeignKeyDescriptor.PART_NAME;
}
else if ( propertyName == null ) {
@ -891,9 +931,8 @@ public class ToOneAttributeMapping
FetchTiming fetchTiming,
DomainResultCreationState creationState) {
final AssociationKey associationKey = foreignKeyDescriptor.getAssociationKey();
if ( creationState.isAssociationKeyVisited( associationKey )
|| bidirectionalAttributePath != null && !creationState.isRegisteringVisitedAssociationKeys() ) {
final boolean associationKeyVisited = creationState.isAssociationKeyVisited( associationKey );
if ( associationKeyVisited || bidirectionalAttributePath != null ) {
NavigablePath parentNavigablePath = fetchablePath.getParent();
assert parentNavigablePath.equals( fetchParent.getNavigablePath() );
// The parent navigable path is {fk} if we are creating the domain result for the foreign key for a circular fetch
@ -952,6 +991,12 @@ public class ToOneAttributeMapping
);
}
if ( !associationKeyVisited && creationState.isRegisteringVisitedAssociationKeys() ) {
// If the current association key hasn't been visited yet and we are registering keys,
// then there can't be a circular fetch
return null;
}
/*
class Child {
@OneToOne
@ -2069,8 +2114,34 @@ public class ToOneAttributeMapping
private void initializeIfNeeded(TableGroup lhs, SqlAstJoinType sqlAstJoinType, TableGroup tableGroup) {
if ( sqlAstJoinType == SqlAstJoinType.INNER && ( isNullable || !lhs.canUseInnerJoins() ) ) {
// Force initialization of the underlying table group join to retain cardinality
tableGroup.getPrimaryTableReference();
if ( hasJoinTable ) {
// Set the join type of the table reference join to INNER to retain cardinality expectation
final TableReference lhsTableReference = lhs.resolveTableReference(
tableGroup.getNavigablePath(),
identifyingColumnsTableExpression
);
final List<TableReferenceJoin> tableReferenceJoins = lhs.getTableReferenceJoins();
for ( int i = 0; i < tableReferenceJoins.size(); i++ ) {
final TableReferenceJoin tableReferenceJoin = tableReferenceJoins.get( i );
if ( tableReferenceJoin.getJoinType() != SqlAstJoinType.INNER
&& tableReferenceJoin.getJoinedTableReference() == lhsTableReference ) {
tableReferenceJoins.set(
i,
new TableReferenceJoin(
true,
tableReferenceJoin.getJoinedTableReference(),
tableReferenceJoin.getPredicate()
)
);
return;
}
}
throw new AssertionFailure( "Couldn't find table reference join for join table: " + lhsTableReference );
}
else {
// Force initialization of the underlying table group join to retain cardinality
tableGroup.getPrimaryTableReference();
}
}
}

View File

@ -55,7 +55,7 @@ public class TableReferenceJoin implements TableJoin, PredicateContainer {
@Override
public String toString() {
return getJoinType().getText() + " join " + getJoinedTableReference().toString();
return getJoinType().getText() + "join " + getJoinedTableReference().toString();
}
@Override

View File

@ -31,6 +31,7 @@ import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.Property;
import org.hibernate.metamodel.mapping.EmbeddableMappingType;
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess;
@ -39,6 +40,7 @@ import org.hibernate.property.access.spi.PropertyAccess;
import org.hibernate.query.sqm.SqmExpressible;
import org.hibernate.resource.beans.internal.FallbackBeanInstanceProducer;
import org.hibernate.resource.beans.spi.ManagedBeanRegistry;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.spi.CompositeTypeImplementor;
import org.hibernate.usertype.CompositeUserType;
@ -507,10 +509,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
values[i] = propertyTypes[i].deepCopy( values[i], factory );
}
final EmbeddableInstantiator instantiator = mappingModelPart.getEmbeddableTypeDescriptor()
.getRepresentationStrategy()
.getInstantiator();
Object result = instantiator.instantiate( () -> values, factory );
Object result = instantiator().instantiate( () -> values, factory );
//not absolutely necessary, but helps for some
//equals()/hashCode() implementations
@ -530,10 +529,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
Object owner,
Map<Object, Object> copyCache) {
if ( !isMutable() ) {
return original;
}
if ( original == null ) {
if ( original == null && target == null ) {
return null;
}
if ( compositeUserType != null ) {
@ -542,15 +538,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
//if ( original == target ) return target;
final Object[] originalValues = getPropertyValues( original );
final Object[] resultValues;
if ( target == null ) {
resultValues = new Object[originalValues.length];
}
else {
resultValues = getPropertyValues( target );
}
final Object[] resultValues = getPropertyValues( target );
final Object[] replacedValues = TypeHelper.replace(
originalValues,
resultValues,
@ -560,11 +548,8 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
copyCache
);
if ( target == null ) {
final EmbeddableInstantiator instantiator = mappingModelPart.getEmbeddableTypeDescriptor()
.getRepresentationStrategy()
.getInstantiator();
return instantiator.instantiate( () -> replacedValues, session.getSessionFactory() );
if ( target == null || !isMutable() ) {
return instantiator().instantiate( () -> replacedValues, session.getSessionFactory() );
}
else {
setPropertyValues( target, replacedValues );
@ -581,10 +566,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
Map<Object, Object> copyCache,
ForeignKeyDirection foreignKeyDirection) {
if ( !isMutable() ) {
return original;
}
if ( original == null ) {
if ( original == null && target == null ) {
return null;
}
if ( compositeUserType != null ) {
@ -594,15 +576,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
final Object[] originalValues = getPropertyValues( original );
final Object[] resultValues;
if ( target == null ) {
resultValues = new Object[originalValues.length];
}
else {
resultValues = getPropertyValues( target );
}
final Object[] resultValues = getPropertyValues( target );
final Object[] replacedValues = TypeHelper.replace(
originalValues,
resultValues,
@ -613,11 +587,8 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
foreignKeyDirection
);
if ( target == null ) {
final EmbeddableInstantiator instantiator = mappingModelPart.getEmbeddableTypeDescriptor()
.getRepresentationStrategy()
.getInstantiator();
return instantiator.instantiate( () -> replacedValues, session.getSessionFactory() );
if ( target == null || !isMutable() ) {
return instantiator().instantiate( () -> replacedValues, session.getSessionFactory() );
}
else {
setPropertyValues( target, replacedValues );
@ -688,10 +659,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
assembled[i] = propertyTypes[i].assemble( (Serializable) values[i], session, owner );
}
final EmbeddableInstantiator instantiator = mappingModelPart.getEmbeddableTypeDescriptor()
.getRepresentationStrategy()
.getInstantiator();
final Object instance = instantiator.instantiate( () -> assembled, session.getFactory() );
final Object instance = instantiator().instantiate( () -> assembled, session.getFactory() );
final PropertyAccess parentInjectionAccess = mappingModelPart.getParentInjectionAttributePropertyAccess();
if ( parentInjectionAccess != null ) {
@ -830,10 +798,15 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen
}
private Object resolve(Object[] value, SharedSessionContractImplementor session) throws HibernateException {
final EmbeddableInstantiator instantiator = mappingModelPart.getEmbeddableTypeDescriptor()
.getRepresentationStrategy()
.getInstantiator();
return instantiator.instantiate( () -> value, session.getFactory() );
return instantiator().instantiate( () -> value, session.getFactory() );
}
private EmbeddableMappingType embeddableTypeDescriptor() {
return mappingModelPart.getEmbeddableTypeDescriptor();
}
protected final EmbeddableInstantiator instantiator() {
return embeddableTypeDescriptor().getRepresentationStrategy().getInstantiator();
}
@Override

View File

@ -213,7 +213,7 @@ public class TypeHelper {
);
if ( target[i] != null && compositeType instanceof ComponentType ) {
final ComponentType componentType = (ComponentType) compositeType;
if ( componentType.isCompositeUserType() ) {
if ( !componentType.isMutable() || componentType.isCompositeUserType() ) {
final EmbeddableInstantiator instantiator = ( (ComponentType) compositeType ).getMappingModelPart()
.getEmbeddableTypeDescriptor()
.getRepresentationStrategy()

View File

@ -177,7 +177,7 @@ public class EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest {
.setParameter( "id", 1 )
.getSingleResult();
statementInspector.assertExecutedCount( 2 );
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 2 );
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 1 );
statementInspector.assertNumberOfOccurrenceInQuery( 1, "join", 3 );
Male son = parent.getSon();
assertThat( son, CoreMatchers.notNullValue() );

View File

@ -4,14 +4,17 @@ import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
@DomainModel(annotatedClasses = {
MergeRecordPropertyTestCase.MyEntity.class
@ -20,6 +23,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@JiraKey("HHH-16759")
public class MergeRecordPropertyTestCase {
@AfterEach
protected void cleanupTest(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery( "update MyEntity e set e.record.assoc = null" ).executeUpdate();
session.createQuery( "delete from MyEntity" ).executeUpdate();
}
);
}
@Test
public void mergeDetached(SessionFactoryScope scope) {
scope.inTransaction(
@ -38,24 +51,53 @@ public class MergeRecordPropertyTestCase {
}
@Test
public void mergeTransient(SessionFactoryScope scope) {
public void mergeDetachedNullComponent(SessionFactoryScope scope) {
scope.inTransaction(
session -> session.merge( new MyEntity( 2L, new MyRecord( "test", "xyz" ) ) )
session -> session.persist( new MyEntity( 1L, new MyRecord( "test", "abc" ) ) )
);
scope.inTransaction(
session -> session.merge( new MyEntity( 1L ) )
);
scope.inSession(
session -> {
final MyEntity entity = session.find( MyEntity.class, 2L );
final MyEntity entity = session.find( MyEntity.class, 1L );
assertNull( entity.record );
}
);
}
@Test
public void mergeTransient(SessionFactoryScope scope) {
scope.inTransaction(
session -> session.merge( new MyEntity( 1L, new MyRecord( "test", "xyz" ) ) )
);
scope.inSession(
session -> {
final MyEntity entity = session.find( MyEntity.class, 1L );
assertEquals( "test", entity.record.name );
assertEquals( "xyz", entity.record.description );
}
);
}
@Test
public void mergeTransientNullComponent(SessionFactoryScope scope) {
scope.inTransaction(
session -> session.merge( new MyEntity( 1L ) )
);
scope.inSession(
session -> {
final MyEntity entity = session.find( MyEntity.class, 1L );
assertNull( entity.record );
}
);
}
@Test
public void mergePersistent(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
final MyEntity entity = new MyEntity( 3L, new MyRecord( "test", "efg" ) );
final MyEntity entity = new MyEntity( 1L, new MyRecord( "test", "efg" ) );
session.persist( entity );
entity.setRecord( new MyRecord( "test", "h" ) );
session.merge( entity );
@ -63,13 +105,41 @@ public class MergeRecordPropertyTestCase {
);
scope.inSession(
session -> {
final MyEntity entity = session.find( MyEntity.class, 3L );
final MyEntity entity = session.find( MyEntity.class, 1L );
assertEquals( "test", entity.record.name );
assertEquals( "h", entity.record.description );
}
);
}
@Test
public void mergePersistentDetached(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
final MyEntity entity = new MyEntity( 1L, new MyRecord( "test", "abc" ) );
session.persist( entity );
session.flush();
session.clear();
final MyEntity entity2 = new MyEntity( 2L, new MyRecord( "test", "efg", new MyEntity( 1L ) ) );
final MyEntity mergedEntity = session.merge( entity2 );
assertTrue( session.contains( mergedEntity.record.assoc() ) );
}
);
}
@Test
public void mergePersistentManaged(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
final MyEntity entity = new MyEntity( 1L, new MyRecord( "test", "abc" ) );
session.persist( entity );
final MyEntity entity2 = new MyEntity( 1L, new MyRecord( "test", "efg", new MyEntity( 1L ) ) );
final MyEntity mergedEntity = session.merge( entity2 );
assertTrue( session.contains( mergedEntity.record.assoc() ) );
}
);
}
@Entity(name = "MyEntity")
public static class MyEntity {
@Id
@ -80,6 +150,10 @@ public class MergeRecordPropertyTestCase {
public MyEntity() {
}
public MyEntity(Long id) {
this.id = id;
}
public MyEntity(Long id, MyRecord record) {
this.id = id;
this.record = record;
@ -103,6 +177,9 @@ public class MergeRecordPropertyTestCase {
}
@Embeddable
public static record MyRecord(String name, String description) {
public static record MyRecord(String name, String description, @ManyToOne(fetch = FetchType.LAZY) MyEntity assoc) {
public MyRecord(String name, String description) {
this( name, description, null );
}
}
}