HHH-16485 Insert ordering doesn't consider root entity names

This commit is contained in:
Christian Beikov 2023-04-19 13:49:39 +02:00
parent 1399adce7d
commit 712b6c7668
4 changed files with 2738 additions and 246 deletions

View File

@ -11,13 +11,13 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.BitSet;
import java.util.HashSet; import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
@ -45,7 +45,7 @@ import org.hibernate.cache.CacheException;
import org.hibernate.engine.internal.NonNullableTransientDependencies; import org.hibernate.engine.internal.NonNullableTransientDependencies;
import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.metadata.ClassMetadata; import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer; import org.hibernate.proxy.LazyInitializer;
import org.hibernate.type.CollectionType; import org.hibernate.type.CollectionType;
@ -128,7 +128,7 @@ public class ActionQueue {
ExecutableList<AbstractEntityInsertAction> init(ActionQueue instance) { ExecutableList<AbstractEntityInsertAction> init(ActionQueue instance) {
if ( instance.isOrderInsertsEnabled() ) { if ( instance.isOrderInsertsEnabled() ) {
return instance.insertions = new ExecutableList<AbstractEntityInsertAction>( return instance.insertions = new ExecutableList<AbstractEntityInsertAction>(
new InsertActionSorter() InsertActionSorter.INSTANCE
); );
} }
else { else {
@ -1022,11 +1022,13 @@ public class ActionQueue {
* directionality of foreign-keys. So even though we will be changing the ordering here, we need to make absolutely * directionality of foreign-keys. So even though we will be changing the ordering here, we need to make absolutely
* certain that we do not circumvent this FK ordering to the extent of causing constraint violations. * certain that we do not circumvent this FK ordering to the extent of causing constraint violations.
* <p> * <p>
* Sorts the insert actions using more hashes. * The algorithm first discovers the transitive incoming dependencies for every insert action
* and groups all inserts by the entity name.
* Finally, it schedules these groups one by one, as long as all the dependencies of the groups are fulfilled.
* </p> * </p>
* NOTE: this class is not thread-safe. * The implementation will only produce an optimal insert order for the insert groups that can be perfectly scheduled serially.
* * Scheduling serially means, that there is an order which doesn't violate the FK constraint dependencies.
* @author Jay Erb * The inserts of insert groups which can't be scheduled, are going to be inserted in the original order.
*/ */
private static class InsertActionSorter implements ExecutableList.Sorter<AbstractEntityInsertAction> { private static class InsertActionSorter implements ExecutableList.Sorter<AbstractEntityInsertAction> {
/** /**
@ -1034,106 +1036,140 @@ public class ActionQueue {
*/ */
public static final InsertActionSorter INSTANCE = new InsertActionSorter(); public static final InsertActionSorter INSTANCE = new InsertActionSorter();
private static class BatchIdentifier { private static class InsertInfo {
private final AbstractEntityInsertAction insertAction;
// Inserts in this set must be executed before this insert
private Set<InsertInfo> transitiveIncomingDependencies;
// Child dependencies of i.e. one-to-many or inverse one-to-one
// It's necessary to have this for unidirectional associations, to propagate incoming dependencies
private Set<InsertInfo> outgoingDependencies;
// The current index of the insert info within an insert schedule
private int index;
private final String entityName; public InsertInfo(AbstractEntityInsertAction insertAction, int index) {
private final String rootEntityName; this.insertAction = insertAction;
this.index = index;
private Set<String> parentEntityNames = new HashSet<>( );
private Set<String> childEntityNames = new HashSet<>( );
private BatchIdentifier parent;
BatchIdentifier(String entityName, String rootEntityName) {
this.entityName = entityName;
this.rootEntityName = rootEntityName;
} }
public BatchIdentifier getParent() { public void buildDirectDependencies(IdentityHashMap<Object, InsertInfo> insertInfosByEntity) {
return parent; final Object[] propertyValues = insertAction.getState();
final Type[] propertyTypes = insertAction.getPersister().getPropertyTypes();
for (int i = 0, propertyTypesLength = propertyTypes.length; i < propertyTypesLength; i++) {
addDirectDependency(propertyTypes[i], propertyValues[i], insertInfosByEntity);
}
} }
public void setParent(BatchIdentifier parent) { public void propagateChildDependencies() {
this.parent = parent; if ( outgoingDependencies != null ) {
for (InsertInfo childDependency : outgoingDependencies) {
if (childDependency.transitiveIncomingDependencies == null) {
childDependency.transitiveIncomingDependencies = new HashSet<>();
}
childDependency.transitiveIncomingDependencies.add(this);
}
}
}
public void buildTransitiveDependencies(Set<InsertInfo> visited) {
if (transitiveIncomingDependencies != null) {
visited.addAll(transitiveIncomingDependencies);
for (InsertInfo insertInfo : transitiveIncomingDependencies.toArray(new InsertInfo[0])) {
insertInfo.addTransitiveDependencies(this, visited);
}
visited.clear();
}
}
public void addTransitiveDependencies(InsertInfo origin, Set<InsertInfo> visited) {
if (transitiveIncomingDependencies != null) {
for (InsertInfo insertInfo : transitiveIncomingDependencies) {
if (visited.add(insertInfo)) {
origin.transitiveIncomingDependencies.add(insertInfo);
insertInfo.addTransitiveDependencies(origin, visited);
}
}
}
}
private void addDirectDependency(Type type, Object value, IdentityHashMap<Object, InsertInfo> insertInfosByEntity) {
if ( type.isEntityType() && value != null ) {
final EntityType entityType = (EntityType) type;
final InsertInfo insertInfo = insertInfosByEntity.get(value);
if (insertInfo != null) {
if (entityType.isOneToOne() && OneToOneType.class.cast(entityType).getForeignKeyDirection() == ForeignKeyDirection.TO_PARENT) {
if (!entityType.isReferenceToPrimaryKey()) {
if (outgoingDependencies == null) {
outgoingDependencies = new HashSet<>();
}
outgoingDependencies.add(insertInfo);
}
}
else {
if (transitiveIncomingDependencies == null) {
transitiveIncomingDependencies = new HashSet<>();
}
transitiveIncomingDependencies.add(insertInfo);
}
}
}
else if ( type.isCollectionType() && value != null ) {
CollectionType collectionType = (CollectionType) type;
final CollectionPersister collectionPersister = insertAction.getSession().getFactory().getMetamodel().collectionPersister(collectionType.getRole());
// We only care about mappedBy one-to-many associations, because for these, the elements depend on the collection owner
if ( collectionPersister.isOneToMany() && collectionPersister.getElementType().isEntityType() ) {
final Iterator<Object> elementsIterator = collectionType.getElementsIterator(value, insertAction.getSession());
while ( elementsIterator.hasNext() ) {
final Object element = elementsIterator.next();
final InsertInfo insertInfo = insertInfosByEntity.get(element);
if (insertInfo != null) {
if (outgoingDependencies == null) {
outgoingDependencies = new HashSet<>();
}
outgoingDependencies.add(insertInfo);
}
}
}
}
else if ( type.isComponentType() && value != null ) {
// Support recursive checks of composite type properties for associations and collections.
CompositeType compositeType = (CompositeType) type;
final SharedSessionContractImplementor session = insertAction.getSession();
Object[] componentValues = compositeType.getPropertyValues( value, session );
for ( int j = 0; j < componentValues.length; ++j ) {
Type componentValueType = compositeType.getSubtypes()[j];
Object componentValue = componentValues[j];
addDirectDependency( componentValueType, componentValue, insertInfosByEntity);
}
}
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if ( this == o ) { if (this == o) {
return true; return true;
} }
if ( !( o instanceof BatchIdentifier ) ) { if (o == null || getClass() != o.getClass()) {
return false; return false;
} }
BatchIdentifier that = (BatchIdentifier) o;
return Objects.equals( entityName, that.entityName ); InsertInfo that = (InsertInfo) o;
return insertAction.equals(that.insertAction);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash( entityName ); return insertAction.hashCode();
} }
String getEntityName() { @Override
return entityName; public String toString() {
} return "InsertInfo{" +
"insertAction=" + insertAction +
String getRootEntityName() { '}';
return rootEntityName;
}
Set<String> getParentEntityNames() {
return parentEntityNames;
}
Set<String> getChildEntityNames() {
return childEntityNames;
}
boolean hasAnyParentEntityNames(BatchIdentifier batchIdentifier) {
return parentEntityNames.contains( batchIdentifier.getEntityName() ) ||
parentEntityNames.contains( batchIdentifier.getRootEntityName() );
}
boolean hasAnyChildEntityNames(BatchIdentifier batchIdentifier) {
return childEntityNames.contains( batchIdentifier.getEntityName() );
}
/**
* Check if this {@link BatchIdentifier} has a parent or grand parent
* matching the given {@link BatchIdentifier} reference.
*
* @param batchIdentifier {@link BatchIdentifier} reference
*
* @return This {@link BatchIdentifier} has a parent matching the given {@link BatchIdentifier} reference
*/
boolean hasParent(BatchIdentifier batchIdentifier) {
return (
parent == batchIdentifier
|| ( parentEntityNames.contains( batchIdentifier.getEntityName() ) )
|| parent != null && parent.hasParent( batchIdentifier, new ArrayList<>() )
);
}
private boolean hasParent(BatchIdentifier batchIdentifier, List<BatchIdentifier> stack) {
if ( !stack.contains( this ) && parent != null ) {
stack.add( this );
return parent.hasParent( batchIdentifier, stack );
}
return (
parent == batchIdentifier
|| parentEntityNames.contains( batchIdentifier.getEntityName() )
);
} }
} }
// the mapping of entity names to their latest batch numbers.
private List<BatchIdentifier> latestBatches;
// the map of batch numbers to EntityInsertAction lists
private Map<BatchIdentifier, List<AbstractEntityInsertAction>> actionBatches;
public InsertActionSorter() { public InsertActionSorter() {
} }
@ -1141,181 +1177,144 @@ public class ActionQueue {
* Sort the insert actions. * Sort the insert actions.
*/ */
public void sort(List<AbstractEntityInsertAction> insertions) { public void sort(List<AbstractEntityInsertAction> insertions) {
// optimize the hash size to eliminate a rehash. final int insertInfoCount = insertions.size();
this.latestBatches = new ArrayList<>( ); // Build up dependency metadata for insert actions
this.actionBatches = new HashMap<>(); final InsertInfo[] insertInfos = new InsertInfo[insertInfoCount];
// A map of all insert infos keyed by the entity instance
// This is needed to discover insert infos for direct dependencies
final IdentityHashMap<Object, InsertInfo> insertInfosByEntity = new IdentityHashMap<>( insertInfos.length );
// Construct insert infos and build a map for that, keyed by entity instance
for (int i = 0; i < insertInfoCount; i++) {
final AbstractEntityInsertAction insertAction = insertions.get(i);
final InsertInfo insertInfo = new InsertInfo(insertAction, i);
insertInfosByEntity.put(insertAction.getInstance(), insertInfo);
insertInfos[i] = insertInfo;
}
// First we must discover the direct dependencies
for (int i = 0; i < insertInfoCount; i++) {
insertInfos[i].buildDirectDependencies(insertInfosByEntity);
}
// Then we can propagate child dependencies to the insert infos incoming dependencies
for (int i = 0; i < insertInfoCount; i++) {
insertInfos[i].propagateChildDependencies();
}
// Finally, we add all the transitive incoming dependencies
// and then group insert infos into EntityInsertGroup keyed by entity name
final Set<InsertInfo> visited = new HashSet<>();
final Map<String, EntityInsertGroup> insertInfosByEntityName = new LinkedHashMap<>();
for (int i = 0; i < insertInfoCount; i++) {
final InsertInfo insertInfo = insertInfos[i];
insertInfo.buildTransitiveDependencies( visited );
for ( AbstractEntityInsertAction action : insertions ) { final String entityName = insertInfo.insertAction.getPersister().getEntityName();
BatchIdentifier batchIdentifier = new BatchIdentifier( EntityInsertGroup entityInsertGroup = insertInfosByEntityName.get(entityName);
action.getEntityName(), if (entityInsertGroup == null) {
action.getSession() insertInfosByEntityName.put(entityName, entityInsertGroup = new EntityInsertGroup(entityName));
.getFactory()
.getMetamodel()
.entityPersister( action.getEntityName() )
.getRootEntityName()
);
// the entity associated with the current action.
Object currentEntity = action.getInstance();
int index = latestBatches.indexOf( batchIdentifier );
if ( index != -1 ) {
batchIdentifier = latestBatches.get( index );
} }
else { entityInsertGroup.add(insertInfo);
latestBatches.add( batchIdentifier ); }
// Now we can go through the EntityInsertGroups and schedule all the ones
// for which we have already scheduled all the dependentEntityNames
final Set<String> scheduledEntityNames = new HashSet<>(insertInfosByEntityName.size());
int schedulePosition = 0;
int lastScheduleSize;
do {
lastScheduleSize = scheduledEntityNames.size();
final Iterator<EntityInsertGroup> iterator = insertInfosByEntityName.values().iterator();
while (iterator.hasNext()) {
final EntityInsertGroup insertGroup = iterator.next();
if (scheduledEntityNames.containsAll(insertGroup.dependentEntityNames)) {
schedulePosition = schedule(insertInfos, insertGroup.insertInfos, schedulePosition);
scheduledEntityNames.add(insertGroup.entityName);
iterator.remove();
}
} }
addParentChildEntityNames( action, batchIdentifier ); // we try to schedule entity groups over and over again, until we can't schedule any further
addToBatch( batchIdentifier, action ); } while (lastScheduleSize != scheduledEntityNames.size());
if ( !insertInfosByEntityName.isEmpty() ) {
LOG.warn("The batch containing " + insertions.size() + " statements could not be sorted. " +
"This might indicate a circular entity relationship.");
} }
insertions.clear(); insertions.clear();
for (InsertInfo insertInfo : insertInfos) {
// Examine each entry in the batch list, and build the dependency graph. insertions.add(insertInfo.insertAction);
for ( int i = 0; i < latestBatches.size(); i++ ) {
BatchIdentifier batchIdentifier = latestBatches.get( i );
for ( int j = i - 1; j >= 0; j-- ) {
BatchIdentifier prevBatchIdentifier = latestBatches.get( j );
if ( prevBatchIdentifier.hasAnyParentEntityNames( batchIdentifier ) ) {
prevBatchIdentifier.parent = batchIdentifier;
}
if ( batchIdentifier.hasAnyChildEntityNames( prevBatchIdentifier ) ) {
prevBatchIdentifier.parent = batchIdentifier;
}
}
for ( int j = i + 1; j < latestBatches.size(); j++ ) {
BatchIdentifier nextBatchIdentifier = latestBatches.get( j );
if ( nextBatchIdentifier.hasAnyParentEntityNames( batchIdentifier ) ) {
nextBatchIdentifier.parent = batchIdentifier;
}
if ( batchIdentifier.hasAnyChildEntityNames( nextBatchIdentifier ) ) {
nextBatchIdentifier.parent = batchIdentifier;
}
}
}
boolean sorted = false;
long maxIterations = latestBatches.size() * latestBatches.size();
long iterations = 0;
sort:
do {
// Examine each entry in the batch list, sorting them based on parent/child association
// as depicted by the dependency graph.
iterations++;
for ( int i = 0; i < latestBatches.size(); i++ ) {
BatchIdentifier batchIdentifier = latestBatches.get( i );
// Iterate next batches and make sure that children types are after parents.
// Since the outer loop looks at each batch entry individually and the prior loop will reorder
// entries as well, we need to look and verify if the current batch is a child of the next
// batch or if the current batch is seen as a parent or child of the next batch.
for ( int j = i + 1; j < latestBatches.size(); j++ ) {
BatchIdentifier nextBatchIdentifier = latestBatches.get( j );
if ( batchIdentifier.hasParent( nextBatchIdentifier ) && !nextBatchIdentifier.hasParent( batchIdentifier ) ) {
latestBatches.remove( batchIdentifier );
latestBatches.add( j, batchIdentifier );
continue sort;
}
}
}
sorted = true;
}
while ( !sorted && iterations <= maxIterations);
if ( iterations > maxIterations ) {
LOG.warn( "The batch containing " + latestBatches.size() + " statements could not be sorted after " + maxIterations + " iterations. " +
"This might indicate a circular entity relationship." );
}
// Now, rebuild the insertions list. There is a batch for each entry in the name list.
for ( BatchIdentifier rootIdentifier : latestBatches ) {
List<AbstractEntityInsertAction> batch = actionBatches.get( rootIdentifier );
insertions.addAll( batch );
} }
} }
/** private int schedule(InsertInfo[] insertInfos, List<InsertInfo> insertInfosToSchedule, int schedulePosition) {
* Add parent and child entity names so that we know how to rearrange dependencies final InsertInfo[] newInsertInfos = new InsertInfo[insertInfos.length];
* // The bitset is there to quickly query if an index is already scheduled
* @param action The action being sorted final BitSet bitSet = new BitSet(insertInfos.length);
* @param batchIdentifier The batch identifier of the entity affected by the action // Remember the smallest index of the insertInfosToSchedule to check if we actually need to reorder anything
*/ int smallestScheduledIndex = -1;
private void addParentChildEntityNames(AbstractEntityInsertAction action, BatchIdentifier batchIdentifier) { // The biggestScheduledIndex is needed as upper bound for shifting elements that were replaced by insertInfosToSchedule
Object[] propertyValues = action.getState(); int biggestScheduledIndex = -1;
ClassMetadata classMetadata = action.getPersister().getClassMetadata(); for (int i = 0; i < insertInfosToSchedule.size(); i++) {
if ( classMetadata != null ) { final int index = insertInfosToSchedule.get(i).index;
Type[] propertyTypes = classMetadata.getPropertyTypes(); bitSet.set(index);
smallestScheduledIndex = Math.min(smallestScheduledIndex, index);
for ( int i = 0; i < propertyValues.length; i++ ) { biggestScheduledIndex = Math.max(biggestScheduledIndex, index);
Object value = propertyValues[i]; }
Type type = propertyTypes[i]; final int nextSchedulePosition = schedulePosition + insertInfosToSchedule.size();
addParentChildEntityNameByPropertyAndValue( action, batchIdentifier, type, value ); if (smallestScheduledIndex == schedulePosition && biggestScheduledIndex == nextSchedulePosition) {
// In this case, the order is already correct and we can skip some copying
return nextSchedulePosition;
}
// The index to which we start to shift elements that appear within the range of [schedulePosition, nextSchedulePosition)
int shiftSchedulePosition = nextSchedulePosition;
for (int i = 0; i < insertInfosToSchedule.size(); i++) {
final InsertInfo insertInfoToSchedule = insertInfosToSchedule.get(i);
final int targetSchedulePosition = schedulePosition + i;
newInsertInfos[targetSchedulePosition] = insertInfoToSchedule;
insertInfoToSchedule.index = targetSchedulePosition;
final InsertInfo oldInsertInfo = insertInfos[targetSchedulePosition];
// Move the insert info previously located at the target schedule position to the current shift position
if (!bitSet.get(targetSchedulePosition)) {
oldInsertInfo.index = shiftSchedulePosition;
// Also set this index in the bitset to skip copying the value later, as it is considered scheduled
bitSet.set(targetSchedulePosition);
newInsertInfos[shiftSchedulePosition++]= oldInsertInfo;
} }
} }
// We have to shift all the elements up to the biggestMovedIndex + 1
biggestScheduledIndex++;
for (int i = bitSet.nextClearBit(schedulePosition); i < biggestScheduledIndex; i++) {
// Only copy the old insert info over if it wasn't already scheduled
if (!bitSet.get(i)) {
final InsertInfo insertInfo = insertInfos[i];
insertInfo.index = shiftSchedulePosition;
newInsertInfos[shiftSchedulePosition++] = insertInfo;
}
}
// Copy over the newly reordered array part into the main array
System.arraycopy(newInsertInfos, schedulePosition, insertInfos, schedulePosition, biggestScheduledIndex - schedulePosition);
return nextSchedulePosition;
} }
private void addParentChildEntityNameByPropertyAndValue(AbstractEntityInsertAction action, BatchIdentifier batchIdentifier, Type type, Object value) { public static class EntityInsertGroup {
if ( type.isEntityType() && value != null ) { private final String entityName;
final EntityType entityType = (EntityType) type; private final List<InsertInfo> insertInfos = new ArrayList<>();
final String entityName = entityType.getName(); private final Set<String> dependentEntityNames = new HashSet<>();
final String rootEntityName = action.getSession().getFactory().getMetamodel().entityPersister( entityName ).getRootEntityName();
if ( entityType.isOneToOne() && OneToOneType.class.cast( entityType ).getForeignKeyDirection() == ForeignKeyDirection.TO_PARENT ) { public EntityInsertGroup(String entityName) {
if ( !entityType.isReferenceToPrimaryKey() ) { this.entityName = entityName;
batchIdentifier.getChildEntityNames().add( entityName );
}
if ( !rootEntityName.equals( entityName ) ) {
batchIdentifier.getChildEntityNames().add( rootEntityName );
}
}
else {
batchIdentifier.getParentEntityNames().add( entityName );
if ( !rootEntityName.equals( entityName ) ) {
batchIdentifier.getParentEntityNames().add( rootEntityName );
}
}
} }
else if ( type.isCollectionType() && value != null ) {
CollectionType collectionType = (CollectionType) type;
final SessionFactoryImplementor sessionFactory = ( (SessionImplementor) action.getSession() )
.getSessionFactory();
if ( collectionType.getElementType( sessionFactory ).isEntityType() ) {
String entityName = collectionType.getAssociatedEntityName( sessionFactory );
String rootEntityName = action.getSession().getFactory().getMetamodel().entityPersister( entityName ).getRootEntityName();
batchIdentifier.getChildEntityNames().add( entityName );
if ( !rootEntityName.equals( entityName ) ) {
batchIdentifier.getChildEntityNames().add( rootEntityName );
}
}
}
else if ( type.isComponentType() && value != null ) {
// Support recursive checks of composite type properties for associations and collections.
CompositeType compositeType = (CompositeType) type;
final SharedSessionContractImplementor session = action.getSession();
Object[] componentValues = compositeType.getPropertyValues( value, session );
for ( int j = 0; j < componentValues.length; ++j ) {
Type componentValueType = compositeType.getSubtypes()[j];
Object componentValue = componentValues[j];
addParentChildEntityNameByPropertyAndValue( action, batchIdentifier, componentValueType, componentValue );
}
}
}
private void addToBatch(BatchIdentifier batchIdentifier, AbstractEntityInsertAction action) { public void add(InsertInfo insertInfo) {
List<AbstractEntityInsertAction> actions = actionBatches.get( batchIdentifier ); insertInfos.add(insertInfo);
if (insertInfo.transitiveIncomingDependencies != null) {
if ( actions == null ) { for (InsertInfo dependency : insertInfo.transitiveIncomingDependencies) {
actions = new LinkedList<>(); dependentEntityNames.add(dependency.insertAction.getEntityName());
actionBatches.put( batchIdentifier, actions ); }
}
}
@Override
public String toString() {
return "EntityInsertGroup{" +
"entityName='" + entityName + '\'' +
'}';
} }
actions.add( action );
} }
} }

View File

@ -0,0 +1,174 @@
/*
* 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.insertordering;
import org.hibernate.cfg.Environment;
import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.FailureExpected;
import org.hibernate.testing.RequiresDialectFeature;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.Test;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
@TestForIssue(jiraKey = "HHH-16485")
@RequiresDialectFeature(DialectChecks.SupportsJdbcDriverProxying.class)
public class InsertOrderingCircularDependencyFalsePositiveTest extends BaseNonConfigCoreFunctionalTestCase {
private PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider(true, false);
@Override
protected Class[] getAnnotatedClasses() {
return new Class[]{
Wrapper.class,
Condition.class,
SimpleCondition.class,
Expression.class,
ConstantExpression.class,
Condition.class,
CompoundCondition.class,
};
}
@Override
protected void addSettings(Map settings) {
settings.put(Environment.ORDER_INSERTS, "true");
settings.put(Environment.ORDER_UPDATES, "true");
settings.put(Environment.STATEMENT_BATCH_SIZE, "50");
settings.put(
org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER,
connectionProvider
);
}
@Override
public void releaseResources() {
super.releaseResources();
connectionProvider.stop();
}
@Override
protected boolean rebuildSessionFactoryOnError() {
return false;
}
@Test
public void testBatching() throws SQLException {
doInHibernate(this::sessionFactory, session -> {
connectionProvider.clear();
// This should be persistable but currently reports that it might be circular
session.persist(Wrapper.create());
});
}
@Entity(name = "Wrapper")
public static class Wrapper {
@Id
private String id;
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private Condition condition;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<ConstantExpression> constantExpressions;
public Wrapper() {
}
public static Wrapper create() {
final Wrapper w = new Wrapper();
final CompoundCondition cc = new CompoundCondition();
final SimpleCondition c1 = new SimpleCondition();
final SimpleCondition c2 = new SimpleCondition();
final ConstantExpression e1 = new ConstantExpression();
final ConstantExpression e2 = new ConstantExpression();
final ConstantExpression e3 = new ConstantExpression();
final ConstantExpression e4 = new ConstantExpression();
final ConstantExpression e5 = new ConstantExpression();
w.id = "w";
w.condition = cc;
cc.id = "cc";
cc.first = c1;
cc.second = c2;
c1.id = "c1";
c1.left = e1;
c1.right = e2;
c2.id = "c2";
c2.left = e3;
c2.right = e4;
e1.id = "e1";
e1.value = "e1";
e2.id = "e2";
e2.value = "e2";
e3.id = "e3";
e3.value = "e3";
e4.id = "e4";
e4.value = "e4";
e5.id = "e5";
e5.value = "e5";
w.constantExpressions = new HashSet<>();
w.constantExpressions.add(e5);
return w;
}
}
@Entity(name = "Condition")
public static abstract class Condition {
@Id
protected String id;
public Condition() {
}
}
@Entity(name = "SimpleCondition")
public static class SimpleCondition extends Condition {
@OneToOne(cascade = CascadeType.ALL)
private Expression left;
@OneToOne(cascade = CascadeType.ALL)
private Expression right;
public SimpleCondition() {
}
}
@Entity(name = "Expression")
public static abstract class Expression {
@Id
protected String id;
protected Expression() {
}
}
@Entity(name = "ConstantExpression")
public static class ConstantExpression extends Expression {
private String value;
public ConstantExpression() {
}
}
@Entity(name = "CompoundCondition")
public static class CompoundCondition extends Condition {
@OneToOne(cascade = CascadeType.ALL)
protected Condition first;
@OneToOne(cascade = CascadeType.ALL)
protected Condition second;
public CompoundCondition() {
}
}
}

View File

@ -0,0 +1,188 @@
/*
* 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.insertordering;
import org.hibernate.cfg.Environment;
import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.RequiresDialectFeature;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.Test;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
@TestForIssue(jiraKey = "HHH-16485")
@RequiresDialectFeature(DialectChecks.SupportsJdbcDriverProxying.class)
public class InsertOrderingRootEntityNameDependencyTest extends BaseNonConfigCoreFunctionalTestCase {
private PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider(true, false);
@Override
protected Class[] getAnnotatedClasses() {
return new Class[]{
Wrapper.class,
Condition.class,
SimpleCondition.class,
Expression.class,
ConstantExpression.class,
Condition.class,
CompoundCondition.class,
};
}
@Override
protected void addSettings(Map settings) {
settings.put(Environment.ORDER_INSERTS, "true");
settings.put(Environment.ORDER_UPDATES, "true");
settings.put(Environment.STATEMENT_BATCH_SIZE, "50");
settings.put(
org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER,
connectionProvider
);
}
@Override
public void releaseResources() {
super.releaseResources();
connectionProvider.stop();
}
@Override
protected boolean rebuildSessionFactoryOnError() {
return false;
}
@Test
public void testBatching() throws SQLException {
doInHibernate(this::sessionFactory, session -> {
connectionProvider.clear();
session.persist(Wrapper.create());
});
}
@Entity(name = "Wrapper")
public static class Wrapper {
@Id
private String id;
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private Condition condition;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<ConstantExpression> constantExpressions;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<Condition> otherConditions;
public Wrapper() {
}
public static Wrapper create() {
final Wrapper w = new Wrapper();
final CompoundCondition cc = new CompoundCondition();
final SimpleCondition c1 = new SimpleCondition();
final SimpleCondition c2 = new SimpleCondition();
final SimpleCondition c3 = new SimpleCondition();
final ConstantExpression e1 = new ConstantExpression();
final ConstantExpression e2 = new ConstantExpression();
final ConstantExpression e3 = new ConstantExpression();
final ConstantExpression e4 = new ConstantExpression();
final ConstantExpression e5 = new ConstantExpression();
final ConstantExpression e6 = new ConstantExpression();
final ConstantExpression e7 = new ConstantExpression();
w.id = "w";
w.condition = cc;
cc.id = "cc";
cc.first = c1;
cc.second = c2;
c1.id = "c1";
c1.left = e1;
c1.right = e2;
c2.id = "c2";
c2.left = e3;
c2.right = e4;
c3.id = "c3";
c3.left = e6;
c3.right = e7;
e1.id = "e1";
e1.value = "e1";
e2.id = "e2";
e2.value = "e2";
e3.id = "e3";
e3.value = "e3";
e4.id = "e4";
e4.value = "e4";
e5.id = "e5";
e5.value = "e5";
e6.id = "e6";
e6.value = "e6";
e7.id = "e7";
e7.value = "e7";
w.constantExpressions = new HashSet<>();
w.constantExpressions.add(e5);
w.otherConditions = new HashSet<>();
w.otherConditions.add(c3);
return w;
}
}
@Entity(name = "Condition")
public static abstract class Condition {
@Id
protected String id;
public Condition() {
}
}
@Entity(name = "SimpleCondition")
public static class SimpleCondition extends Condition {
@OneToOne(cascade = CascadeType.ALL)
private Expression left;
@OneToOne(cascade = CascadeType.ALL)
private Expression right;
public SimpleCondition() {
}
}
@Entity(name = "Expression")
public static abstract class Expression {
@Id
protected String id;
protected Expression() {
}
}
@Entity(name = "ConstantExpression")
public static class ConstantExpression extends Expression {
private String value;
public ConstantExpression() {
}
}
@Entity(name = "CompoundCondition")
public static class CompoundCondition extends Condition {
@OneToOne(cascade = CascadeType.ALL)
protected Condition first;
@OneToOne(cascade = CascadeType.ALL)
protected Condition second;
public CompoundCondition() {
}
}
}