From 51fd19f0558cb7da9ecbe461ea601df3b3743dfa Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 22 Aug 2024 14:02:16 +0200 Subject: [PATCH] HHH-18489 Test initialization of unowned, lazy one-to-one associations --- ...AssociationNotExplicitlySpecifiedTest.java | 443 ++++++++++++++++ ...CollectionsNotExplicitlySpecifiedTest.java | 235 +++++++++ ...AssociationNotExplicitlySpecifiedTest.java | 484 ------------------ .../testing/orm/assertj/ManagedAssert.java | 80 --- 4 files changed, 678 insertions(+), 564 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java delete mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java delete mode 100644 hibernate-testing/src/main/java/org/hibernate/testing/orm/assertj/ManagedAssert.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java new file mode 100644 index 0000000000..63111a85e0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java @@ -0,0 +1,443 @@ +package org.hibernate.orm.test.bytecode.enhancement.graph; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import org.hibernate.Hibernate; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.graph.GraphSemantic; + +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +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.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.hibernate.graph.GraphSemantic.FETCH; + +/** + * Checks that associations that are **not** explicitly specified in a fetch/load graph + * are correctly initialized (or not) according to the graph semantics, + * for several association topologies. + */ +@DomainModel( + annotatedClasses = { + LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.RootEntity.class, + LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.ContainedEntity.class + } +) +@SessionFactory(useCollectingStatementInspector = true) +@JiraKey("HHH-18489") +@BytecodeEnhanced() +@EnhancementOptions(lazyLoading = true) +@ServiceRegistry(settings = @Setting(name = AvailableSettings.MAX_FETCH_DEPTH, value = "")) +public class LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest { + + @BeforeEach + void init(SessionFactoryScope scope) { + scope.inTransaction( session -> { + for ( long i = 0; i < 3; ++i ) { + RootEntity root = new RootEntity( i * 100 ); + + long j = i * 100; + root.setLazyOneToOneOwned( new ContainedEntity( ++j ) ); + root.setLazyManyToOneOwned( new ContainedEntity( ++j ) ); + root.setEagerOneToOneOwned( new ContainedEntity( ++j ) ); + root.setEagerManyToOneOwned( new ContainedEntity( ++j ) ); + + session.persist( root ); + + ContainedEntity contained; + + contained = new ContainedEntity( ++j ); + root.setLazyOneToOneUnowned( contained ); + contained.setInverseSideOfLazyOneToOneUnowned( root ); + session.persist( contained ); + + contained = new ContainedEntity( ++j ); + root.setEagerOneToOneUnowned( contained ); + contained.setInverseSideOfEagerOneToOneUnowned( root ); + session.persist( contained ); + } + } ); + } + + @AfterEach + void cleanUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( + "update ContainedEntity set" + + " inverseSideOfLazyOneToOneUnowned = null" + + ", inverseSideOfEagerOneToOneUnowned = null" + ).executeUpdate(); + session.createMutationQuery( "delete RootEntity" ).executeUpdate(); + session.createMutationQuery( "delete ContainedEntity" ).executeUpdate(); + } ); + } + + // Arguments for the parameterized test below + List queryWithEntityGraph() { + List args = new ArrayList<>(); + for ( GraphSemantic graphSemantic : GraphSemantic.values() ) { + for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { + args.add( Arguments.of( graphSemantic, propertyName ) ); + } + for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { + args.add( Arguments.of( graphSemantic, propertyName ) ); + } + } + // Also test without a graph, for reference + args.add( Arguments.of( null, null ) ); + return args; + } + + @Test + public void testWithFetchGraph(SessionFactoryScope scope) { + String propertySpecifiedInGraph = "eagerOneToOneOwned"; + scope.inTransaction( session -> { + var sqlStatementInspector = scope.getCollectingStatementInspector(); + sqlStatementInspector.clear(); + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( propertySpecifiedInGraph ); + query.applyGraph( graph, FETCH ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ); + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ); + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + } ); + } + + @ParameterizedTest + @MethodSource + public void queryWithEntityGraph(GraphSemantic graphSemantic, String propertySpecifiedInGraph, SessionFactoryScope scope) { + scope.inTransaction( session -> { + var sqlStatementInspector = scope.getCollectingStatementInspector(); + sqlStatementInspector.clear(); + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + if ( graphSemantic != null ) { + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( propertySpecifiedInGraph ); + query.applyGraph( graph, graphSemantic ); + } // else just run the query without a graph + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ); + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ) + // Under LOAD semantics, or when not using graphs, + // eager properties also get loaded (even if not specified in the graph). + || GraphSemantic.LOAD.equals( graphSemantic ) || graphSemantic == null; + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + } ); + } + + private void assertAssociationInitialized( + List resultList, + String propertyName, + boolean expectInitialized, + SQLStatementInspector sqlStatementInspector) { + for ( var rootEntity : resultList ) { + sqlStatementInspector.clear(); + if ( propertyName.endsWith( "Unowned" ) ) { + final Supplier supplier; + switch ( propertyName ) { + case ( "lazyOneToOneUnowned" ): + supplier = () -> rootEntity.getLazyOneToOneUnowned(); + break; + case ( "eagerOneToOneUnowned" ): + supplier = () -> rootEntity.getEagerOneToOneUnowned(); + break; + default: + supplier = null; + fail( "unknown association property name : " + propertyName ); + } + assertUnownedAssociationLazyness( + supplier, + rootEntity, + propertyName, + expectInitialized, + sqlStatementInspector + ); + } + else { + final Supplier supplier; + switch ( propertyName ) { + case "lazyOneToOneOwned": + supplier = () -> rootEntity.getLazyOneToOneOwned(); + break; + case "lazyManyToOneOwned": + supplier = () -> rootEntity.getLazyManyToOneOwned(); + break; + case "eagerOneToOneOwned": + supplier = () -> rootEntity.getEagerOneToOneOwned(); + break; + case "eagerManyToOneOwned": + supplier = () -> rootEntity.getEagerManyToOneOwned(); + break; + default: + supplier = null; + fail( "unknown association property name : " + propertyName ); + } + assertOwnedAssociationLazyness( + supplier, + propertyName, + expectInitialized, + sqlStatementInspector + ); + } + } + } + + private static void assertUnownedAssociationLazyness( + Supplier associationSupplier, + RootEntity rootEntity, + String associationName, + boolean expectInitialized, + SQLStatementInspector sqlStatementInspector) { + // for an unowned lazy association the value is null and accessing the association triggers its initialization + assertThat( Hibernate.isPropertyInitialized( rootEntity, associationName ) ) + .as( associationName + " association expected to be initialized ? expected is :" + expectInitialized + " but it's not " ) + .isEqualTo( expectInitialized ); + if ( !expectInitialized ) { + var containedEntity = associationSupplier.get(); + sqlStatementInspector.assertExecutedCount( 1 ); + assertThat( Hibernate.isInitialized( containedEntity ) ); + sqlStatementInspector.clear(); + + assertThat( containedEntity ).isNotNull(); + associationSupplier.get().getName(); + sqlStatementInspector.assertExecutedCount( 0 ); + } + } + + private static void assertOwnedAssociationLazyness( + Supplier associationSupplier, + String associationName, + boolean expectInitialized, + SQLStatementInspector sqlStatementInspector) { + // for an owned lazy association the value is an enhanced proxy, Hibernate.isPropertyInitialized( rootEntity, "lazyManyToOneOwned" ) returns true. + // accessing the association does not trigger its initialization + assertThat( Hibernate.isInitialized( associationSupplier.get() ) ) + .as( associationName + " association expected to be initialized ? expected is :" + expectInitialized + " but it's not " ) + .isEqualTo( expectInitialized ); + if ( !expectInitialized ) { + var containedEntity = associationSupplier.get(); + sqlStatementInspector.assertExecutedCount( 0 ); + + containedEntity.getName(); + sqlStatementInspector.assertExecutedCount( 1 ); + assertThat( Hibernate.isInitialized( containedEntity ) ).isTrue(); + sqlStatementInspector.clear(); + + assertThat( containedEntity ).isNotNull(); + associationSupplier.get().getName(); + sqlStatementInspector.assertExecutedCount( 0 ); + } + } + + @Entity(name = "RootEntity") + static class RootEntity { + + public static final Set LAZY_PROPERTY_NAMES = Set.of( + "lazyOneToOneOwned", "lazyManyToOneOwned", "lazyOneToOneUnowned" + ); + + public static final Set EAGER_PROPERTY_NAMES = Set.of( + "eagerOneToOneOwned", "eagerManyToOneOwned", "eagerOneToOneUnowned" + + ); + + @Id + private Long id; + + private String name; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private ContainedEntity lazyOneToOneOwned; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private ContainedEntity lazyManyToOneOwned; + + @OneToOne(fetch = FetchType.LAZY, mappedBy = "inverseSideOfLazyOneToOneUnowned") + private ContainedEntity lazyOneToOneUnowned; + + @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + private ContainedEntity eagerOneToOneOwned; + + @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + private ContainedEntity eagerManyToOneOwned; + + @OneToOne(fetch = FetchType.EAGER, mappedBy = "inverseSideOfEagerOneToOneUnowned") + private ContainedEntity eagerOneToOneUnowned; + + public RootEntity() { + } + + public RootEntity(Long id) { + this.id = id; + } + + @Override + public String toString() { + return "RootEntity#" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ContainedEntity getLazyOneToOneOwned() { + return lazyOneToOneOwned; + } + + public void setLazyOneToOneOwned(ContainedEntity lazyOneToOneOwned) { + this.lazyOneToOneOwned = lazyOneToOneOwned; + } + + public ContainedEntity getLazyManyToOneOwned() { + return lazyManyToOneOwned; + } + + public void setLazyManyToOneOwned(ContainedEntity lazyManyToOneOwned) { + this.lazyManyToOneOwned = lazyManyToOneOwned; + } + + public ContainedEntity getEagerOneToOneOwned() { + return eagerOneToOneOwned; + } + + public void setEagerOneToOneOwned(ContainedEntity eagerOneToOneOwned) { + this.eagerOneToOneOwned = eagerOneToOneOwned; + } + + public ContainedEntity getEagerManyToOneOwned() { + return eagerManyToOneOwned; + } + + public void setEagerManyToOneOwned(ContainedEntity eagerManyToOneOwned) { + this.eagerManyToOneOwned = eagerManyToOneOwned; + } + + public ContainedEntity getLazyOneToOneUnowned() { + return lazyOneToOneUnowned; + } + + public void setLazyOneToOneUnowned(ContainedEntity lazyOneToOneUnowned) { + this.lazyOneToOneUnowned = lazyOneToOneUnowned; + } + + public ContainedEntity getEagerOneToOneUnowned() { + return eagerOneToOneUnowned; + } + + public void setEagerOneToOneUnowned(ContainedEntity eagerOneToOneUnowned) { + this.eagerOneToOneUnowned = eagerOneToOneUnowned; + } + } + + @Entity(name = "ContainedEntity") + static class ContainedEntity { + + @Id + private Long id; + + private String name; + + @OneToOne(fetch = FetchType.LAZY) + private RootEntity inverseSideOfLazyOneToOneUnowned; + + @OneToOne(fetch = FetchType.LAZY) + private RootEntity inverseSideOfEagerOneToOneUnowned; + + public ContainedEntity() { + } + + public ContainedEntity(Long id) { + this.id = id; + this.name = "Name #" + id; + } + + @Override + public String toString() { + return "ContainedEntity#" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public RootEntity getInverseSideOfLazyOneToOneUnowned() { + return inverseSideOfLazyOneToOneUnowned; + } + + public void setInverseSideOfLazyOneToOneUnowned(RootEntity inverseSideOfLazyOneToOneUnowned) { + this.inverseSideOfLazyOneToOneUnowned = inverseSideOfLazyOneToOneUnowned; + } + + public RootEntity getInverseSideOfEagerOneToOneUnowned() { + return inverseSideOfEagerOneToOneUnowned; + } + + public void setInverseSideOfEagerOneToOneUnowned(RootEntity inverseSideOfEagerOneToOneUnowned) { + this.inverseSideOfEagerOneToOneUnowned = inverseSideOfEagerOneToOneUnowned; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java new file mode 100644 index 0000000000..1c8acb026a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java @@ -0,0 +1,235 @@ +package org.hibernate.orm.test.bytecode.enhancement.graph; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.graph.GraphSemantic; + +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.RootEntity.class, + LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.ContainedEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18489") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@EnhancementOptions(lazyLoading = true) +public class LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest { + + @BeforeEach + void init(SessionFactoryScope scope) { + scope.inTransaction( session -> { + for ( long i = 0; i < 3; ++i ) { + var root = new RootEntity( i * 100 ); + long j = i * 100; + session.persist( root ); + + var contained = new ContainedEntity( ++j ); + session.persist( contained ); + + root.addEagerContainedEntity( contained ); + + var contained2 = new ContainedEntity( ++j ); + session.persist( contained2 ); + + root.addLazyContainedEntity( contained2 ); + } + } ); + } + + @AfterEach + void cleanUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete ContainedEntity" ).executeUpdate(); + session.createMutationQuery( "delete RootEntity" ).executeUpdate(); + } ); + } + + @Test + void queryWithFetchGraph(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( "lazyContainedEntities" ); + query.applyGraph( graph, GraphSemantic.FETCH ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( var rootEntity : resultList ) { + // GraphSemantic.FETCH, so eagerContainedEntities is lazy because it's not present in the EntityGraph + assertThat( Hibernate.isInitialized( rootEntity.getEagerContainedEntities() )).isFalse(); + assertThat( Hibernate.isInitialized( rootEntity.getLazyContainedEntities() ) ).isTrue(); + } + } ); + } + + @Test + void queryWithLoadGraph(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( "lazyContainedEntities" ); + query.applyGraph( graph, GraphSemantic.LOAD ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( var rootEntity : resultList ) { + // GraphSemantic.LOAD, eagerContainedEntities maintains is eagerness + assertThat( Hibernate.isInitialized( rootEntity.getEagerContainedEntities() )).isTrue(); + assertThat( Hibernate.isInitialized( rootEntity.getLazyContainedEntities() ) ).isTrue(); + } + } ); + } + + @Test + void queryWithNoEntityGraph(SessionFactoryScope scope) { + + scope.inTransaction( session -> { + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( var rootEntity : resultList ) { + assertThat( Hibernate.isInitialized( rootEntity.getEagerContainedEntities() ) ).isTrue(); + assertThat( Hibernate.isInitialized( rootEntity.getLazyContainedEntities() ) ).isFalse(); + } + } ); + } + + @Entity(name = "RootEntity") + static class RootEntity { + + @Id + private Long id; + + private String name; + + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "eager_id") + private List eagerContainedEntities; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "lazy_id") + private List lazyContainedEntities; + + public RootEntity() { + } + + public RootEntity(Long id) { + this.id = id; + this.name = "Name #" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getEagerContainedEntities() { + return eagerContainedEntities; + } + + public void setEagerContainedEntities(List eagerContainedEntities) { + this.eagerContainedEntities = eagerContainedEntities; + } + + public List getLazyContainedEntities() { + return lazyContainedEntities; + } + + public void setLazyContainedEntities(List lazyContainedEntities) { + this.lazyContainedEntities = lazyContainedEntities; + } + + public void addEagerContainedEntity(ContainedEntity containedEntity) { + if ( eagerContainedEntities == null ) { + eagerContainedEntities = new ArrayList<>(); + } + eagerContainedEntities.add( containedEntity ); + } + + public void addLazyContainedEntity(ContainedEntity containedEntity) { + if ( lazyContainedEntities == null ) { + lazyContainedEntities = new ArrayList<>(); + } + lazyContainedEntities.add( containedEntity ); + } + + @Override + public String toString() { + return "RootEntity#" + id; + } + } + + @Entity(name = "ContainedEntity") + static class ContainedEntity { + + @Id + private Long id; + + private String name; + + public ContainedEntity() { + } + + public ContainedEntity(Long id) { + this.id = id; + this.name = "Name #" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ContainedEntity#" + id; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java deleted file mode 100644 index 2ece4fa78b..0000000000 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java +++ /dev/null @@ -1,484 +0,0 @@ -package org.hibernate.orm.test.entitygraph; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Set; - -import org.hibernate.graph.GraphSemantic; - -import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; -import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; -import org.hibernate.testing.orm.assertj.ManagedAssert; -import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.JiraKey; -import org.hibernate.testing.orm.junit.SessionFactory; -import org.hibernate.testing.orm.junit.SessionFactoryScope; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; -import jakarta.persistence.ManyToMany; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; -import org.assertj.core.api.AbstractListAssert; - -/** - * Checks that associations that are **not** explicitly specified in a fetch/load graph - * are correctly initialized (or not) according to the graph semantics, - * for several association topologies. - */ -@DomainModel( - annotatedClasses = { - LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.RootEntity.class, - LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.ContainedEntity.class - } -) -@SessionFactory -@JiraKey("HHH-18489") -@BytecodeEnhanced(runNotEnhancedAsWell = true) -@EnhancementOptions(lazyLoading = true) -public class LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest { - - @BeforeEach - void init(SessionFactoryScope scope) { - scope.inTransaction( session -> { - for ( long i = 0; i < 3; ++i ) { - RootEntity root = new RootEntity( i * 100 ); - - long j = i * 100; - root.setLazyOneToOneOwned( new ContainedEntity( ++j ) ); - root.setLazyManyToOneOwned( new ContainedEntity( ++j ) ); - root.setLazyOneToManyOwned( List.of( new ContainedEntity( ++j ) ) ); - root.setLazyManyToManyOwned( List.of( new ContainedEntity( ++j ) ) ); - root.setEagerOneToOneOwned( new ContainedEntity( ++j ) ); - root.setEagerManyToOneOwned( new ContainedEntity( ++j ) ); - root.setEagerOneToManyOwned( List.of( new ContainedEntity( ++j ) ) ); - root.setEagerManyToManyOwned( List.of( new ContainedEntity( ++j ) ) ); - - session.persist( root ); - - ContainedEntity contained; - - contained = new ContainedEntity( ++j ); - root.setLazyOneToOneUnowned( contained ); - contained.setInverseSideOfLazyOneToOneUnowned( root ); - session.persist( contained ); - - contained = new ContainedEntity( ++j ); - root.setLazyOneToManyUnowned( List.of( contained ) ); - contained.setInverseSideOfLazyOneToManyUnowned( root ); - session.persist( contained ); - - contained = new ContainedEntity( ++j ); - root.setLazyOneToManyUnowned( List.of( contained ) ); - contained.setInverseSideOfLazyManyToManyUnowned( List.of( root ) ); - session.persist( contained ); - - contained = new ContainedEntity( ++j ); - root.setEagerOneToOneUnowned( contained ); - contained.setInverseSideOfEagerOneToOneUnowned( root ); - session.persist( contained ); - - contained = new ContainedEntity( ++j ); - root.setEagerOneToManyUnowned( List.of( contained ) ); - contained.setInverseSideOfEagerOneToManyUnowned( root ); - session.persist( contained ); - - contained = new ContainedEntity( ++j ); - root.setEagerOneToManyUnowned( List.of( contained ) ); - contained.setInverseSideOfEagerManyToManyUnowned( List.of( root ) ); - session.persist( contained ); - } - } ); - } - - @AfterEach - void cleanUp(SessionFactoryScope scope) { - scope.inTransaction( session -> { - session.createMutationQuery( "update ContainedEntity set" - + " inverseSideOfLazyOneToOneUnowned = null" - + ", inverseSideOfLazyOneToManyUnowned = null" - + ", inverseSideOfEagerOneToOneUnowned = null" - + ", inverseSideOfEagerOneToManyUnowned = null" ).executeUpdate(); - session.createNativeQuery( "delete from RootEntity_lazyManyToManyUnowned" ).executeUpdate(); - session.createNativeQuery( "delete from RootEntity_eagerManyToManyUnowned" ).executeUpdate(); - session.createMutationQuery( "delete RootEntity" ).executeUpdate(); - session.createMutationQuery( "delete ContainedEntity" ).executeUpdate(); - } ); - } - - // Arguments for the parameterized test below - List queryWithFetchGraph() { - List args = new ArrayList<>(); - for ( GraphSemantic graphSemantic : GraphSemantic.values() ) { - for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { - args.add( Arguments.of( graphSemantic, propertyName ) ); - } - for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { - args.add( Arguments.of( graphSemantic, propertyName ) ); - } - } - // Also test without a graph, for reference - args.add( Arguments.of( null, null ) ); - return args; - } - - @ParameterizedTest - @MethodSource - void queryWithFetchGraph(GraphSemantic graphSemantic, String propertySpecifiedInGraph, SessionFactoryScope scope) { - scope.inTransaction( session -> { - var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) - .setFetchSize( 100 ) - // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) - .setParameter( "ids", List.of( 0L, 100L, 200L ) ); - - if ( graphSemantic != null ) { - var graph = session.createEntityGraph( RootEntity.class ); - graph.addAttributeNode( propertySpecifiedInGraph ); - query.applyGraph( graph, graphSemantic ); - } // else just run the query without a graph - - var resultList = query.list(); - assertThat( resultList ).isNotEmpty(); - assertSoftly( softly -> { // "softly" is used to report all failures instead of just the first one - var resultListAssert = softly.assertThat( resultList ); - - for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { - boolean expectInitialized = propertyName.equals( propertySpecifiedInGraph ); - assertAssociationInitialized( resultListAssert, propertyName, expectInitialized ); - } - for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { - boolean expectInitialized = propertyName.equals( propertySpecifiedInGraph ) - // Under LOAD semantics, or when not using graphs, - // eager properties also get loaded (even if not specified in the graph). - || GraphSemantic.LOAD.equals( graphSemantic ) || graphSemantic == null; - assertAssociationInitialized( resultListAssert, propertyName, expectInitialized ); - } - } ); - } ); - } - - private void assertAssociationInitialized(AbstractListAssert resultListAssert, - String propertyName, boolean expectInitialized) { - resultListAssert.allSatisfy( loaded -> assertThat( loaded ).extracting( propertyName, ManagedAssert.factory() ) - .as( "Managed object held in attribute '" + propertyName + "' of '" + loaded + "'" ) - .isInitialized( expectInitialized ) ); - } - - @Entity(name = "RootEntity") - static class RootEntity { - - public static final Set LAZY_PROPERTY_NAMES = Set.of( - "lazyOneToOneOwned", "lazyManyToOneOwned", "lazyOneToManyOwned", "lazyManyToManyOwned", - "lazyOneToOneUnowned", "lazyOneToManyUnowned", "lazyManyToManyUnowned" - ); - public static final Set EAGER_PROPERTY_NAMES = Set.of( - "eagerOneToOneOwned", "eagerManyToOneOwned", "eagerOneToManyOwned", "eagerManyToManyOwned", - "eagerOneToOneUnowned", "eagerOneToManyUnowned", "eagerManyToManyUnowned" - ); - - @Id - private Long id; - - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) - private ContainedEntity lazyOneToOneOwned; - - @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) - private ContainedEntity lazyManyToOneOwned; - - @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) - @JoinTable(name = "RootEntity_lazyOneToManyOwned") - private List lazyOneToManyOwned; - - @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) - @JoinTable(name = "RootEntity_lazyManyToManyOwned") - private List lazyManyToManyOwned; - - @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) - private ContainedEntity eagerOneToOneOwned; - - @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) - private ContainedEntity eagerManyToOneOwned; - - @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) - @JoinTable(name = "RootEntity_eagerOneToManyOwned") - private List eagerOneToManyOwned; - - @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) - @JoinTable(name = "RootEntity_eagerManyToManyOwned") - private List eagerManyToManyOwned; - - @OneToOne(fetch = FetchType.LAZY, mappedBy = "inverseSideOfLazyOneToOneUnowned") - private ContainedEntity lazyOneToOneUnowned; - - @OneToMany(fetch = FetchType.LAZY, mappedBy = "inverseSideOfLazyOneToManyUnowned") - private List lazyOneToManyUnowned; - - @ManyToMany(fetch = FetchType.LAZY, mappedBy = "inverseSideOfLazyManyToManyUnowned") - private List lazyManyToManyUnowned; - - @OneToOne(fetch = FetchType.EAGER, mappedBy = "inverseSideOfEagerOneToOneUnowned") - private ContainedEntity eagerOneToOneUnowned; - - @OneToMany(fetch = FetchType.EAGER, mappedBy = "inverseSideOfEagerOneToManyUnowned") - private List eagerOneToManyUnowned; - - @ManyToMany(fetch = FetchType.EAGER, mappedBy = "inverseSideOfEagerManyToManyUnowned") - private List eagerManyToManyUnowned; - - public RootEntity() { - } - - public RootEntity(Long id) { - this.id = id; - } - - @Override - public String toString() { - return "RootEntity#" + id; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public ContainedEntity getLazyOneToOneOwned() { - return lazyOneToOneOwned; - } - - public void setLazyOneToOneOwned(ContainedEntity lazyOneToOneOwned) { - this.lazyOneToOneOwned = lazyOneToOneOwned; - } - - public ContainedEntity getLazyManyToOneOwned() { - return lazyManyToOneOwned; - } - - public void setLazyManyToOneOwned(ContainedEntity lazyManyToOneOwned) { - this.lazyManyToOneOwned = lazyManyToOneOwned; - } - - public List getLazyOneToManyOwned() { - return lazyOneToManyOwned; - } - - public void setLazyOneToManyOwned(List lazyOneToManyOwned) { - this.lazyOneToManyOwned = lazyOneToManyOwned; - } - - public List getLazyManyToManyOwned() { - return lazyManyToManyOwned; - } - - public void setLazyManyToManyOwned(List lazyManyToManyOwned) { - this.lazyManyToManyOwned = lazyManyToManyOwned; - } - - public ContainedEntity getEagerOneToOneOwned() { - return eagerOneToOneOwned; - } - - public void setEagerOneToOneOwned(ContainedEntity eagerOneToOneOwned) { - this.eagerOneToOneOwned = eagerOneToOneOwned; - } - - public ContainedEntity getEagerManyToOneOwned() { - return eagerManyToOneOwned; - } - - public void setEagerManyToOneOwned(ContainedEntity eagerManyToOneOwned) { - this.eagerManyToOneOwned = eagerManyToOneOwned; - } - - public List getEagerOneToManyOwned() { - return eagerOneToManyOwned; - } - - public void setEagerOneToManyOwned(List eagerOneToManyOwned) { - this.eagerOneToManyOwned = eagerOneToManyOwned; - } - - public List getEagerManyToManyOwned() { - return eagerManyToManyOwned; - } - - public void setEagerManyToManyOwned(List eagerManyToManyOwned) { - this.eagerManyToManyOwned = eagerManyToManyOwned; - } - - public ContainedEntity getLazyOneToOneUnowned() { - return lazyOneToOneUnowned; - } - - public void setLazyOneToOneUnowned(ContainedEntity lazyOneToOneUnowned) { - this.lazyOneToOneUnowned = lazyOneToOneUnowned; - } - - public List getLazyOneToManyUnowned() { - return lazyOneToManyUnowned; - } - - public void setLazyOneToManyUnowned(List lazyOneToManyUnowned) { - this.lazyOneToManyUnowned = lazyOneToManyUnowned; - } - - public List getLazyManyToManyUnowned() { - return lazyManyToManyUnowned; - } - - public void setLazyManyToManyUnowned(List lazyManyToManyUnowned) { - this.lazyManyToManyUnowned = lazyManyToManyUnowned; - } - - public ContainedEntity getEagerOneToOneUnowned() { - return eagerOneToOneUnowned; - } - - public void setEagerOneToOneUnowned(ContainedEntity eagerOneToOneUnowned) { - this.eagerOneToOneUnowned = eagerOneToOneUnowned; - } - - public List getEagerOneToManyUnowned() { - return eagerOneToManyUnowned; - } - - public void setEagerOneToManyUnowned(List eagerOneToManyUnowned) { - this.eagerOneToManyUnowned = eagerOneToManyUnowned; - } - - public List getEagerManyToManyUnowned() { - return eagerManyToManyUnowned; - } - - public void setEagerManyToManyUnowned(List eagerManyToManyUnowned) { - this.eagerManyToManyUnowned = eagerManyToManyUnowned; - } - } - - @Entity(name = "ContainedEntity") - static class ContainedEntity { - - @Id - private Long id; - - private String name; - - @OneToOne(fetch = FetchType.LAZY) - private RootEntity inverseSideOfLazyOneToOneUnowned; - - @ManyToOne(fetch = FetchType.LAZY) - private RootEntity inverseSideOfLazyOneToManyUnowned; - - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "RootEntity_lazyManyToManyUnowned", - // Default column name is too long for some DBs, and in fact we don't care about it - joinColumns = @JoinColumn(name = "invLazManyToManyUnowned_id")) - private List inverseSideOfLazyManyToManyUnowned; - - @OneToOne(fetch = FetchType.LAZY) - private RootEntity inverseSideOfEagerOneToOneUnowned; - - @ManyToOne(fetch = FetchType.LAZY) - private RootEntity inverseSideOfEagerOneToManyUnowned; - - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable(name = "RootEntity_eagerManyToManyUnowned", - // Default column name is too long for some DBs, and in fact we don't care about it - joinColumns = @JoinColumn(name = "invEagManyToManyUnowned_id")) - private List inverseSideOfEagerManyToManyUnowned; - - public ContainedEntity() { - } - - public ContainedEntity(Long id) { - this.id = id; - this.name = "Name #" + id; - } - - @Override - public String toString() { - return "ContainedEntity#" + id; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public RootEntity getInverseSideOfLazyOneToOneUnowned() { - return inverseSideOfLazyOneToOneUnowned; - } - - public void setInverseSideOfLazyOneToOneUnowned(RootEntity inverseSideOfLazyOneToOneUnowned) { - this.inverseSideOfLazyOneToOneUnowned = inverseSideOfLazyOneToOneUnowned; - } - - public RootEntity getInverseSideOfLazyOneToManyUnowned() { - return inverseSideOfLazyOneToManyUnowned; - } - - public void setInverseSideOfLazyOneToManyUnowned(RootEntity inverseSideOfLazyOneToManyUnowned) { - this.inverseSideOfLazyOneToManyUnowned = inverseSideOfLazyOneToManyUnowned; - } - - public List getInverseSideOfLazyManyToManyUnowned() { - return inverseSideOfLazyManyToManyUnowned; - } - - public void setInverseSideOfLazyManyToManyUnowned(List inverseSideOfLazyManyToManyUnowned) { - this.inverseSideOfLazyManyToManyUnowned = inverseSideOfLazyManyToManyUnowned; - } - - public RootEntity getInverseSideOfEagerOneToOneUnowned() { - return inverseSideOfEagerOneToOneUnowned; - } - - public void setInverseSideOfEagerOneToOneUnowned(RootEntity inverseSideOfEagerOneToOneUnowned) { - this.inverseSideOfEagerOneToOneUnowned = inverseSideOfEagerOneToOneUnowned; - } - - public RootEntity getInverseSideOfEagerOneToManyUnowned() { - return inverseSideOfEagerOneToManyUnowned; - } - - public void setInverseSideOfEagerOneToManyUnowned(RootEntity inverseSideOfEagerOneToManyUnowned) { - this.inverseSideOfEagerOneToManyUnowned = inverseSideOfEagerOneToManyUnowned; - } - - public List getInverseSideOfEagerManyToManyUnowned() { - return inverseSideOfEagerManyToManyUnowned; - } - - public void setInverseSideOfEagerManyToManyUnowned(List inverseSideOfEagerManyToManyUnowned) { - this.inverseSideOfEagerManyToManyUnowned = inverseSideOfEagerManyToManyUnowned; - } - } -} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/assertj/ManagedAssert.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/assertj/ManagedAssert.java deleted file mode 100644 index 1ef619c01f..0000000000 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/assertj/ManagedAssert.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.testing.orm.assertj; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hibernate.Hibernate; - -import org.assertj.core.api.AbstractBooleanAssert; -import org.assertj.core.api.AbstractObjectAssert; -import org.assertj.core.api.InstanceOfAssertFactory; - -public class ManagedAssert extends AbstractObjectAssert, Object> { - - public static ManagedAssert assertThatManaged(T managed) { - return new ManagedAssert<>( managed ); - } - - public static InstanceOfAssertFactory> factory() { - return new InstanceOfAssertFactory<>( Object.class, ManagedAssert::new ); - } - - public ManagedAssert(Object t) { - super( t, ManagedAssert.class ); - } - - public ManagedAssert isInitialized(boolean expectInitialized) { - isNotNull(); - managedInitialization().isEqualTo( expectInitialized ); - return this; - } - - public ManagedAssert isInitialized() { - return isInitialized( true ); - } - - @Override - protected AbstractObjectAssert newObjectAssert(T objectUnderTest) { - return new ManagedAssert( objectUnderTest ); - } - - public ManagedAssert isNotInitialized() { - return isInitialized( false ); - } - - public ManagedAssert isPropertyInitialized(String propertyName, boolean expectInitialized) { - isNotNull(); - propertyInitialization( propertyName ).isEqualTo( expectInitialized ); - return this; - } - - public ManagedAssert isPropertyInitialized(String propertyName) { - return isPropertyInitialized( propertyName, true ); - } - - public ManagedAssert isPropertyNotInitialized(String propertyName) { - return isPropertyInitialized( propertyName, false ); - } - - private AbstractBooleanAssert managedInitialization() { - return assertThat( Hibernate.isInitialized( actual ) ) - .as( "Is '" + actualAsText() + "' initialized?" ); - } - - private AbstractBooleanAssert propertyInitialization(String propertyName) { - return assertThat( Hibernate.isPropertyInitialized( actual, propertyName ) ) - .as( "Is property '" + propertyName + "' of '" + actualAsText() + "' initialized?" ); - } - - private String actualAsText() { - String text = descriptionText(); - if ( text == null || text.isEmpty() ) { - text = String.valueOf( actual ); - } - return text; - } - -}