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 ce9f2b9c8af8..5766de8a17bf 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 @@ -17,7 +17,6 @@ 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; @@ -48,6 +47,7 @@ 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; @@ -1024,11 +1024,21 @@ private static class BatchIdentifier { private Set childEntityNames = new HashSet( ); + private BatchIdentifier parent; + BatchIdentifier(String entityName, String rootEntityName) { this.entityName = entityName; this.rootEntityName = rootEntityName; } + public BatchIdentifier getParent() { + return parent; + } + + public void setParent(BatchIdentifier parent) { + this.parent = parent; + } + @Override public boolean equals(Object o) { if ( this == o ) { @@ -1070,6 +1080,22 @@ boolean hasAnyParentEntityNames(BatchIdentifier batchIdentifier) { boolean hasAnyChildEntityNames(BatchIdentifier batchIdentifier) { return childEntityNames.contains( batchIdentifier.getEntityName() ); } + + /** + * Check if the 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 || + ( parent != null && parent.hasParent( batchIdentifier ) ) || + ( parentEntityNames.contains( batchIdentifier.getEntityName() ) ) + ); + } } // the mapping of entity names to their latest batch numbers. @@ -1114,42 +1140,71 @@ public void sort(List insertions) { } insertions.clear(); - // Examine each entry in the batch list, sorting them based on parent/child associations. + // 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 ); - // Iterate previous batches and make sure that parent types are before children - // Since the outer loop looks at each batch entry individually, we need to verify that any - // prior batches in the list are not considered children (or have a parent) of the current - // batch. If so, we reordered them. for ( int j = i - 1; j >= 0; j-- ) { BatchIdentifier prevBatchIdentifier = latestBatches.get( j ); if ( prevBatchIdentifier.hasAnyParentEntityNames( batchIdentifier ) ) { - latestBatches.remove( batchIdentifier ); - latestBatches.add( j, batchIdentifier ); + prevBatchIdentifier.parent = batchIdentifier; + } + if ( batchIdentifier.hasAnyChildEntityNames( prevBatchIdentifier ) ) { + prevBatchIdentifier.parent = batchIdentifier; } } - // 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 ); - final boolean nextBatchHasChild = nextBatchIdentifier.hasAnyChildEntityNames( batchIdentifier ); - final boolean batchHasChild = batchIdentifier.hasAnyChildEntityNames( nextBatchIdentifier ); - final boolean batchHasParent = batchIdentifier.hasAnyParentEntityNames( nextBatchIdentifier ); + if ( nextBatchIdentifier.hasAnyParentEntityNames( batchIdentifier ) ) { + nextBatchIdentifier.parent = batchIdentifier; + } + if ( batchIdentifier.hasAnyChildEntityNames( nextBatchIdentifier ) ) { + nextBatchIdentifier.parent = batchIdentifier; + } + } + } + + boolean sorted = false; + + long maxIterations = latestBatches.size() * 2; + 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 ); - // Take care of unidirectional @OneToOne associations but exclude bidirectional @ManyToMany - if ( ( nextBatchHasChild && !batchHasChild ) || batchHasParent ) { - 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. + // Now, rebuild the insertions list. There is a batch for each entry in the name list. for ( BatchIdentifier rootIdentifier : latestBatches ) { List batch = actionBatches.get( rootIdentifier ); insertions.addAll( batch ); @@ -1172,39 +1227,53 @@ private void addParentChildEntityNames(AbstractEntityInsertAction action, BatchI 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().getEntityPersister( 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 { - batchIdentifier.getParentEntityNames().add( entityName ); - if ( !rootEntityName.equals( entityName ) ) { - batchIdentifier.getParentEntityNames().add( rootEntityName ); - } - } + 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().getEntityPersister( 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 = action.getSession().getFactory(); - if ( collectionType.getElementType( sessionFactory ).isEntityType() ) { - String entityName = collectionType.getAssociatedEntityName( sessionFactory ); - String rootEntityName = action.getSession().getFactory().getEntityPersister( 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 = action.getSession().getFactory(); + if ( collectionType.getElementType( sessionFactory ).isEntityType() ) { + String entityName = collectionType.getAssociatedEntityName( sessionFactory ); + String rootEntityName = action.getSession().getFactory().getEntityPersister( 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 SessionImplementor 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/InsertOrderingWithBidirectionalOneToManyFlushProblem.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalOneToManyFlushProblem.java new file mode 100644 index 000000000000..1ae79616e5c6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalOneToManyFlushProblem.java @@ -0,0 +1,243 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.SequenceGenerator; + +import org.hibernate.Session; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.junit.Test; + +import static javax.persistence.CascadeType.PERSIST; +import static javax.persistence.GenerationType.SEQUENCE; +import static org.hibernate.cfg.AvailableSettings.ORDER_INSERTS; +import static org.hibernate.cfg.AvailableSettings.STATEMENT_BATCH_SIZE; + +@TestForIssue(jiraKey = "HHH-12074") +public class InsertOrderingWithBidirectionalOneToManyFlushProblem + extends BaseNonConfigCoreFunctionalTestCase { + + @Test + public void testBatchingWithFlush() { + Session session = openSession(); + session.getTransaction().begin(); + { + TopEntity top1 = new TopEntity(); + + session.persist( top1 ); + + // InsertActionSorter#sort is invoked during this flush. + // + // input: [top1] + // output: [top1] + session.flush(); + + MiddleEntity middle1 = new MiddleEntity(); + + middle1.addBottom( new BottomEntity() ); + top1.addMiddle( middle1 ); + session.persist( middle1 ); + + TopEntity top2 = new TopEntity(); + + session.persist( top2 ); + + MiddleEntity middle2 = new MiddleEntity(); + + middle2.addBottom( new BottomEntity() ); + top2.addMiddle( middle2 ); + session.persist( middle2 ); + + // InsertActionSorter#sort is invoked during this flush + // + // input: [middle1,bottom1,top2,middle2,bottom2] output: + // [middle1,middle2,bottom1,bottom2,top2] + // + // This ordering causes a constraint violation during the flush + // when the attempt to insert middle2 before top2 is made. + // + // correct ordering is: [top2,middle1,middle2,bottom1,bottom2] + } + session.getTransaction().commit(); + session.close(); + } + + @Test + @TestForIssue(jiraKey = "HHH-12086") + public void testBatchingWithFlush2() { + Session session = openSession(); + session.getTransaction().begin(); + { + TopEntity top1 = new TopEntity(); + + session.persist( top1 ); + + // InsertActionSorter#sort is invoked during this flush. + // + // input: [top1] + // output: [top1] + session.flush(); + + MiddleEntity middle1 = new MiddleEntity(); + + middle1.addBottom( new BottomEntity() ); + middle1.addBottom2( new BottomEntity2() ); + top1.addMiddle( middle1 ); + session.persist( middle1 ); + + TopEntity top2 = new TopEntity(); + + session.persist( top2 ); + + MiddleEntity middle2 = new MiddleEntity(); + + middle2.addBottom( new BottomEntity() ); + middle2.addBottom2( new BottomEntity2() ); + top2.addMiddle( middle2 ); + session.persist( middle2 ); + + session.persist(new TopEntity()); + + // InsertActionSorter#sort is invoked during this flush + // + // input: [middle1,bottom1,top2,middle2,bottom2] output: + // [middle1,middle2,bottom1,bottom2,top2] + // + // This ordering causes a constraint violation during the flush + // when the attempt to insert middle2 before top2 is made. + // + // correct ordering is: [top2,middle1,middle2,bottom1,bottom2] + } + session.getTransaction().commit(); + session.close(); + } + + @Override + protected void addSettings(Map settings) { + settings.put( ORDER_INSERTS, "true" ); + settings.put( STATEMENT_BATCH_SIZE, "10" ); + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { TopEntity.class, MiddleEntity.class, BottomEntity.class, BottomEntity2.class }; + } + + @Entity(name = "BottomEntity") + public static class BottomEntity { + + @Column(nullable = false) + @GeneratedValue( + strategy = SEQUENCE, + generator = "ID" + ) + @Id + @SequenceGenerator( + name = "ID", + sequenceName = "BOTTOM_SEQ" + ) + private Long id; + + @ManyToOne(optional = false) + private MiddleEntity middle; + } + + @Entity(name = "BottomEntity2") + public static class BottomEntity2 { + + @Column(nullable = false) + @GeneratedValue( + strategy = SEQUENCE, + generator = "ID" + ) + @Id + @SequenceGenerator( + name = "ID", + sequenceName = "BOTTOM2_SEQ" + ) + private Long id; + + @ManyToOne(optional = false) + private MiddleEntity middle; + } + + @Entity(name = "MiddleEntity") + public static class MiddleEntity { + + @Column(nullable = false) + @GeneratedValue( + strategy = SEQUENCE, + generator = "ID" + ) + @Id + @SequenceGenerator( + name = "ID", + sequenceName = "MIDDLE_SEQ" + ) + private Long id; + + @ManyToOne(optional = false) + private TopEntity top; + + @OneToMany( + cascade = PERSIST, + mappedBy = "middle" + ) + private List bottoms = new ArrayList<>(); + + @OneToMany( + cascade = PERSIST, + mappedBy = "middle" + ) + private List bottom2s = new ArrayList<>(); + + private void addBottom(BottomEntity bottom) { + bottoms.add( bottom ); + bottom.middle = this; + } + + private void addBottom2(BottomEntity2 bottom2) { + bottom2s.add( bottom2 ); + bottom2.middle = this; + } + } + + @Entity(name = "TopEntity") + public static class TopEntity { + + @Column(nullable = false) + @GeneratedValue( + strategy = SEQUENCE, + generator = "ID" + ) + @Id + @SequenceGenerator( + name = "ID", + sequenceName = "TOP_SEQ" + ) + private Long id; + + @OneToMany(mappedBy = "top") + private List middles = new ArrayList<>(); + + void addMiddle(MiddleEntity middle) { + middles.add( middle ); + middle.top = this; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalOneToOneFlushProblem.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalOneToOneFlushProblem.java new file mode 100644 index 000000000000..6b813a53ed21 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalOneToOneFlushProblem.java @@ -0,0 +1,142 @@ +/* + * 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 org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.junit.Test; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.hibernate.Session; + +import static org.hibernate.cfg.AvailableSettings.ORDER_INSERTS; +import static org.hibernate.cfg.AvailableSettings.STATEMENT_BATCH_SIZE; + +@TestForIssue(jiraKey = "HHH-12105") +public class InsertOrderingWithBidirectionalOneToOneFlushProblem + extends BaseNonConfigCoreFunctionalTestCase { + + @Test + public void testInsertSortingWithFlushPersistLeftBeforeRight() { + Session session = openSession(); + session.getTransaction().begin(); + { + TopEntity top1 = new TopEntity(); + + session.persist(top1); + session.flush(); + + LeftEntity left = new LeftEntity(); + RightEntity right = new RightEntity(); + TopEntity top2 = new TopEntity(); + + top1.lefts.add(left); + left.top = top1; + top1.rights.add(right); + right.top = top1; + + // This one-to-one triggers the problem + right.left = left; + + // If you persist right before left the problem goes away + session.persist( left ); + session.persist( right ); + session.persist( top2 ); + } + session.getTransaction().commit(); + session.close(); + } + + @Test + public void testInsertSortingWithFlushPersistRightBeforeLeft() { + Session session = openSession(); + session.getTransaction().begin(); + { + TopEntity top1 = new TopEntity(); + + session.persist(top1); + session.flush(); + + LeftEntity left = new LeftEntity(); + RightEntity right = new RightEntity(); + TopEntity top2 = new TopEntity(); + + top1.lefts.add(left); + left.top = top1; + top1.rights.add(right); + right.top = top1; + + // This one-to-one triggers the problem + right.left = left; + + // If you persist right before left the problem goes away + session.persist(right); + session.persist(left); + session.persist(top2); + } + session.getTransaction().commit(); + session.close(); + } + + @Override + protected void addSettings(Map settings) { + settings.put( ORDER_INSERTS, "true" ); + settings.put( STATEMENT_BATCH_SIZE, "10" ); + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{ + LeftEntity.class, RightEntity.class, TopEntity.class, + }; + } + + @Entity(name = "LeftEntity") + public static class LeftEntity { + @GeneratedValue + @Id + private Long id; + + @ManyToOne + private TopEntity top; + } + + @Entity(name = "RightEntity") + public static class RightEntity { + @GeneratedValue + @Id + private Long id; + + @ManyToOne + private TopEntity top; + + @OneToOne + private LeftEntity left; + } + + @Entity(name = "TopEntity") + public static class TopEntity { + @GeneratedValue + @Id + private Long id; + + @OneToMany(mappedBy = "top") + private List rights = new ArrayList<>(); + + @OneToMany(mappedBy = "top") + private List lefts = new ArrayList<>(); + } +} 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 000000000000..42b33630cd89 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithCompositeTypeAssociation.java @@ -0,0 +1,157 @@ +/* + * 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.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.Session; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; + +import org.junit.Test; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; + +/** + * + * @author Chris Cranford + */ +@TestForIssue(jiraKey = "HHH-12355") +public class InsertOrderingWithCompositeTypeAssociation extends BaseCoreFunctionalTestCase { + + @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 configure(Configuration cfg) { + super.configure( cfg ); + cfg.setProperty( AvailableSettings.ORDER_INSERTS, "true" ); + cfg.setProperty( 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. + Session session = openSession(); + session.getTransaction().begin(); + { + 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" ) ) ); + + session.persist( bookNoComment ); + session.persist( bookWithComment ); + } + session.getTransaction().commit(); + session.close(); + } +}