From 493c9681414862efdb4ad88d50f44bd3678f89be Mon Sep 17 00:00:00 2001 From: Chris Cranford Date: Tue, 6 Mar 2018 10:40:04 -0500 Subject: [PATCH] HHH-12355 - Fix ordered insert failures with composite types having associations. --- .../org/hibernate/engine/spi/ActionQueue.java | 73 +++++---- ...tOrderingWithCompositeTypeAssociation.java | 153 ++++++++++++++++++ 2 files changed, 197 insertions(+), 29 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithCompositeTypeAssociation.java diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java index 9dd45e4119..270de8e991 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java @@ -49,6 +49,7 @@ import org.hibernate.metadata.ClassMetadata; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.type.CollectionType; +import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.OneToOneType; @@ -1240,40 +1241,54 @@ public class ActionQueue { for ( int i = 0; i < propertyValues.length; i++ ) { Object value = propertyValues[i]; Type type = propertyTypes[i]; - if ( type.isEntityType() && value != null ) { - EntityType entityType = (EntityType) type; - String entityName = entityType.getName(); - String rootEntityName = action.getSession().getFactory().getMetamodel().entityPersister( entityName ).getRootEntityName(); + addParentChildEntityNameByPropertyAndValue( action, batchIdentifier, type, value ); + } + } + } - if ( entityType.isOneToOne() && - OneToOneType.class.cast( entityType ).getForeignKeyDirection() == ForeignKeyDirection.TO_PARENT ) { - 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 ); - } - } + 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 ) { + batchIdentifier.getChildEntityNames().add( entityName ); + if ( !rootEntityName.equals( entityName ) ) { + batchIdentifier.getChildEntityNames().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 { + 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) { diff --git a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithCompositeTypeAssociation.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithCompositeTypeAssociation.java new file mode 100644 index 0000000000..d14e1edaeb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithCompositeTypeAssociation.java @@ -0,0 +1,153 @@ +/* + * 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 . + */ +package org.hibernate.test.insertordering; + +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; +import org.junit.Test; + +import org.hibernate.testing.TestForIssue; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; + +/** + * + * @author Chris Cranford + */ +@TestForIssue(jiraKey = "HHH-12355") +public class InsertOrderingWithCompositeTypeAssociation extends BaseEntityManagerFunctionalTestCase { + + @Entity(name = "Book") + public static class Book { + @Id + private String id; + @Embedded + private IntermediateObject intermediateObject; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public IntermediateObject getIntermediateObject() { + return intermediateObject; + } + + public void setIntermediateObject(IntermediateObject intermediateObject) { + this.intermediateObject = intermediateObject; + } + } + + @Entity(name = "Comment") + public static class Comment { + @Id + private String id; + @Column(length = 256) + private String comment; + + Comment() { + + } + + Comment(String comment) { + this.id = UUID.randomUUID().toString(); + this.comment = comment; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + } + + @Embeddable + public static class IntermediateObject { + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, targetEntity = Comment.class) + @JoinColumn(name = "comment_comment", foreignKey = @ForeignKey(name = "id" ) ) + private Comment comment; + + IntermediateObject() { + + } + + IntermediateObject(Comment comment) { + this.comment = comment; + } + + public Comment getComment() { + return comment; + } + + public void setComment(Comment comment) { + this.comment = comment; + } + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Book.class, Comment.class }; + } + + @Override + protected void addConfigOptions(Map options) { + super.addConfigOptions( options ); + options.put( AvailableSettings.ORDER_INSERTS, "true" ); + options.put( AvailableSettings.ORDER_UPDATES, "true" ); + } + + @Test + public void testOrderedInsertSupport() { + // Without the fix, this transaction would eventually fail with a foreign-key constraint violation. + // + // The bookNoComment entity would be persisted just fine; however the bookWithComment would fail + // because it would lead to inserting the Book entities first rather than making sure that the + // Comment would be inserted first. + // + // The associated ActionQueue fix makes sure that regardless of the order of operations, the Comment + // entity associated in the embeddable takes insert priority over the parent Book entity. + doInJPA( this::entityManagerFactory, entityManager -> { + Book bookNoComment = new Book(); + bookNoComment.setId( UUID.randomUUID().toString() ); + + Book bookWithComment = new Book(); + bookWithComment.setId( UUID.randomUUID().toString() ); + bookWithComment.setIntermediateObject( new IntermediateObject( new Comment( "This is a comment" ) ) ); + + entityManager.persist( bookNoComment ); + entityManager.persist( bookWithComment ); + } ); + } +}