HHH-16485 Insert ordering doesn't consider root entity names
This commit is contained in:
parent
1399adce7d
commit
712b6c7668
|
@ -11,13 +11,13 @@ import java.io.ObjectInputStream;
|
|||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.BitSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
@ -45,7 +45,7 @@ import org.hibernate.cache.CacheException;
|
|||
import org.hibernate.engine.internal.NonNullableTransientDependencies;
|
||||
import org.hibernate.internal.CoreLogging;
|
||||
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.LazyInitializer;
|
||||
import org.hibernate.type.CollectionType;
|
||||
|
@ -128,7 +128,7 @@ public class ActionQueue {
|
|||
ExecutableList<AbstractEntityInsertAction> init(ActionQueue instance) {
|
||||
if ( instance.isOrderInsertsEnabled() ) {
|
||||
return instance.insertions = new ExecutableList<AbstractEntityInsertAction>(
|
||||
new InsertActionSorter()
|
||||
InsertActionSorter.INSTANCE
|
||||
);
|
||||
}
|
||||
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
|
||||
* certain that we do not circumvent this FK ordering to the extent of causing constraint violations.
|
||||
* <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>
|
||||
* NOTE: this class is not thread-safe.
|
||||
*
|
||||
* @author Jay Erb
|
||||
* 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.
|
||||
* 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> {
|
||||
/**
|
||||
|
@ -1034,106 +1036,140 @@ public class ActionQueue {
|
|||
*/
|
||||
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;
|
||||
private final String rootEntityName;
|
||||
|
||||
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 InsertInfo(AbstractEntityInsertAction insertAction, int index) {
|
||||
this.insertAction = insertAction;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public BatchIdentifier getParent() {
|
||||
return parent;
|
||||
public void buildDirectDependencies(IdentityHashMap<Object, InsertInfo> insertInfosByEntity) {
|
||||
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) {
|
||||
this.parent = parent;
|
||||
public void propagateChildDependencies() {
|
||||
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
|
||||
public boolean equals(Object o) {
|
||||
if ( this == o ) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if ( !( o instanceof BatchIdentifier ) ) {
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
BatchIdentifier that = (BatchIdentifier) o;
|
||||
return Objects.equals( entityName, that.entityName );
|
||||
|
||||
InsertInfo that = (InsertInfo) o;
|
||||
|
||||
return insertAction.equals(that.insertAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash( entityName );
|
||||
return insertAction.hashCode();
|
||||
}
|
||||
|
||||
String getEntityName() {
|
||||
return entityName;
|
||||
}
|
||||
|
||||
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() )
|
||||
);
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InsertInfo{" +
|
||||
"insertAction=" + insertAction +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
}
|
||||
|
||||
|
@ -1141,182 +1177,145 @@ public class ActionQueue {
|
|||
* Sort the insert actions.
|
||||
*/
|
||||
public void sort(List<AbstractEntityInsertAction> insertions) {
|
||||
// optimize the hash size to eliminate a rehash.
|
||||
this.latestBatches = new ArrayList<>( );
|
||||
this.actionBatches = new HashMap<>();
|
||||
|
||||
for ( AbstractEntityInsertAction action : insertions ) {
|
||||
BatchIdentifier batchIdentifier = new BatchIdentifier(
|
||||
action.getEntityName(),
|
||||
action.getSession()
|
||||
.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 );
|
||||
final int insertInfoCount = insertions.size();
|
||||
// Build up dependency metadata for insert actions
|
||||
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;
|
||||
}
|
||||
else {
|
||||
latestBatches.add( batchIdentifier );
|
||||
// First we must discover the direct dependencies
|
||||
for (int i = 0; i < insertInfoCount; i++) {
|
||||
insertInfos[i].buildDirectDependencies(insertInfosByEntity);
|
||||
}
|
||||
addParentChildEntityNames( action, batchIdentifier );
|
||||
addToBatch( batchIdentifier, action );
|
||||
// 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 );
|
||||
|
||||
final String entityName = insertInfo.insertAction.getPersister().getEntityName();
|
||||
EntityInsertGroup entityInsertGroup = insertInfosByEntityName.get(entityName);
|
||||
if (entityInsertGroup == null) {
|
||||
insertInfosByEntityName.put(entityName, entityInsertGroup = new EntityInsertGroup(entityName));
|
||||
}
|
||||
entityInsertGroup.add(insertInfo);
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
// we try to schedule entity groups over and over again, until we can't schedule any further
|
||||
} 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();
|
||||
|
||||
// Examine each entry in the batch list, and build the dependency graph.
|
||||
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 (InsertInfo insertInfo : insertInfos) {
|
||||
insertions.add(insertInfo.insertAction);
|
||||
}
|
||||
}
|
||||
|
||||
for ( int j = i + 1; j < latestBatches.size(); j++ ) {
|
||||
BatchIdentifier nextBatchIdentifier = latestBatches.get( j );
|
||||
|
||||
if ( nextBatchIdentifier.hasAnyParentEntityNames( batchIdentifier ) ) {
|
||||
nextBatchIdentifier.parent = batchIdentifier;
|
||||
private int schedule(InsertInfo[] insertInfos, List<InsertInfo> insertInfosToSchedule, int schedulePosition) {
|
||||
final InsertInfo[] newInsertInfos = new InsertInfo[insertInfos.length];
|
||||
// The bitset is there to quickly query if an index is already scheduled
|
||||
final BitSet bitSet = new BitSet(insertInfos.length);
|
||||
// Remember the smallest index of the insertInfosToSchedule to check if we actually need to reorder anything
|
||||
int smallestScheduledIndex = -1;
|
||||
// The biggestScheduledIndex is needed as upper bound for shifting elements that were replaced by insertInfosToSchedule
|
||||
int biggestScheduledIndex = -1;
|
||||
for (int i = 0; i < insertInfosToSchedule.size(); i++) {
|
||||
final int index = insertInfosToSchedule.get(i).index;
|
||||
bitSet.set(index);
|
||||
smallestScheduledIndex = Math.min(smallestScheduledIndex, index);
|
||||
biggestScheduledIndex = Math.max(biggestScheduledIndex, index);
|
||||
}
|
||||
if ( batchIdentifier.hasAnyChildEntityNames( nextBatchIdentifier ) ) {
|
||||
nextBatchIdentifier.parent = batchIdentifier;
|
||||
final int nextSchedulePosition = schedulePosition + insertInfosToSchedule.size();
|
||||
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;
|
||||
}
|
||||
|
||||
public static class EntityInsertGroup {
|
||||
private final String entityName;
|
||||
private final List<InsertInfo> insertInfos = new ArrayList<>();
|
||||
private final Set<String> dependentEntityNames = new HashSet<>();
|
||||
|
||||
public EntityInsertGroup(String entityName) {
|
||||
this.entityName = entityName;
|
||||
}
|
||||
|
||||
public void add(InsertInfo insertInfo) {
|
||||
insertInfos.add(insertInfo);
|
||||
if (insertInfo.transitiveIncomingDependencies != null) {
|
||||
for (InsertInfo dependency : insertInfo.transitiveIncomingDependencies) {
|
||||
dependentEntityNames.add(dependency.insertAction.getEntityName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EntityInsertGroup{" +
|
||||
"entityName='" + entityName + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add parent and child entity names so that we know how to rearrange dependencies
|
||||
*
|
||||
* @param action The action being sorted
|
||||
* @param batchIdentifier The batch identifier of the entity affected by the action
|
||||
*/
|
||||
private void addParentChildEntityNames(AbstractEntityInsertAction action, BatchIdentifier batchIdentifier) {
|
||||
Object[] propertyValues = action.getState();
|
||||
ClassMetadata classMetadata = action.getPersister().getClassMetadata();
|
||||
if ( classMetadata != null ) {
|
||||
Type[] propertyTypes = classMetadata.getPropertyTypes();
|
||||
|
||||
for ( int i = 0; i < propertyValues.length; i++ ) {
|
||||
Object value = propertyValues[i];
|
||||
Type type = propertyTypes[i];
|
||||
addParentChildEntityNameByPropertyAndValue( action, batchIdentifier, type, value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addParentChildEntityNameByPropertyAndValue(AbstractEntityInsertAction action, BatchIdentifier batchIdentifier, Type type, Object value) {
|
||||
if ( type.isEntityType() && value != null ) {
|
||||
final EntityType entityType = (EntityType) type;
|
||||
final String entityName = entityType.getName();
|
||||
final String rootEntityName = action.getSession().getFactory().getMetamodel().entityPersister( entityName ).getRootEntityName();
|
||||
|
||||
if ( entityType.isOneToOne() && OneToOneType.class.cast( entityType ).getForeignKeyDirection() == ForeignKeyDirection.TO_PARENT ) {
|
||||
if ( !entityType.isReferenceToPrimaryKey() ) {
|
||||
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) {
|
||||
List<AbstractEntityInsertAction> actions = actionBatches.get( batchIdentifier );
|
||||
|
||||
if ( actions == null ) {
|
||||
actions = new LinkedList<>();
|
||||
actionBatches.put( batchIdentifier, actions );
|
||||
}
|
||||
actions.add( action );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue