diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/NestedLazyManyToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/NestedLazyManyToOneTest.java new file mode 100644 index 0000000000..b04331cd3c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/batchfetch/NestedLazyManyToOneTest.java @@ -0,0 +1,236 @@ +/* + * 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.orm.test.batchfetch; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; + +import static jakarta.persistence.CascadeType.ALL; +import static jakarta.persistence.FetchType.LAZY; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Marco Belladelli + */ +@DomainModel( + annotatedClasses = { + NestedLazyManyToOneTest.AbstractEntity.class, + NestedLazyManyToOneTest.Entity1.class, + NestedLazyManyToOneTest.Entity2.class, + NestedLazyManyToOneTest.Entity3.class + } +) +@SessionFactory(useCollectingStatementInspector = true) +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "5") +}) +@JiraKey("HHH-16043") +public class NestedLazyManyToOneTest { + @BeforeAll + public void prepareData(SessionFactoryScope scope) { + final Entity1 entity1 = new Entity1(); + entity1.setId( "0" ); + + final Set entities2 = new HashSet<>(); + for ( int i = 0; i < 8; i++ ) { + final Entity2 entity2 = new Entity2(); + entity2.setId( entity1.getId() + "_" + i ); + entity2.setParent( entity1 ); + entities2.add( entity2 ); + + // add nested children only to first and last entity + if ( i == 0 || i == 7 ) { + final Set entities3 = new HashSet<>(); + for ( int j = 0; j < 5; j++ ) { + final Entity3 entity3 = new Entity3(); + entity3.setId( entity2.getId() + "_" + j ); + entity3.setParent( entity2 ); + entities3.add( entity3 ); + } + entity2.setChildren( entities3 ); + } + } + entity1.setChildren( entities2 ); + + scope.inTransaction( session -> { + session.persist( entity1 ); + } ); + } + + @Test + public void testGetFirstLevelChildren(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( session -> { + Entity1 fromDb = session.find( Entity1.class, "0" ); + Set children = fromDb.getChildren(); + + assertEquals( 8, children.size() ); + statementInspector.assertExecutedCount( 2 ); + statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 1, "\\?", 1 ); + } ); + } + + @Test + public void testGetNestedChildrenLessThanBatchSize(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( session -> { + Entity1 entity1 = session.find( Entity1.class, "0" ); + int i = 0; + for ( Entity2 child2 : entity1.getChildren() ) { + // get only first 5 (< batch size) elements + // this doesn't trigger an additional query only because entity1.children + // are ordered with @OrderBy, and we always get the first 5 first + if ( i++ >= 5 ) { + break; + } + else { + Set children3 = child2.getChildren(); + if ( child2.getId().equals( "0_0" ) ) { + assertEquals( 5, children3.size() ); + } + else { + assertEquals( 0, children3.size() ); + } + } + } + + assertEquals( 8, entity1.getChildren().size() ); + statementInspector.assertExecutedCount( 3 ); // 1 for Entity1, 1 for Entity2, 1 for Entity3 + statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 2, "\\?", 5 ); + } ); + } + + @Test + public void testGetNestedChildrenMoreThanBatchSize(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( session -> { + Entity1 entity1 = session.find( Entity1.class, "0" ); + for ( Entity2 child2 : entity1.getChildren() ) { + Set children3 = child2.getChildren(); + if ( child2.getId().equals( "0_0" ) || child2.getId().equals( "0_7" ) ) { + assertEquals( 5, children3.size() ); + } + else { + assertEquals( 0, children3.size() ); + } + } + + assertEquals( 8, entity1.getChildren().size() ); + statementInspector.assertExecutedCount( 4 ); // 1 for Entity1, 1 for Entity2, 2 for Entity3 + statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 2, "\\?", 5 ); + statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 3, "\\?", 3 ); + } ); + } + + @MappedSuperclass + public static class AbstractEntity { + @Id + private String id; + + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "#" + getId(); + } + } + + + @Entity(name = "Entity1") + public static class Entity1 extends AbstractEntity { + @OneToMany(mappedBy = "parent", cascade = ALL, orphanRemoval = true) + @OrderBy("id") + private Set children = new HashSet<>(); + + public Set getChildren() { + return children; + } + + public void setChildren(Set children) { + this.children = children; + } + } + + @Entity(name = "Entity2") + public static class Entity2 extends AbstractEntity { + @ManyToOne(fetch = LAZY) + private Entity1 parent; + + @OneToMany(mappedBy = "parent", cascade = ALL, orphanRemoval = true) + private Set children = new HashSet<>(); + + public Entity1 getParent() { + return parent; + } + + public void setParent(Entity1 parent) { + this.parent = parent; + } + + public Set getChildren() { + return children; + } + + public void setChildren(Set children) { + this.children = children; + } + } + + @Entity(name = "Entity3") + public static class Entity3 extends AbstractEntity { + @ManyToOne(fetch = LAZY) + private Entity2 parent; + + public Entity2 getParent() { + return parent; + } + + public void setParent(Entity2 parent) { + this.parent = parent; + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SQLStatementInspector.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SQLStatementInspector.java index e59b37a8c2..85ced4b382 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SQLStatementInspector.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SQLStatementInspector.java @@ -93,8 +93,12 @@ else if ( parts[i].endsWith( " cross" ) ) { } public void assertNumberOfOccurrenceInQuery(int queryNumber, String toCheck, int expectedNumberOfOccurrences) { + assertNumberOfOccurrenceInQueryNoSpace( queryNumber, " " + toCheck + " ", expectedNumberOfOccurrences ); + } + + public void assertNumberOfOccurrenceInQueryNoSpace(int queryNumber, String toCheck, int expectedNumberOfOccurrences) { String query = sqlQueries.get( queryNumber ); - int actual = query.split( " " + toCheck + " ", -1 ).length - 1; + int actual = query.split( toCheck, -1 ).length - 1; assertThat( "number of " + toCheck, actual, is( expectedNumberOfOccurrences ) ); }