Skip to content

Commit 0c8261b

Browse files
committed
HHH-9864 - foreign key violation with order_inserts=true and batches with mixed subclass entities
1 parent 814ea97 commit 0c8261b

8 files changed

+1007
-277
lines changed

hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java

Lines changed: 104 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
import java.io.ObjectInputStream;
1111
import java.io.ObjectOutputStream;
1212
import java.io.Serializable;
13+
import java.util.ArrayList;
1314
import java.util.HashMap;
1415
import java.util.HashSet;
1516
import java.util.LinkedHashMap;
1617
import java.util.LinkedList;
1718
import java.util.List;
1819
import java.util.Map;
20+
import java.util.Objects;
1921
import java.util.Queue;
2022
import java.util.Set;
2123
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -44,6 +46,8 @@
4446
import org.hibernate.internal.CoreMessageLogger;
4547
import org.hibernate.proxy.HibernateProxy;
4648
import org.hibernate.proxy.LazyInitializer;
49+
import org.hibernate.type.CollectionType;
50+
import org.hibernate.type.EntityType;
4751
import org.hibernate.type.Type;
4852

4953
/**
@@ -1008,12 +1012,56 @@ private static class InsertActionSorter implements ExecutableList.Sorter<Abstrac
10081012
*/
10091013
public static final InsertActionSorter INSTANCE = new InsertActionSorter();
10101014

1015+
private static class BatchIdentifier {
1016+
1017+
private final String entityName;
1018+
1019+
private Set<String> parentEntityNames = new HashSet<>( );
1020+
1021+
private Set<String> childEntityNames = new HashSet<>( );
1022+
1023+
public BatchIdentifier(
1024+
String entityName) {
1025+
this.entityName = entityName;
1026+
}
1027+
1028+
@Override
1029+
public boolean equals(Object o) {
1030+
if ( this == o ) {
1031+
return true;
1032+
}
1033+
if ( !( o instanceof BatchIdentifier ) ) {
1034+
return false;
1035+
}
1036+
BatchIdentifier that = (BatchIdentifier) o;
1037+
return Objects.equals( entityName, that.entityName );
1038+
}
1039+
1040+
@Override
1041+
public int hashCode() {
1042+
return Objects.hash( entityName );
1043+
}
1044+
1045+
public String getEntityName() {
1046+
return entityName;
1047+
}
1048+
1049+
public Set<String> getParentEntityNames() {
1050+
return parentEntityNames;
1051+
}
1052+
1053+
public Set<String> getChildEntityNames() {
1054+
return childEntityNames;
1055+
}
1056+
}
1057+
10111058
// the mapping of entity names to their latest batch numbers.
1012-
private Map<String, Integer> latestBatches;
1013-
private Map<Object, Integer> entityBatchNumber;
1059+
private List<BatchIdentifier> latestBatches;
1060+
1061+
private Map<Object, BatchIdentifier> entityBatchIdentifier;
10141062

10151063
// the map of batch numbers to EntityInsertAction lists
1016-
private Map<Integer, List<AbstractEntityInsertAction>> actionBatches;
1064+
private Map<BatchIdentifier, List<AbstractEntityInsertAction>> actionBatches;
10171065

10181066
public InsertActionSorter() {
10191067
}
@@ -1023,100 +1071,99 @@ public InsertActionSorter() {
10231071
*/
10241072
public void sort(List<AbstractEntityInsertAction> insertions) {
10251073
// optimize the hash size to eliminate a rehash.
1026-
this.latestBatches = new HashMap<String, Integer>();
1027-
this.entityBatchNumber = new HashMap<Object, Integer>( insertions.size() + 1, 1.0f );
1028-
this.actionBatches = new HashMap<Integer, List<AbstractEntityInsertAction>>();
1074+
this.latestBatches = new ArrayList<>( );
1075+
this.entityBatchIdentifier = new HashMap<>( insertions.size() + 1, 1.0f );
1076+
this.actionBatches = new HashMap<>();
10291077

1030-
// the list of entity names that indicate the batch number
10311078
for ( AbstractEntityInsertAction action : insertions ) {
1032-
// remove the current element from insertions. It will be added back later.
1033-
String entityName = action.getEntityName();
1079+
BatchIdentifier batchIdentifier = new BatchIdentifier( action.getEntityName() );
10341080

10351081
// the entity associated with the current action.
10361082
Object currentEntity = action.getInstance();
1083+
int index = latestBatches.indexOf( batchIdentifier );
10371084

1038-
Integer batchNumber;
1039-
if ( latestBatches.containsKey( entityName ) ) {
1040-
// There is already an existing batch for this type of entity.
1041-
// Check to see if the latest batch is acceptable.
1042-
batchNumber = findBatchNumber( action, entityName );
1085+
if ( index != -1 ) {
1086+
batchIdentifier = latestBatches.get( index );
10431087
}
10441088
else {
1045-
// add an entry for this type of entity.
1046-
// we can be assured that all referenced entities have already
1047-
// been processed,
1048-
// so specify that this entity is with the latest batch.
1049-
// doing the batch number beforeQuery adding the name to the list is
1050-
// a faster way to get an accurate number.
1051-
1052-
batchNumber = actionBatches.size();
1053-
latestBatches.put( entityName, batchNumber );
1089+
latestBatches.add( batchIdentifier );
10541090
}
1055-
entityBatchNumber.put( currentEntity, batchNumber );
1056-
addToBatch( batchNumber, action );
1091+
addParentChildEntityNames( action, batchIdentifier );
1092+
entityBatchIdentifier.put( currentEntity, batchIdentifier );
1093+
addToBatch(batchIdentifier, action);
10571094
}
10581095
insertions.clear();
10591096

1097+
for ( int i = 0; i < latestBatches.size(); i++ ) {
1098+
BatchIdentifier batchIdentifier = latestBatches.get( i );
1099+
String entityName = batchIdentifier.getEntityName();
1100+
1101+
//Make sure that child entries are not before parents
1102+
for ( int j = i - 1; j >= 0; j-- ) {
1103+
BatchIdentifier prevBatchIdentifier = latestBatches.get( j );
1104+
if(prevBatchIdentifier.getParentEntityNames().contains( entityName )) {
1105+
latestBatches.remove( i );
1106+
latestBatches.add( j, batchIdentifier );
1107+
}
1108+
}
1109+
1110+
//Make sure that parent entries are not after children
1111+
for ( int j = i + 1; j < latestBatches.size(); j++ ) {
1112+
BatchIdentifier nextBatchIdentifier = latestBatches.get( j );
1113+
//Take care of unidirectional @OneToOne associations but exclude bidirectional @ManyToMany
1114+
if(nextBatchIdentifier.getChildEntityNames().contains( entityName ) &&
1115+
!batchIdentifier.getChildEntityNames().contains( nextBatchIdentifier.getEntityName() )) {
1116+
latestBatches.remove( i );
1117+
latestBatches.add( j, batchIdentifier );
1118+
}
1119+
}
1120+
}
1121+
10601122
// now rebuild the insertions list. There is a batch for each entry in the name list.
1061-
for ( int i = 0; i < actionBatches.size(); i++ ) {
1062-
List<AbstractEntityInsertAction> batch = actionBatches.get( i );
1123+
for ( BatchIdentifier rootIdentifier : latestBatches ) {
1124+
List<AbstractEntityInsertAction> batch = actionBatches.get( rootIdentifier );
10631125
insertions.addAll( batch );
10641126
}
10651127
}
10661128

10671129
/**
1068-
* Finds an acceptable batch for this entity to be a member as part of the {@link InsertActionSorter}
1130+
* Add parent and child entity names so that we know how to rearrange dependencies
10691131
*
10701132
* @param action The action being sorted
1071-
* @param entityName The name of the entity affected by the action
1072-
* @return An appropriate batch number; todo document this process better
1133+
* @param batchIdentifier The batch identifier of the entity affected by the action
10731134
*/
1074-
private Integer findBatchNumber(AbstractEntityInsertAction action, String entityName) {
1075-
// loop through all the associated entities and make sure they have been
1076-
// processed beforeQuery the latest
1077-
// batch associated with this entity type.
1078-
1079-
// the current batch number is the latest batch for this entity type.
1080-
Integer latestBatchNumberForType = latestBatches.get( entityName );
1081-
1082-
// loop through all the associations of the current entity and make sure that they are processed
1083-
// beforeQuery the current batch number
1135+
private void addParentChildEntityNames(AbstractEntityInsertAction action, BatchIdentifier batchIdentifier) {
10841136
Object[] propertyValues = action.getState();
10851137
Type[] propertyTypes = action.getPersister().getClassMetadata().getPropertyTypes();
10861138

10871139
for ( int i = 0; i < propertyValues.length; i++ ) {
10881140
Object value = propertyValues[i];
10891141
Type type = propertyTypes[i];
10901142
if ( type.isEntityType() && value != null ) {
1091-
// find the batch number associated with the current association, if any.
1092-
Integer associationBatchNumber = entityBatchNumber.get( value );
1093-
if ( associationBatchNumber != null && associationBatchNumber.compareTo( latestBatchNumberForType ) > 0 ) {
1094-
// create a new batch for this type. The batch number is the number of current batches.
1095-
latestBatchNumberForType = actionBatches.size();
1096-
latestBatches.put( entityName, latestBatchNumberForType );
1097-
// since this entity will now be processed in the latest possible batch,
1098-
// we can be assured that it will come afterQuery all other associations,
1099-
// there's not need to continue checking.
1100-
break;
1101-
}
1143+
EntityType entityType = (EntityType) type;
1144+
String entityName = entityType.getName();
1145+
batchIdentifier.getParentEntityNames().add( entityName );
1146+
}
1147+
else if ( type.isCollectionType() && value != null ) {
1148+
CollectionType collectionType = (CollectionType) type;
1149+
String entityName = collectionType.getAssociatedEntityName( ( (SessionImplementor) action.getSession() ).getSessionFactory() );
1150+
batchIdentifier.getChildEntityNames().add( entityName );
11021151
}
11031152
}
1104-
return latestBatchNumberForType;
11051153
}
11061154

1107-
private void addToBatch(Integer batchNumber, AbstractEntityInsertAction action) {
1108-
List<AbstractEntityInsertAction> actions = actionBatches.get( batchNumber );
1155+
private void addToBatch(BatchIdentifier batchIdentifier, AbstractEntityInsertAction action) {
1156+
List<AbstractEntityInsertAction> actions = actionBatches.get( batchIdentifier );
11091157

11101158
if ( actions == null ) {
1111-
actions = new LinkedList<AbstractEntityInsertAction>();
1112-
actionBatches.put( batchNumber, actions );
1159+
actions = new LinkedList<>();
1160+
actionBatches.put( batchIdentifier, actions );
11131161
}
11141162
actions.add( action );
11151163
}
11161164

11171165
}
11181166

1119-
11201167
private static abstract class ListProvider<T extends Executable & Comparable & Serializable> {
11211168
abstract ExecutableList<T> get(ActionQueue instance);
11221169
abstract ExecutableList<T> init(ActionQueue instance);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
6+
*/
7+
package org.hibernate.test.insertordering;
8+
9+
import java.sql.PreparedStatement;
10+
import java.sql.SQLException;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.Map;
14+
import javax.persistence.CascadeType;
15+
import javax.persistence.Column;
16+
import javax.persistence.Entity;
17+
import javax.persistence.GeneratedValue;
18+
import javax.persistence.GenerationType;
19+
import javax.persistence.Id;
20+
import javax.persistence.ManyToMany;
21+
import javax.persistence.SequenceGenerator;
22+
23+
import org.hibernate.cfg.Environment;
24+
25+
import org.hibernate.testing.TestForIssue;
26+
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
27+
import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider;
28+
import org.junit.Test;
29+
30+
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
31+
import static org.junit.Assert.assertEquals;
32+
import static org.mockito.Mockito.times;
33+
import static org.mockito.Mockito.verify;
34+
35+
/**
36+
* @author Vlad Mihalcea
37+
*/
38+
@TestForIssue(jiraKey = "HHH-9864")
39+
public class InsertOrderingWithBidirectionalManyToMany
40+
extends BaseNonConfigCoreFunctionalTestCase {
41+
42+
private PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider();
43+
44+
@Override
45+
protected Class[] getAnnotatedClasses() {
46+
return new Class[] { Address.class, Person.class };
47+
}
48+
49+
@Override
50+
protected void addSettings(Map settings) {
51+
settings.put( Environment.ORDER_INSERTS, "true" );
52+
settings.put( Environment.STATEMENT_BATCH_SIZE, "10" );
53+
settings.put(
54+
org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER,
55+
connectionProvider
56+
);
57+
}
58+
59+
@Override
60+
public void releaseResources() {
61+
super.releaseResources();
62+
connectionProvider.stop();
63+
}
64+
65+
@Test
66+
public void testBatching() throws SQLException {
67+
doInHibernate( this::sessionFactory, session -> {
68+
Person father = new Person();
69+
Person mother = new Person();
70+
Person son = new Person();
71+
Person daughter = new Person();
72+
73+
Address home = new Address();
74+
Address office = new Address();
75+
76+
home.addPerson( father );
77+
home.addPerson( mother );
78+
home.addPerson( son );
79+
home.addPerson( daughter );
80+
81+
office.addPerson( father );
82+
office.addPerson( mother );
83+
84+
session.persist( home );
85+
session.persist( office );
86+
87+
connectionProvider.clear();
88+
} );
89+
90+
assertEquals( 3, connectionProvider.getPreparedStatements().size() );
91+
PreparedStatement addressPreparedStatement = connectionProvider.getPreparedStatement(
92+
"insert into Address (ID) values (?)" );
93+
verify( addressPreparedStatement, times( 2 ) ).addBatch();
94+
PreparedStatement personPreparedStatement = connectionProvider.getPreparedStatement(
95+
"insert into Person (ID) values (?)" );
96+
verify( personPreparedStatement, times( 4 ) ).addBatch();
97+
}
98+
99+
@Entity(name = "Address")
100+
public static class Address {
101+
@Id
102+
@Column(name = "ID", nullable = false)
103+
@SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ")
104+
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID")
105+
private Long id;
106+
107+
@ManyToMany(mappedBy = "addresses", cascade = CascadeType.PERSIST)
108+
private List<Person> persons = new ArrayList<>();
109+
110+
public void addPerson(Person person) {
111+
persons.add( person );
112+
person.addresses.add( this );
113+
}
114+
}
115+
116+
@Entity(name = "Person")
117+
public static class Person {
118+
@Id
119+
@Column(name = "ID", nullable = false)
120+
@SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ")
121+
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID")
122+
private Long id;
123+
124+
@ManyToMany
125+
private List<Address> addresses = new ArrayList<>();
126+
}
127+
}

0 commit comments

Comments
 (0)