HHH-12439 : Merging of new entities can fail depending on cascade order

This commit is contained in:
Gail Badner 2018-03-28 11:36:52 -07:00
parent 9d958291d8
commit c10dbe9d9c
3 changed files with 168 additions and 22 deletions

View File

@ -231,17 +231,6 @@ class MergeContext implements Map {
throw new NullPointerException( "null merge and managed entities are not supported by " + getClass().getName() );
}
// Detect invalid 'managed entity' -> 'managed entity' mappings where key != value
if ( managedToMergeEntityXref.containsKey( mergeEntity ) ) {
if ( managedToMergeEntityXref.get( mergeEntity ) != mergeEntity ) {
throw new IllegalStateException(
"MergeContext#attempt to create managed -> managed mapping with different entities: "
+ printEntity( mergeEntity ) + "; " + printEntity(
managedEntity )
);
}
}
Object oldManagedEntity = mergeToManagedEntityXref.put( mergeEntity, managedEntity );
Boolean oldOperatedOn = mergeEntityToOperatedOnFlagMap.put( mergeEntity, isOperatedOn );
// If managedEntity already corresponds with a different merge entity, that means

View File

@ -322,13 +322,6 @@ public abstract class EntityType extends AbstractType implements AssociationType
return null;
}
Object cached = copyCache.get( original );
if ( cached == null ) {
// Avoid creation of invalid managed -> managed mapping in copyCache when traversing
// cascade loop (@OneToMany(cascade=ALL) with associated @ManyToOne(cascade=ALL)) in entity graph
if ( copyCache.containsValue( original ) ) {
cached = original;
}
}
if ( cached != null ) {
return cached;
}
@ -338,10 +331,19 @@ public abstract class EntityType extends AbstractType implements AssociationType
}
if ( session.getContextEntityIdentifier( original ) == null &&
ForeignKeys.isTransient( associatedEntityName, original, Boolean.FALSE, session ) ) {
final Object copy = session.getEntityPersister( associatedEntityName, original )
.instantiate( null, session );
copyCache.put( original, copy );
return copy;
// original is transient; it is possible that original is a "managed" entity that has
// not been made persistent yet, so check if copyCache contains original as a "managed" value
// that corresponds with some "merge" value.
if ( copyCache.containsValue( original ) ) {
return original;
}
else {
// the transient entity is not "managed"; add the merge/managed pair to copyCache
final Object copy = session.getEntityPersister( associatedEntityName, original )
.instantiate( null, session );
copyCache.put( original, copy );
return copy;
}
}
else {
Object id = getIdentifier( original, session );

View File

@ -0,0 +1,155 @@
/*
* 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.test.ops;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import org.hibernate.cfg.Configuration;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
/**
* A 1 ------------> 1 B 1 ----------> 1 C
* 1 1
* | |
* | |
* V V
* 1 N
* D 1------------>N E
*
*
* @author Gail Badner
*/
public class MergeManagedAndCopiesAllowedTest extends BaseCoreFunctionalTestCase {
protected Class[] getAnnotatedClasses() {
return new Class[]{
A.class,
B.class,
C.class,
D.class,
E.class
};
}
protected void configure(Configuration cfg) {
super.configure( cfg );
cfg.setProperty( "hibernate.event.merge.entity_copy_observer", "allow" );
}
@Test
public void testIt() {
A a = new A();
a.b = new B();
a.b.d = new D();
a.b.d.dEs.add( new E() );
doInHibernate(
this::sessionFactory, session -> {
session.persist( a );
}
);
doInHibernate(
this::sessionFactory, session -> {
A aGet= session.get( A.class, a.id );
aGet.b.c = new C();
Set<E> copies = new HashSet<>();
for ( E e : aGet.b.d.dEs ) {
copies.add ( new E( e.id, "description" ) );
}
aGet.b.c.cEs.addAll( copies );
session.merge( aGet );
}
);
doInHibernate(
this::sessionFactory, session -> {
A aGet= session.get( A.class, a.id );
E e = aGet.b.c.cEs.iterator().next();
assertSame( e, aGet.b.d.dEs.iterator().next() );
assertEquals( "description", e.description );
}
);
}
@Entity(name = "A")
public static class A {
@Id
@GeneratedValue
private int id;
@OneToOne(fetch=FetchType.EAGER, cascade = CascadeType.ALL)
private B b;
}
@Entity(name = "B")
public static class B {
@Id
@GeneratedValue
private int id;
@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private C c;
@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private D d;
}
@Entity(name = "C")
public static class C {
@Id
@GeneratedValue
private int id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(nullable = true)
private Set<E> cEs = new HashSet<>();
}
@Entity(name = "D")
public static class D {
@Id
@GeneratedValue
private int id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(nullable = false)
private Set<E> dEs = new HashSet<>();
}
@Entity(name = "E")
public static class E {
@Id
@GeneratedValue
private int id;
private String description;
E() {}
E(int id, String description) {
this.id = id;
this.description = description;
}
}
}