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 f2f9b47a3a..efcb900889 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 @@ -10,12 +10,14 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; 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; @@ -44,6 +46,8 @@ import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; +import org.hibernate.type.CollectionType; +import org.hibernate.type.EntityType; import org.hibernate.type.Type; /** @@ -1008,12 +1012,56 @@ public class ActionQueue { */ public static final InsertActionSorter INSTANCE = new InsertActionSorter(); + private static class BatchIdentifier { + + private final String entityName; + + private Set parentEntityNames = new HashSet<>( ); + + private Set childEntityNames = new HashSet<>( ); + + public BatchIdentifier( + String entityName) { + this.entityName = entityName; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( !( o instanceof BatchIdentifier ) ) { + return false; + } + BatchIdentifier that = (BatchIdentifier) o; + return Objects.equals( entityName, that.entityName ); + } + + @Override + public int hashCode() { + return Objects.hash( entityName ); + } + + public String getEntityName() { + return entityName; + } + + public Set getParentEntityNames() { + return parentEntityNames; + } + + public Set getChildEntityNames() { + return childEntityNames; + } + } + // the mapping of entity names to their latest batch numbers. - private Map latestBatches; - private Map entityBatchNumber; + private List latestBatches; + + private Map entityBatchIdentifier; // the map of batch numbers to EntityInsertAction lists - private Map> actionBatches; + private Map> actionBatches; public InsertActionSorter() { } @@ -1023,64 +1071,68 @@ public class ActionQueue { */ public void sort(List insertions) { // optimize the hash size to eliminate a rehash. - this.latestBatches = new HashMap(); - this.entityBatchNumber = new HashMap( insertions.size() + 1, 1.0f ); - this.actionBatches = new HashMap>(); + this.latestBatches = new ArrayList<>( ); + this.entityBatchIdentifier = new HashMap<>( insertions.size() + 1, 1.0f ); + this.actionBatches = new HashMap<>(); - // the list of entity names that indicate the batch number for ( AbstractEntityInsertAction action : insertions ) { - // remove the current element from insertions. It will be added back later. - String entityName = action.getEntityName(); + BatchIdentifier batchIdentifier = new BatchIdentifier( action.getEntityName() ); // the entity associated with the current action. Object currentEntity = action.getInstance(); + int index = latestBatches.indexOf( batchIdentifier ); - Integer batchNumber; - if ( latestBatches.containsKey( entityName ) ) { - // There is already an existing batch for this type of entity. - // Check to see if the latest batch is acceptable. - batchNumber = findBatchNumber( action, entityName ); + if ( index != -1 ) { + batchIdentifier = latestBatches.get( index ); } else { - // add an entry for this type of entity. - // we can be assured that all referenced entities have already - // been processed, - // so specify that this entity is with the latest batch. - // doing the batch number beforeQuery adding the name to the list is - // a faster way to get an accurate number. - - batchNumber = actionBatches.size(); - latestBatches.put( entityName, batchNumber ); + latestBatches.add( batchIdentifier ); } - entityBatchNumber.put( currentEntity, batchNumber ); - addToBatch( batchNumber, action ); + addParentChildEntityNames( action, batchIdentifier ); + entityBatchIdentifier.put( currentEntity, batchIdentifier ); + addToBatch(batchIdentifier, action); } insertions.clear(); + for ( int i = 0; i < latestBatches.size(); i++ ) { + BatchIdentifier batchIdentifier = latestBatches.get( i ); + String entityName = batchIdentifier.getEntityName(); + + //Make sure that child entries are not before parents + for ( int j = i - 1; j >= 0; j-- ) { + BatchIdentifier prevBatchIdentifier = latestBatches.get( j ); + if(prevBatchIdentifier.getParentEntityNames().contains( entityName )) { + latestBatches.remove( i ); + latestBatches.add( j, batchIdentifier ); + } + } + + //Make sure that parent entries are not after children + for ( int j = i + 1; j < latestBatches.size(); j++ ) { + BatchIdentifier nextBatchIdentifier = latestBatches.get( j ); + //Take care of unidirectional @OneToOne associations but exclude bidirectional @ManyToMany + if(nextBatchIdentifier.getChildEntityNames().contains( entityName ) && + !batchIdentifier.getChildEntityNames().contains( nextBatchIdentifier.getEntityName() )) { + latestBatches.remove( i ); + latestBatches.add( j, batchIdentifier ); + } + } + } + // now rebuild the insertions list. There is a batch for each entry in the name list. - for ( int i = 0; i < actionBatches.size(); i++ ) { - List batch = actionBatches.get( i ); + for ( BatchIdentifier rootIdentifier : latestBatches ) { + List batch = actionBatches.get( rootIdentifier ); insertions.addAll( batch ); } } /** - * Finds an acceptable batch for this entity to be a member as part of the {@link InsertActionSorter} + * Add parent and child entity names so that we know how to rearrange dependencies * * @param action The action being sorted - * @param entityName The name of the entity affected by the action - * @return An appropriate batch number; todo document this process better + * @param batchIdentifier The batch identifier of the entity affected by the action */ - private Integer findBatchNumber(AbstractEntityInsertAction action, String entityName) { - // loop through all the associated entities and make sure they have been - // processed beforeQuery the latest - // batch associated with this entity type. - - // the current batch number is the latest batch for this entity type. - Integer latestBatchNumberForType = latestBatches.get( entityName ); - - // loop through all the associations of the current entity and make sure that they are processed - // beforeQuery the current batch number + private void addParentChildEntityNames(AbstractEntityInsertAction action, BatchIdentifier batchIdentifier) { Object[] propertyValues = action.getState(); Type[] propertyTypes = action.getPersister().getClassMetadata().getPropertyTypes(); @@ -1088,35 +1140,30 @@ public class ActionQueue { Object value = propertyValues[i]; Type type = propertyTypes[i]; if ( type.isEntityType() && value != null ) { - // find the batch number associated with the current association, if any. - Integer associationBatchNumber = entityBatchNumber.get( value ); - if ( associationBatchNumber != null && associationBatchNumber.compareTo( latestBatchNumberForType ) > 0 ) { - // create a new batch for this type. The batch number is the number of current batches. - latestBatchNumberForType = actionBatches.size(); - latestBatches.put( entityName, latestBatchNumberForType ); - // since this entity will now be processed in the latest possible batch, - // we can be assured that it will come afterQuery all other associations, - // there's not need to continue checking. - break; - } + EntityType entityType = (EntityType) type; + String entityName = entityType.getName(); + batchIdentifier.getParentEntityNames().add( entityName ); + } + else if ( type.isCollectionType() && value != null ) { + CollectionType collectionType = (CollectionType) type; + String entityName = collectionType.getAssociatedEntityName( ( (SessionImplementor) action.getSession() ).getSessionFactory() ); + batchIdentifier.getChildEntityNames().add( entityName ); } } - return latestBatchNumberForType; } - private void addToBatch(Integer batchNumber, AbstractEntityInsertAction action) { - List actions = actionBatches.get( batchNumber ); + private void addToBatch(BatchIdentifier batchIdentifier, AbstractEntityInsertAction action) { + List actions = actionBatches.get( batchIdentifier ); if ( actions == null ) { - actions = new LinkedList(); - actionBatches.put( batchNumber, actions ); + actions = new LinkedList<>(); + actionBatches.put( batchIdentifier, actions ); } actions.add( action ); } } - private static abstract class ListProvider { abstract ExecutableList get(ActionQueue instance); abstract ExecutableList init(ActionQueue instance); diff --git a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalManyToMany.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalManyToMany.java new file mode 100644 index 0000000000..2b51659a79 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalManyToMany.java @@ -0,0 +1,127 @@ +/* + * 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.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.SequenceGenerator; + +import org.hibernate.cfg.Environment; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue(jiraKey = "HHH-9864") +public class InsertOrderingWithBidirectionalManyToMany + extends BaseNonConfigCoreFunctionalTestCase { + + private PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider(); + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Address.class, Person.class }; + } + + @Override + protected void addSettings(Map settings) { + settings.put( Environment.ORDER_INSERTS, "true" ); + settings.put( Environment.STATEMENT_BATCH_SIZE, "10" ); + settings.put( + org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ); + } + + @Override + public void releaseResources() { + super.releaseResources(); + connectionProvider.stop(); + } + + @Test + public void testBatching() throws SQLException { + doInHibernate( this::sessionFactory, session -> { + Person father = new Person(); + Person mother = new Person(); + Person son = new Person(); + Person daughter = new Person(); + + Address home = new Address(); + Address office = new Address(); + + home.addPerson( father ); + home.addPerson( mother ); + home.addPerson( son ); + home.addPerson( daughter ); + + office.addPerson( father ); + office.addPerson( mother ); + + session.persist( home ); + session.persist( office ); + + connectionProvider.clear(); + } ); + + assertEquals( 3, connectionProvider.getPreparedStatements().size() ); + PreparedStatement addressPreparedStatement = connectionProvider.getPreparedStatement( + "insert into Address (ID) values (?)" ); + verify( addressPreparedStatement, times( 2 ) ).addBatch(); + PreparedStatement personPreparedStatement = connectionProvider.getPreparedStatement( + "insert into Person (ID) values (?)" ); + verify( personPreparedStatement, times( 4 ) ).addBatch(); + } + + @Entity(name = "Address") + public static class Address { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + + @ManyToMany(mappedBy = "addresses", cascade = CascadeType.PERSIST) + private List persons = new ArrayList<>(); + + public void addPerson(Person person) { + persons.add( person ); + person.addresses.add( this ); + } + } + + @Entity(name = "Person") + public static class Person { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + + @ManyToMany + private List
addresses = new ArrayList<>(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalOneToMany.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalOneToMany.java new file mode 100644 index 0000000000..3e8f9fe726 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithBidirectionalOneToMany.java @@ -0,0 +1,126 @@ +/* + * 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.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.SequenceGenerator; + +import org.hibernate.cfg.Environment; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue(jiraKey = "HHH-9864") +public class InsertOrderingWithBidirectionalOneToMany + extends BaseNonConfigCoreFunctionalTestCase { + + private PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider(); + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Address.class, Person.class }; + } + + @Override + protected void addSettings(Map settings) { + settings.put( Environment.ORDER_INSERTS, "true" ); + settings.put( Environment.STATEMENT_BATCH_SIZE, "10" ); + settings.put( + org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ); + } + + @Override + public void releaseResources() { + super.releaseResources(); + connectionProvider.stop(); + } + + @Test + public void testBatching() throws SQLException { + doInHibernate( this::sessionFactory, session -> { + Person father = new Person(); + Person mother = new Person(); + Person son = new Person(); + Person daughter = new Person(); + + Address home = new Address(); + Address office = new Address(); + + home.addPerson( father ); + home.addPerson( mother ); + home.addPerson( son ); + home.addPerson( daughter ); + + office.addPerson( father ); + office.addPerson( mother ); + + session.persist( home ); + session.persist( office ); + + connectionProvider.clear(); + } ); + + PreparedStatement addressPreparedStatement = connectionProvider.getPreparedStatement( + "insert into Address (ID) values (?)" ); + verify( addressPreparedStatement, times( 2 ) ).addBatch(); + PreparedStatement personPreparedStatement = connectionProvider.getPreparedStatement( + "insert into Person (address_ID, ID) values (?, ?)" ); + verify( personPreparedStatement, times( 4 ) ).addBatch(); + } + + @Entity(name = "Address") + public static class Address { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + + @OneToMany(mappedBy = "address", cascade = CascadeType.PERSIST) + private List persons = new ArrayList<>(); + + public void addPerson(Person person) { + persons.add( person ); + person.address = this; + } + } + + @Entity(name = "Person") + public static class Person { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + + @ManyToOne + private Address address; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithInheritance.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithInheritance.java deleted file mode 100644 index e8eebd4b4f..0000000000 --- a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithInheritance.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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.sql.PreparedStatement; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.persistence.Access; -import javax.persistence.AccessType; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.DiscriminatorColumn; -import javax.persistence.DiscriminatorType; -import javax.persistence.DiscriminatorValue; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Inheritance; -import javax.persistence.InheritanceType; -import javax.persistence.JoinColumn; -import javax.persistence.OneToMany; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; - -import org.hibernate.Session; -import org.hibernate.annotations.BatchSize; -import org.hibernate.cfg.Environment; -import org.hibernate.engine.jdbc.batch.internal.BatchBuilderImpl; -import org.hibernate.engine.jdbc.batch.internal.BatchBuilderInitiator; -import org.hibernate.engine.jdbc.batch.internal.BatchingBatch; -import org.hibernate.engine.jdbc.batch.spi.Batch; -import org.hibernate.engine.jdbc.batch.spi.BatchKey; -import org.hibernate.engine.jdbc.spi.JdbcCoordinator; - -import org.hibernate.testing.FailureExpected; -import org.hibernate.testing.TestForIssue; -import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * @author Steve Ebersole - */ -@TestForIssue( jiraKey = "HHH-9864" ) -public class InsertOrderingWithInheritance extends BaseNonConfigCoreFunctionalTestCase { - @Test - @FailureExpected( jiraKey = "HHH-9864" ) - public void testBatchOrdering() { - Session session = openSession(); - session.getTransaction().begin(); - - // First object with dependent object (address) - final Person person = new Person(); - person.addAddress(new Address()); - session.persist(person); - - // Derived Object with dependent object (address) - final SpecialPerson specialPerson = new SpecialPerson(); - specialPerson.addAddress(new Address()); - session.persist( specialPerson ); - - session.getTransaction().commit(); - session.close(); - - cleanupData(); - } - - @Test - @FailureExpected( jiraKey = "HHH-9864" ) - public void testBatchingAmongstSubClasses() { - StatsBatch.reset(); - Session session = openSession(); - session.getTransaction().begin(); - int iterations = 12; - for ( int i = 0; i < iterations; i++ ) { - final Person person = new Person(); - person.addAddress( new Address() ); - session.persist( person ); - - final SpecialPerson specialPerson = new SpecialPerson(); - specialPerson.addAddress(new Address()); - session.persist( specialPerson ); - } - StatsBatch.reset(); - session.getTransaction().commit(); - session.close(); - - // 2 batches of Person - // 2 batches of SpecialPerson - // 2 batches of Address - assertEquals( 6, StatsBatch.batchSizes.size() ); - - cleanupData(); - } - - private void cleanupData() { - Session session = openSession(); - session.getTransaction().begin(); - session.createQuery( "delete Address" ).executeUpdate(); - session.createQuery( "delete Person" ).executeUpdate(); - session.getTransaction().commit(); - session.close(); - } - - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { Address.class, Person.class, SpecialPerson.class }; - } - - @Override - protected void addSettings(Map settings) { - super.addSettings( settings ); - settings.put( Environment.ORDER_INSERTS, "true" ); - settings.put( Environment.STATEMENT_BATCH_SIZE, "10" ); - settings.put( BatchBuilderInitiator.BUILDER, StatsBatchBuilder.class.getName() ); - } - - @Entity(name = "Address") - @Table(name = "ADDRESS") - @Access(AccessType.FIELD) - public static class Address { - @Id - @Column(name = "ID", nullable = false) - @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") - @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") - private Long id; - } - - @Entity( name = "Person" ) - @Access(AccessType.FIELD) - @Table(name = "PERSON") - @Inheritance(strategy = InheritanceType.SINGLE_TABLE) - @DiscriminatorColumn(name = "CLASSINDICATOR", discriminatorType = DiscriminatorType.INTEGER) - @DiscriminatorValue("1") - public static class Person { - @Id - @Column(name = "ID", nullable = false) - @SequenceGenerator(name = "ID", sequenceName = "PERSON_SEQ") - @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") - private Long id; - - @OneToMany(orphanRemoval = true, cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) - @JoinColumn(name = "PERSONID", referencedColumnName = "ID", nullable = false, updatable = false) - @BatchSize(size = 100) - private Set
addresses = new HashSet
(); - - public void addAddress(Address address) { - this.addresses.add(address); - } - - } - - @Entity - @Access(AccessType.FIELD) - @DiscriminatorValue("2") - public static class SpecialPerson extends Person { - @Column(name = "special") - private String special; - } - - public static class Counter { - public int count = 0; - } - - public static class StatsBatch extends BatchingBatch { - private static String batchSQL; - private static List batchSizes = new ArrayList(); - private static int currentBatch = -1; - - public StatsBatch(BatchKey key, JdbcCoordinator jdbcCoordinator, int jdbcBatchSize) { - super( key, jdbcCoordinator, jdbcBatchSize ); - } - - static void reset() { - batchSizes = new ArrayList(); - currentBatch = -1; - batchSQL = null; - } - - @Override - public PreparedStatement getBatchStatement(String sql, boolean callable) { - if ( batchSQL == null || ! batchSQL.equals( sql ) ) { - currentBatch++; - batchSQL = sql; - batchSizes.add( currentBatch, new Counter() ); - } - return super.getBatchStatement( sql, callable ); - } - - @Override - public void addToBatch() { - Counter counter = ( Counter ) batchSizes.get( currentBatch ); - counter.count++; - super.addToBatch(); - } - } - - public static class StatsBatchBuilder extends BatchBuilderImpl { - private int jdbcBatchSize; - - @Override - public void setJdbcBatchSize(int jdbcBatchSize) { - this.jdbcBatchSize = jdbcBatchSize; - } - - @Override - public Batch buildBatch(BatchKey key, JdbcCoordinator jdbcCoordinator) { - return new StatsBatch( key, jdbcCoordinator, jdbcBatchSize ); - } - } -} diff --git a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithJoinedTableInheritance.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithJoinedTableInheritance.java new file mode 100644 index 0000000000..aafd95d680 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithJoinedTableInheritance.java @@ -0,0 +1,154 @@ +/* + * 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.HashSet; +import java.util.Map; +import java.util.Set; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.DiscriminatorColumn; +import javax.persistence.DiscriminatorType; +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.Environment; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue(jiraKey = "HHH-9864") +public class InsertOrderingWithJoinedTableInheritance + extends BaseNonConfigCoreFunctionalTestCase { + + private PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider(); + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Address.class, Person.class, SpecialPerson.class }; + } + + @Override + protected void addSettings(Map settings) { + settings.put( Environment.ORDER_INSERTS, "true" ); + settings.put( Environment.STATEMENT_BATCH_SIZE, "10" ); + settings.put( + org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ); + } + + @Override + public void releaseResources() { + super.releaseResources(); + connectionProvider.stop(); + } + + @Test + public void testBatchOrdering() { + doInHibernate( this::sessionFactory, session -> { + final Person person = new Person(); + person.addAddress( new Address() ); + session.persist( person ); + + // Derived Object with dependent object (address) + final SpecialPerson specialPerson = new SpecialPerson(); + specialPerson.addAddress( new Address() ); + session.persist( specialPerson ); + } ); + } + + @Test + public void testBatchingAmongstSubClasses() { + doInHibernate( this::sessionFactory, session -> { + int iterations = 12; + for ( int i = 0; i < iterations; i++ ) { + final Person person = new Person(); + person.addAddress( new Address() ); + session.persist( person ); + + final SpecialPerson specialPerson = new SpecialPerson(); + specialPerson.addAddress( new Address() ); + session.persist( specialPerson ); + } + connectionProvider.clear(); + } ); + + assertEquals( 26, connectionProvider.getPreparedStatements().size() ); + } + + @Override + protected boolean isCleanupTestDataRequired() { + return true; + } + + @Entity(name = "Address") + @Table(name = "ADDRESS") + @Access(AccessType.FIELD) + public static class Address { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + } + + @Entity(name = "Person") + @Access(AccessType.FIELD) + @Table(name = "PERSON") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn(name = "CLASSINDICATOR", discriminatorType = DiscriminatorType.INTEGER) + @DiscriminatorValue("1") + public static class Person { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "PERSON_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + + @OneToMany(orphanRemoval = true, cascade = { + CascadeType.PERSIST, + CascadeType.REMOVE + }) + @JoinColumn(name = "PERSONID", referencedColumnName = "ID", nullable = false, updatable = false) + @BatchSize(size = 100) + private Set
addresses = new HashSet
(); + + public void addAddress(Address address) { + this.addresses.add( address ); + } + + } + + @Entity(name = "SpecialPerson") + @Access(AccessType.FIELD) + @DiscriminatorValue("2") + public static class SpecialPerson extends Person { + @Column(name = "special") + private String special; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithJoinedTableMultiLevelInheritance.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithJoinedTableMultiLevelInheritance.java new file mode 100644 index 0000000000..a238e8b590 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithJoinedTableMultiLevelInheritance.java @@ -0,0 +1,186 @@ +/* + * 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.math.BigDecimal; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.DiscriminatorColumn; +import javax.persistence.DiscriminatorType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.Environment; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue(jiraKey = "HHH-9864") +public class InsertOrderingWithJoinedTableMultiLevelInheritance + extends BaseNonConfigCoreFunctionalTestCase { + + private PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider(); + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Address.class, + Person.class, + SpecialPerson.class, + AnotherPerson.class, + President.class, + Office.class + }; + } + + @Override + protected void addSettings(Map settings) { + settings.put( Environment.ORDER_INSERTS, "true" ); + settings.put( Environment.STATEMENT_BATCH_SIZE, "10" ); + settings.put( + org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ); + } + + @Override + public void releaseResources() { + super.releaseResources(); + connectionProvider.stop(); + } + + @Test + public void testBatchingAmongstSubClasses() { + doInHibernate( this::sessionFactory, session -> { + int iterations = 2; + for ( int i = 0; i < iterations; i++ ) { + final President president = new President(); + president.addAddress( new Address() ); + session.persist( president ); + + final AnotherPerson anotherPerson = new AnotherPerson(); + Office office = new Office(); + session.persist( office ); + anotherPerson.office = office; + session.persist( anotherPerson ); + + final Person person = new Person(); + session.persist( person ); + + final SpecialPerson specialPerson = new SpecialPerson(); + specialPerson.addAddress( new Address() ); + session.persist( specialPerson ); + } + connectionProvider.clear(); + } ); + + assertEquals( 17, connectionProvider.getPreparedStatements().size() ); + } + + @Override + protected boolean isCleanupTestDataRequired() { + return true; + } + + @Override + protected void cleanupTestData() throws Exception { + doInHibernate( this::sessionFactory, session -> { + session.createQuery( "delete Address" ).executeUpdate(); + session.createQuery( "delete Person" ).executeUpdate(); + session.createQuery( "delete SpecialPerson" ).executeUpdate(); + session.createQuery( "delete AnotherPerson" ).executeUpdate(); + session.createQuery( "delete Office" ).executeUpdate(); + session.createQuery( "delete President" ).executeUpdate(); + } ); + } + + @Entity(name = "Address") + @Table(name = "ADDRESS") + public static class Address { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + } + + @Entity(name = "Office") + public static class Office { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + } + + @Entity(name = "Person") + @Table(name = "PERSON") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn(name = "CLASSINDICATOR", discriminatorType = DiscriminatorType.INTEGER) + public static class Person { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "PERSON_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + + } + + @Entity(name = "SpecialPerson") + public static class SpecialPerson extends Person { + @Column(name = "special") + private String special; + + @OneToMany(orphanRemoval = true, cascade = { + CascadeType.PERSIST, + CascadeType.REMOVE + }) + @JoinColumn(name = "PERSONID", referencedColumnName = "ID", nullable = false, updatable = false) + @BatchSize(size = 100) + private Set
addresses = new HashSet
(); + + public void addAddress(Address address) { + this.addresses.add( address ); + } + } + + @Entity(name = "AnotherPerson") + public static class AnotherPerson extends Person { + private boolean working; + + @ManyToOne + private Office office; + } + + @Entity(name = "President") + public static class President extends SpecialPerson { + + @Column(name = "salary") + private BigDecimal salary; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithSingleTableInheritance.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithSingleTableInheritance.java new file mode 100644 index 0000000000..20b0e240cb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithSingleTableInheritance.java @@ -0,0 +1,155 @@ +/* + * 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.HashSet; +import java.util.Map; +import java.util.Set; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.DiscriminatorColumn; +import javax.persistence.DiscriminatorType; +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.Environment; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + +/** + * @author Steve Ebersole + */ +@TestForIssue(jiraKey = "HHH-9864") +public class InsertOrderingWithSingleTableInheritance + extends BaseNonConfigCoreFunctionalTestCase { + + private PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider(); + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Address.class, Person.class, SpecialPerson.class }; + } + + @Override + protected void addSettings(Map settings) { + settings.put( Environment.ORDER_INSERTS, "true" ); + settings.put( Environment.STATEMENT_BATCH_SIZE, "10" ); + settings.put( + org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ); + } + + @Override + public void releaseResources() { + super.releaseResources(); + connectionProvider.stop(); + } + + @Test + public void testBatchOrdering() { + doInHibernate( this::sessionFactory, session -> { + // First object with dependent object (address) + final Person person = new Person(); + person.addAddress( new Address() ); + session.persist( person ); + + // Derived Object with dependent object (address) + final SpecialPerson specialPerson = new SpecialPerson(); + specialPerson.addAddress( new Address() ); + session.persist( specialPerson ); + } ); + } + + @Test + public void testBatchingAmongstSubClasses() { + doInHibernate( this::sessionFactory, session -> { + int iterations = 12; + for ( int i = 0; i < iterations; i++ ) { + final Person person = new Person(); + person.addAddress( new Address() ); + session.persist( person ); + + final SpecialPerson specialPerson = new SpecialPerson(); + specialPerson.addAddress( new Address() ); + session.persist( specialPerson ); + } + connectionProvider.clear(); + } ); + + assertEquals( 3, connectionProvider.getPreparedStatements().size() ); + } + + @Override + protected boolean isCleanupTestDataRequired() { + return true; + } + + @Entity(name = "Address") + @Table(name = "ADDRESS") + @Access(AccessType.FIELD) + public static class Address { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + } + + @Entity(name = "Person") + @Access(AccessType.FIELD) + @Table(name = "PERSON") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @DiscriminatorColumn(name = "CLASSINDICATOR", discriminatorType = DiscriminatorType.INTEGER) + @DiscriminatorValue("1") + public static class Person { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "PERSON_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + + @OneToMany(orphanRemoval = true, cascade = { + CascadeType.PERSIST, + CascadeType.REMOVE + }) + @JoinColumn(name = "PERSONID", referencedColumnName = "ID", nullable = false, updatable = false) + @BatchSize(size = 100) + private Set
addresses = new HashSet
(); + + public void addAddress(Address address) { + this.addresses.add( address ); + } + + } + + @Entity + @Access(AccessType.FIELD) + @DiscriminatorValue("2") + public static class SpecialPerson extends Person { + @Column(name = "special") + private String special; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithTablePerClassInheritance.java b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithTablePerClassInheritance.java new file mode 100644 index 0000000000..73b4da879e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/insertordering/InsertOrderingWithTablePerClassInheritance.java @@ -0,0 +1,155 @@ +/* + * 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.HashSet; +import java.util.Map; +import java.util.Set; +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.DiscriminatorColumn; +import javax.persistence.DiscriminatorType; +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.Environment; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue(jiraKey = "HHH-9864") +public class InsertOrderingWithTablePerClassInheritance + extends BaseNonConfigCoreFunctionalTestCase { + + private PreparedStatementSpyConnectionProvider connectionProvider = new PreparedStatementSpyConnectionProvider(); + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Address.class, Person.class, SpecialPerson.class }; + } + + @Override + protected void addSettings(Map settings) { + settings.put( Environment.ORDER_INSERTS, "true" ); + settings.put( Environment.STATEMENT_BATCH_SIZE, "10" ); + settings.put( + org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ); + } + + @Override + public void releaseResources() { + super.releaseResources(); + connectionProvider.stop(); + } + + @Test + public void testBatchOrdering() { + doInHibernate( this::sessionFactory, session -> { + // First object with dependent object (address) + final Person person = new Person(); + person.addAddress( new Address() ); + session.persist( person ); + + // Derived Object with dependent object (address) + final SpecialPerson specialPerson = new SpecialPerson(); + specialPerson.addAddress( new Address() ); + session.persist( specialPerson ); + } ); + } + + @Test + public void testBatchingAmongstSubClasses() { + doInHibernate( this::sessionFactory, session -> { + int iterations = 12; + for ( int i = 0; i < iterations; i++ ) { + final Person person = new Person(); + person.addAddress( new Address() ); + session.persist( person ); + + final SpecialPerson specialPerson = new SpecialPerson(); + specialPerson.addAddress( new Address() ); + session.persist( specialPerson ); + } + connectionProvider.clear(); + } ); + + assertEquals( 3, connectionProvider.getPreparedStatements().size() ); + } + + @Override + protected boolean isCleanupTestDataRequired() { + return true; + } + + @Entity(name = "Address") + @Table(name = "ADDRESS") + @Access(AccessType.FIELD) + public static class Address { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "ADDRESS_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + } + + @Entity(name = "Person") + @Access(AccessType.FIELD) + @Table(name = "PERSON") + @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) + @DiscriminatorColumn(name = "CLASSINDICATOR", discriminatorType = DiscriminatorType.INTEGER) + @DiscriminatorValue("1") + public static class Person { + @Id + @Column(name = "ID", nullable = false) + @SequenceGenerator(name = "ID", sequenceName = "PERSON_SEQ") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ID") + private Long id; + + @OneToMany(orphanRemoval = true, cascade = { + CascadeType.PERSIST, + CascadeType.REMOVE + }) + @JoinColumn(name = "PERSONID", referencedColumnName = "ID", nullable = false, updatable = false) + @BatchSize(size = 100) + private Set
addresses = new HashSet
(); + + public void addAddress(Address address) { + this.addresses.add( address ); + } + + } + + @Entity(name = "SpecialPerson") + @Access(AccessType.FIELD) + @DiscriminatorValue("2") + public static class SpecialPerson extends Person { + @Column(name = "special") + private String special; + } +}