diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MapAttributeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MapAttributeImpl.java index 0d401cdfb9a7..fed66dfe2ae7 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MapAttributeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MapAttributeImpl.java @@ -8,9 +8,12 @@ import java.util.Map; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.mapping.Property; import org.hibernate.metamodel.model.creation.spi.RuntimeModelCreationContext; import org.hibernate.metamodel.model.domain.spi.AbstractPluralPersistentAttribute; +import org.hibernate.metamodel.model.domain.spi.CollectionElement; +import org.hibernate.metamodel.model.domain.spi.CollectionIndex; import org.hibernate.metamodel.model.domain.spi.MapPersistentAttribute; import org.hibernate.metamodel.model.domain.spi.PersistentCollectionDescriptor; import org.hibernate.metamodel.model.domain.spi.SimpleTypeDescriptor; @@ -45,4 +48,25 @@ public Class getKeyJavaType() { public SimpleTypeDescriptor getKeyType() { return (SimpleTypeDescriptor) getPersistentCollectionDescriptor().getKeyDomainTypeDescriptor(); } + + @Override + @SuppressWarnings("unchecked") + protected Map replaceElements( + Map originalValue, + Map targetValue, + Object owner, + Map copyCache, + SessionImplementor session) { + targetValue.clear(); + + final CollectionIndex indexDescriptor = getCollectionDescriptor().getIndexDescriptor(); + final CollectionElement elementDescriptor = getCollectionDescriptor().getElementDescriptor(); + for ( Map.Entry entry : originalValue.entrySet() ) { + K key = indexDescriptor.replace( entry.getKey(), null, owner, copyCache, session ); + V value = elementDescriptor.replace( entry.getValue(), null, owner, copyCache, session ); + targetValue.put( key, value ); + } + + return targetValue; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/AbstractPluralPersistentAttribute.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/AbstractPluralPersistentAttribute.java index 1074164bc0d4..6b08c420925f 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/AbstractPluralPersistentAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/AbstractPluralPersistentAttribute.java @@ -6,23 +6,36 @@ */ package org.hibernate.metamodel.model.domain.spi; +import java.io.Serializable; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.function.BiConsumer; import java.util.function.Consumer; +import org.hibernate.Hibernate; import org.hibernate.LockMode; import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.collection.internal.AbstractPersistentCollection; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.FetchStrategy; import org.hibernate.engine.FetchTiming; import org.hibernate.engine.internal.ForeignKeys; import org.hibernate.engine.internal.NonNullableTransientDependencies; import org.hibernate.engine.spi.CascadeStyle; +import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.engine.spi.CollectionKey; import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.MarkerObject; +import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.mapping.Collection; import org.hibernate.mapping.Property; import org.hibernate.metamodel.model.creation.spi.RuntimeModelCreationContext; @@ -455,4 +468,158 @@ public DomainType getAttributeType() { public ForeignKeyDirection getForeignKeyDirection() { return getPersistentCollectionDescriptor().getForeignKeyDirection(); } + + @Override + public Object replace(C originalValue, C targetValue, Object owner, Map copyCache, SessionImplementor session) { + if ( originalValue == null ) { + return null; + } + + if ( !Hibernate.isInitialized( originalValue ) ) { + final AbstractPersistentCollection collection = (AbstractPersistentCollection) originalValue; + if ( collection.hasQueuedOperations() ) { + collection.replaceQueuedOperationValues( getCollectionDescriptor(), copyCache ); + } + return targetValue; + } + + // For a null target or a target which is the same as the original, + // we need to put the merged elements into a new collection. + C result = ( targetValue == null || + targetValue == originalValue || + targetValue == LazyPropertyInitializer.UNFETCHED_PROPERTY ) ? + instantiateResult( originalValue ) : targetValue; + + // For arrays, replaceElements() may return a different reference, + // since the array length might not match. + result = replaceElements( originalValue, result, owner, copyCache, session ); + + if ( originalValue == targetValue ) { + // Get the elements back into the target, making sure to handle the dirty flag. + boolean wasClean = PersistentCollection.class.isInstance( targetValue ) && + !( (PersistentCollection) targetValue ).isDirty(); + + // todo (6.0) - this is a bit inefficient, no need to do a whole deep replaceElements() call + replaceElements( result, targetValue, owner, copyCache, session ); + if ( wasClean ) { + ( (PersistentCollection) targetValue ).clearDirty();; + } + } + + return result; + } + + @SuppressWarnings("unchecked") + protected C instantiateResult(C originalValue) { + // todo (6.0) - how to handle arrays? 5.x handled this slightly different. + return (C) collectionDescriptor.instantiateRaw( -1 ); + } + + protected C replaceElements( + C originalValue, + C targetValue, + Object owner, + Map copyCache, + SessionImplementor session) { + java.util.Collection result = (java.util.Collection) targetValue; + result.clear(); + + // copy elements into newly empty target collection + final CollectionElement elementDescriptor = collectionDescriptor.getElementDescriptor(); + Iterator iter = ( (java.util.Collection) originalValue ).iterator(); + while ( iter.hasNext() ) { + result.add( elementDescriptor.replace( iter.next(), null, owner, copyCache, session ) ); + } + + // If the originalValue is a PersistentCollection and that originalValue + // was not flagged as dirty, then reset the targetValue's dirty flag + // here after the copy operation + // + // One thing to note is if the originalValue was a bare collection, we + // should not reset the dirty flag because we simply do not know. + if ( originalValue instanceof PersistentCollection ) { + if ( targetValue instanceof PersistentCollection ) { + final PersistentCollection originalCollection = (PersistentCollection) originalValue; + final PersistentCollection targetCollection = (PersistentCollection) targetValue; + + preserveSnapshot( originalCollection, targetCollection, owner, copyCache, session ); + + if ( !originalCollection.isDirty() ) { + targetCollection.clearDirty(); + } + } + } + + return (C) result; + } + + protected void preserveSnapshot( + PersistentCollection originalCollection, + PersistentCollection targetCollection, + Object owner, + Map copyCache, + SessionImplementor session) { + + // todo (6.0) - is it possible to refactor this code to subtypes? + + Object originalValue = originalCollection.getStoredSnapshot(); + Object targetValue = targetCollection.getStoredSnapshot(); + Object result; + + final CollectionElement elementDescriptor = getCollectionDescriptor().getElementDescriptor(); + + if ( originalValue instanceof List ) { + result = new ArrayList<>( ( (List) originalValue ).size() ); + for ( Object entry : (List) originalValue ) { + ( (List) result ).add( elementDescriptor.replace( entry, null, owner, copyCache, session ) ); + } + } + else if ( originalValue instanceof Map ) { + if ( originalValue instanceof SortedMap ) { + result = new TreeMap<>( ( (SortedMap) originalValue ).comparator() ); + } + else { + result = new HashMap<>( + CollectionHelper.determineProperSizing( ( (Map) originalValue ).size() ), + CollectionHelper.LOAD_FACTOR + ); + } + + for ( Map.Entry entry : ( (Map) originalValue ).entrySet() ) { + Object key = entry.getKey(); + Object value = entry.getValue(); + Object resultSnapshotValue = ( targetValue == null ) + ? null + : ( (Map) targetValue ).get( key ); + + Object newValue = elementDescriptor.replace( value, resultSnapshotValue,owner, copyCache, session ); + + if ( key == value ) { + ( (Map) result ).put( newValue, newValue ); + + } + else { + ( (Map) result ).put( key, newValue ); + } + } + + } + else if ( originalValue instanceof Object[] ) { + Object[] arr = (Object[]) originalValue; + for ( int i = 0; i < arr.length; i++ ) { + arr[i] = elementDescriptor.replace( arr[i], null, owner, copyCache, session ); + } + result = originalValue; + + } + else { + // retain the same snapshot + result = targetValue; + } + + CollectionEntry entry = session.getPersistenceContext().getCollectionEntry( targetCollection ); + if ( entry != null ) { + entry.resetStoredSnapshot( targetCollection, (Serializable) result ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/CollectionElement.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/CollectionElement.java index 1f86c5e76efb..4a754a16e298 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/CollectionElement.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/CollectionElement.java @@ -6,6 +6,9 @@ */ package org.hibernate.metamodel.model.domain.spi; +import java.util.Map; + +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.metamodel.model.domain.CollectionDomainType; import org.hibernate.metamodel.model.relational.spi.Table; import org.hibernate.sql.ast.produce.spi.TableReferenceContributor; @@ -42,4 +45,15 @@ default Class getJavaType() { boolean hasNotNullColumns(); boolean isMutable(); + + // todo (6.0) - should this be moved into a super contract? + default J replace(J originalValue, J targetValue, Object owner, Map copyCache, SessionImplementor session) { + return getJavaTypeDescriptor().getMutabilityPlan().replace( + originalValue, + targetValue, + owner, + copyCache, + session + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/CollectionIndex.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/CollectionIndex.java index 77d543245bf9..a4341383d098 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/CollectionIndex.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/CollectionIndex.java @@ -7,7 +7,9 @@ package org.hibernate.metamodel.model.domain.spi; import java.util.List; +import java.util.Map; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.metamodel.model.relational.spi.Column; import org.hibernate.sql.ast.produce.spi.TableReferenceContributor; @@ -42,4 +44,15 @@ default boolean canContainSubGraphs() { boolean isSettable(); int getBaseIndex(); + + // todo (6.0) - should this be moved into a super contract? + default J replace(J originalValue, J targetValue, Object owner, Map copyCache, SessionImplementor session) { + return getJavaTypeDescriptor().getMutabilityPlan().replace( + originalValue, + targetValue, + owner, + copyCache, + session + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/StateArrayContributor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/StateArrayContributor.java index b627ed752035..fd58208162fa 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/StateArrayContributor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/spi/StateArrayContributor.java @@ -117,7 +117,7 @@ default Object replace( Object owner, Map copyCache, SessionImplementor session) { - throw new NotYetImplementedFor6Exception(); + throw new NotYetImplementedFor6Exception( getClass() ); } default Object replace( diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/MutabilityPlan.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/MutabilityPlan.java index 6ad3497966f2..cb84212c2524 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/MutabilityPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/MutabilityPlan.java @@ -7,6 +7,10 @@ package org.hibernate.type.descriptor.java; import java.io.Serializable; +import java.util.Map; + +import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.engine.spi.SessionImplementor; /** * Describes the mutability aspects of a Java type. The term mutability refers to the fact that generally speaking @@ -54,4 +58,13 @@ public interface MutabilityPlan extends Serializable { * @see #disassemble */ T assemble(Serializable cached); + + default T replace( + T originalValue, + T targetValue, + Object owner, + Map copyCache, + SessionImplementor session) { + throw new NotYetImplementedFor6Exception( getClass() ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/ImmutableMutabilityPlan.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/ImmutableMutabilityPlan.java index 3351483f0cc9..f594c679bf71 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/ImmutableMutabilityPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/ImmutableMutabilityPlan.java @@ -7,7 +7,11 @@ package org.hibernate.type.descriptor.java.spi; import java.io.Serializable; +import java.util.Map; +import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.type.descriptor.java.MutabilityPlan; /** @@ -38,4 +42,21 @@ public Serializable disassemble(T value) { public T assemble(Serializable cached) { return (T) cached; } + + @Override + public T replace( + T originalValue, + T targetValue, + Object owner, + Map copyCache, + SessionImplementor session) { + if ( originalValue == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + // todo (6.0) - Is this scenario possible? + throw new NotYetImplementedFor6Exception( getClass() ); +// return targetValue; + } + else { + return originalValue; + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/internal/BasicTypeMutabilityPlanAdapter.java b/hibernate-core/src/main/java/org/hibernate/type/internal/BasicTypeMutabilityPlanAdapter.java index 1fd3b7b017c5..a487114c4c14 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/internal/BasicTypeMutabilityPlanAdapter.java +++ b/hibernate-core/src/main/java/org/hibernate/type/internal/BasicTypeMutabilityPlanAdapter.java @@ -7,7 +7,10 @@ package org.hibernate.type.internal; import java.io.Serializable; +import java.util.Map; +import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -42,4 +45,25 @@ public Serializable disassemble(T value) { public T assemble(Serializable cached) { return (T) basicType.assemble( cached ); } + + @Override + @SuppressWarnings("unchecked") + public T replace( + T originalValue, + T targetValue, + Object owner, + Map copyCache, + SessionImplementor session) { + if ( originalValue == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + return targetValue; + } + else if ( !isMutable() || + ( targetValue == LazyPropertyInitializer.UNFETCHED_PROPERTY && + basicType.getJavaTypeDescriptor().areEqual( originalValue, targetValue ) ) ) { + return originalValue; + } + else { + return deepCopy( originalValue ); + } + } }