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:
Alex Belyaev 2014-11-21 03:39:15 +04:00 committed by Gail Badner
parent f23b550b62
commit aa47860936
4 changed files with 303 additions and 0 deletions

View File

@ -231,6 +231,17 @@ 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

@ -296,6 +296,13 @@ 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;
}

View File

@ -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>

View File

@ -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();
}
}