HHH-9512: Avoid creation of invalid managed -> managed entity mapping in MergeContext when traversing cascade loop
(cherry picked from commit 816c97613d
)
This commit is contained in:
parent
f23b550b62
commit
aa47860936
|
@ -231,6 +231,17 @@ class MergeContext implements Map {
|
||||||
throw new NullPointerException( "null merge and managed entities are not supported by " + getClass().getName() );
|
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 );
|
Object oldManagedEntity = mergeToManagedEntityXref.put( mergeEntity, managedEntity );
|
||||||
Boolean oldOperatedOn = mergeEntityToOperatedOnFlagMap.put( mergeEntity, isOperatedOn );
|
Boolean oldOperatedOn = mergeEntityToOperatedOnFlagMap.put( mergeEntity, isOperatedOn );
|
||||||
// If managedEntity already corresponds with a different merge entity, that means
|
// If managedEntity already corresponds with a different merge entity, that means
|
||||||
|
|
|
@ -296,6 +296,13 @@ public abstract class EntityType extends AbstractType implements AssociationType
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Object cached = copyCache.get( original );
|
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 ) {
|
if ( cached != null ) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
~ 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>.
|
||||||
|
-->
|
||||||
|
<!DOCTYPE hibernate-mapping SYSTEM "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd" >
|
||||||
|
|
||||||
|
<hibernate-mapping package="org.hibernate.test.cascade.circle">
|
||||||
|
|
||||||
|
<class name="Route" table="HB_Route">
|
||||||
|
|
||||||
|
<id name="routeID" type="long"><generator class="native"/></id>
|
||||||
|
<version name="version" column="VERS" type="long" />
|
||||||
|
|
||||||
|
<property name="name" type="string" not-null="true"/>
|
||||||
|
|
||||||
|
<set name="nodes" inverse="true" cascade="persist,merge,refresh,save-update">
|
||||||
|
<key column="routeID"/>
|
||||||
|
<one-to-many class="Node"/>
|
||||||
|
</set>
|
||||||
|
<set name="vehicles" inverse="true" cascade="persist,merge,refresh,save-update">
|
||||||
|
<key column="routeID"/>
|
||||||
|
<one-to-many class="Vehicle"/>
|
||||||
|
</set>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
<class name="Tour" table="HB_Tour">
|
||||||
|
|
||||||
|
<id name="tourID" type="long"><generator class="native"/></id>
|
||||||
|
<version name="version" column="VERS" type="long" />
|
||||||
|
|
||||||
|
<property name="name" type="string" not-null="true"/>
|
||||||
|
|
||||||
|
<set name="nodes" inverse="true" lazy="true" cascade="merge,refresh,persist,save-update">
|
||||||
|
<key column="tourID"/>
|
||||||
|
<one-to-many class="Node"/>
|
||||||
|
</set>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
<class name="Transport" table="HB_Transport">
|
||||||
|
|
||||||
|
<id name="transportID" type="long"><generator class="native"/></id>
|
||||||
|
<version name="version" column="VERS" type="long" />
|
||||||
|
|
||||||
|
<property name="name" type="string" not-null="true"/>
|
||||||
|
|
||||||
|
<many-to-one name="pickupNode"
|
||||||
|
column="pickupNodeID"
|
||||||
|
unique="true"
|
||||||
|
not-null="true"
|
||||||
|
cascade="merge,refresh,persist,save-update"
|
||||||
|
lazy="false"/>
|
||||||
|
|
||||||
|
<many-to-one name="deliveryNode"
|
||||||
|
column="deliveryNodeID"
|
||||||
|
unique="true"
|
||||||
|
not-null="true"
|
||||||
|
cascade="merge,refresh,persist,save-update"
|
||||||
|
lazy="false"/>
|
||||||
|
|
||||||
|
<many-to-one name="vehicle"
|
||||||
|
column="vehicleID"
|
||||||
|
unique="false"
|
||||||
|
not-null="true"
|
||||||
|
cascade="merge,refresh,persist,save-update"
|
||||||
|
lazy="false"/>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
<class name="Vehicle" table="HB_Vehicle">
|
||||||
|
<id name="vehicleID" type="long"><generator class="native"/></id>
|
||||||
|
<version name="version" column="VERS" type="long" />
|
||||||
|
|
||||||
|
<property name="name"/>
|
||||||
|
<set name="transports" inverse="false" lazy="true" cascade="merge,refresh,persist,save-update">
|
||||||
|
<key column="vehicleID"/>
|
||||||
|
<one-to-many class="Transport" not-found="exception"/>
|
||||||
|
</set>
|
||||||
|
<many-to-one name="route"
|
||||||
|
column="routeID"
|
||||||
|
unique="false"
|
||||||
|
not-null="false"
|
||||||
|
cascade="merge,refresh,persist,save-update,save-update"
|
||||||
|
lazy="false"/>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
|
||||||
|
<class name="Node" table="HB_Node">
|
||||||
|
|
||||||
|
<id name="nodeID" type="long"><generator class="native"/></id>
|
||||||
|
<version name="version" column="VERS" type="long" />
|
||||||
|
|
||||||
|
<property name="name" type="string" not-null="true"/>
|
||||||
|
|
||||||
|
<set name="deliveryTransports" inverse="true" lazy="true" cascade="merge,refresh,persist,save-update">
|
||||||
|
<key column="deliveryNodeID"/>
|
||||||
|
<one-to-many class="Transport"/>
|
||||||
|
</set>
|
||||||
|
|
||||||
|
<set name="pickupTransports" inverse="true" lazy="true" cascade="merge,refresh,persist,save-update">
|
||||||
|
<key column="pickupNodeID"/>
|
||||||
|
<one-to-many class="Transport"/>
|
||||||
|
</set>
|
||||||
|
|
||||||
|
<many-to-one name="route"
|
||||||
|
column="routeID"
|
||||||
|
unique="false"
|
||||||
|
not-null="true"
|
||||||
|
cascade="merge,refresh,persist,save-update"
|
||||||
|
lazy="false"/>
|
||||||
|
|
||||||
|
<many-to-one name="tour"
|
||||||
|
column="tourID"
|
||||||
|
unique="false"
|
||||||
|
not-null="true"
|
||||||
|
cascade="merge,refresh,persist,save-update"
|
||||||
|
lazy="false"/>
|
||||||
|
</class>
|
||||||
|
|
||||||
|
</hibernate-mapping>
|
|
@ -0,0 +1,164 @@
|
||||||
|
/*
|
||||||
|
* 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.cascade.circle;
|
||||||
|
|
||||||
|
import org.hibernate.Session;
|
||||||
|
import org.hibernate.cfg.Configuration;
|
||||||
|
import org.hibernate.cfg.Environment;
|
||||||
|
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test case uses the following model:
|
||||||
|
*
|
||||||
|
* <- ->
|
||||||
|
* -- (N : 0,1) -- Tour
|
||||||
|
* | <- ->
|
||||||
|
* | -- (1 : N) -- (pickup) ----
|
||||||
|
* <- -> | | |
|
||||||
|
* Route -- (1 : N) -- Node Transport
|
||||||
|
* | <- -> |
|
||||||
|
* -- (1 : N) -- (delivery) --
|
||||||
|
*
|
||||||
|
* Arrows indicate the direction of cascade-merge, cascade-save, cascade-refresh and cascade-save-or-update
|
||||||
|
*
|
||||||
|
* It reproduces the following issues:
|
||||||
|
* https://hibernate.atlassian.net/browse/HHH-9512
|
||||||
|
* <p/>
|
||||||
|
* This tests that cascades are done properly from each entity.
|
||||||
|
*
|
||||||
|
* @author Alex Belyaev (based on code by Pavol Zibrita and Gail Badner)
|
||||||
|
*/
|
||||||
|
public class CascadeManagedAndTransientTest extends BaseCoreFunctionalTestCase {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getMappings() {
|
||||||
|
return new String[] {
|
||||||
|
"cascade/circle/CascadeManagedAndTransient.hbm.xml"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(Configuration cfg) {
|
||||||
|
super.configure( cfg );
|
||||||
|
cfg.setProperty( Environment.CHECK_NULLABILITY, "true" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanupTest() {
|
||||||
|
Session s = openSession();
|
||||||
|
s.beginTransaction();
|
||||||
|
s.createQuery( "delete from Transport" );
|
||||||
|
s.createQuery( "delete from Tour" );
|
||||||
|
s.createQuery( "delete from Node" );
|
||||||
|
s.createQuery( "delete from Route" );
|
||||||
|
s.createQuery( "delete from Vehicle" );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkNewVehicle(Vehicle newVehicle) {
|
||||||
|
assertEquals("Bus", newVehicle.getName());
|
||||||
|
assertEquals(1, newVehicle.getTransports().size());
|
||||||
|
Transport t = (Transport) newVehicle.getTransports().iterator().next();
|
||||||
|
assertEquals("Transport 2 -> 3", t.getName());
|
||||||
|
assertEquals("Node 2", t.getPickupNode().getName());
|
||||||
|
assertEquals("Node 3", t.getDeliveryNode().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAttachedChildInMerge() {
|
||||||
|
fillInitialData();
|
||||||
|
|
||||||
|
Session s = openSession();
|
||||||
|
s.beginTransaction();
|
||||||
|
|
||||||
|
Route route = (Route) s.createQuery("FROM Route WHERE name = :name").setString("name", "Route 1").uniqueResult();
|
||||||
|
Node n2 = (Node) s.createQuery("FROM Node WHERE name = :name").setString("name", "Node 2").uniqueResult();
|
||||||
|
Node n3 = (Node) s.createQuery("FROM Node WHERE name = :name").setString("name", "Node 3").uniqueResult();
|
||||||
|
|
||||||
|
Vehicle vehicle = new Vehicle();
|
||||||
|
vehicle.setName("Bus");
|
||||||
|
vehicle.setRoute(route);
|
||||||
|
|
||||||
|
Transport $2to3 = new Transport();
|
||||||
|
$2to3.setName("Transport 2 -> 3");
|
||||||
|
$2to3.setPickupNode(n2); n2.getPickupTransports().add($2to3);
|
||||||
|
$2to3.setDeliveryNode(n3); n3.getDeliveryTransports().add($2to3);
|
||||||
|
$2to3.setVehicle(vehicle);
|
||||||
|
|
||||||
|
vehicle.setTransports(new HashSet<Transport>(Arrays.asList($2to3)));
|
||||||
|
|
||||||
|
// Try to save graph of transient entities (vehicle, transport) which contains attached entities (node2, node3)
|
||||||
|
Vehicle managedVehicle = (Vehicle) s.merge(vehicle);
|
||||||
|
checkNewVehicle(managedVehicle);
|
||||||
|
|
||||||
|
s.flush();
|
||||||
|
s.clear();
|
||||||
|
|
||||||
|
assertEquals(3, s.createQuery("FROM Transport").list().size());
|
||||||
|
assertEquals(2, s.createQuery("FROM Vehicle").list().size());
|
||||||
|
assertEquals(4, s.createQuery("FROM Node").list().size());
|
||||||
|
|
||||||
|
Vehicle newVehicle = (Vehicle) s.createQuery("FROM Vehicle WHERE name = :name").setParameter("name", "Bus").uniqueResult();
|
||||||
|
checkNewVehicle(newVehicle);
|
||||||
|
|
||||||
|
s.getTransaction().commit();
|
||||||
|
s.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillInitialData() {
|
||||||
|
Tour tour = new Tour();
|
||||||
|
tour.setName("Tour 1");
|
||||||
|
|
||||||
|
Route route = new Route();
|
||||||
|
route.setName("Route 1");
|
||||||
|
|
||||||
|
ArrayList<Node> nodes = new ArrayList<Node>();
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
Node n = new Node();
|
||||||
|
n.setName("Node " + i);
|
||||||
|
n.setTour(tour);
|
||||||
|
n.setRoute(route);
|
||||||
|
nodes.add(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
tour.setNodes(new HashSet<Node>(nodes));
|
||||||
|
route.setNodes(new HashSet<Node>(Arrays.asList(nodes.get(0), nodes.get(1), nodes.get(2))));
|
||||||
|
|
||||||
|
Vehicle vehicle = new Vehicle();
|
||||||
|
vehicle.setName("Car");
|
||||||
|
route.setVehicles(new HashSet<Vehicle>(Arrays.asList(vehicle)));
|
||||||
|
vehicle.setRoute(route);
|
||||||
|
|
||||||
|
Transport $0to1 = new Transport();
|
||||||
|
$0to1.setName("Transport 0 -> 1");
|
||||||
|
$0to1.setPickupNode(nodes.get(0));
|
||||||
|
$0to1.setDeliveryNode(nodes.get(1));
|
||||||
|
$0to1.setVehicle(vehicle);
|
||||||
|
|
||||||
|
Transport $1to2 = new Transport();
|
||||||
|
$1to2.setName("Transport 1 -> 2");
|
||||||
|
$1to2.setPickupNode(nodes.get(1));
|
||||||
|
$1to2.setDeliveryNode(nodes.get(2));
|
||||||
|
$1to2.setVehicle(vehicle);
|
||||||
|
|
||||||
|
vehicle.setTransports(new HashSet<Transport>(Arrays.asList($0to1, $1to2)));
|
||||||
|
|
||||||
|
Session s = openSession();
|
||||||
|
s.beginTransaction();
|
||||||
|
|
||||||
|
s.persist(tour);
|
||||||
|
|
||||||
|
s.getTransaction().commit();
|
||||||
|
s.close();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue