From 09d1dd3daf123451089d6be0ac3367ade7adb7cf Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Wed, 13 Nov 2019 17:03:49 +0000 Subject: [PATCH] HHH-13725 - Implement ToOne Associations support --- .../SingularAssociationAttributeMapping.java | 18 +- .../domain/entity/DelayedEntityAssembler.java | 13 -- .../domain/entity/DelayedEntityFetch.java | 192 ------------------ .../domain/entity/DelayedEntityFetchImpl.java | 92 +++++++++ .../entity/DelayedEntityFetchInitializer.java | 125 ++++++++++++ ...yManyToOneTest.java => ManyToOneTest.java} | 84 ++++++-- 6 files changed, 297 insertions(+), 227 deletions(-) delete mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityAssembler.java delete mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetch.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetchImpl.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetchInitializer.java rename hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/{LazyManyToOneTest.java => ManyToOneTest.java} (67%) diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SingularAssociationAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SingularAssociationAttributeMapping.java index c2718c4f14..da27837870 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SingularAssociationAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SingularAssociationAttributeMapping.java @@ -29,7 +29,7 @@ import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; import org.hibernate.sql.ast.tree.from.TableReferenceCollector; import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.results.internal.domain.entity.DelayedEntityFetch; +import org.hibernate.sql.results.internal.domain.entity.DelayedEntityFetchImpl; import org.hibernate.sql.results.internal.domain.entity.EntityFetchImpl; import org.hibernate.sql.results.spi.DomainResultCreationState; import org.hibernate.sql.results.spi.Fetch; @@ -87,14 +87,13 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu LockMode lockMode, String resultVariable, DomainResultCreationState creationState) { + final SqlAstCreationState sqlAstCreationState = creationState.getSqlAstCreationState(); + final TableGroup lhsTableGroup = sqlAstCreationState.getFromClauseAccess() + .getTableGroup( fetchParent.getNavigablePath() ); + if ( fetchTiming == FetchTiming.IMMEDIATE && selected ) { - final SqlAstCreationState sqlAstCreationState = creationState.getSqlAstCreationState(); - - TableGroup lhsTableGroup = sqlAstCreationState.getFromClauseAccess() - .getTableGroup( fetchParent.getNavigablePath() ); - if ( sqlAstCreationState.getFromClauseAccess().findTableGroup( fetchablePath ) == null ) { - // todo (6.0) : verify the JoinType si correct + // todo (6.0) : verify the JoinType is correct JoinType joinType; if ( isNullable ) { joinType = JoinType.LEFT; @@ -132,12 +131,13 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu ); } - return new DelayedEntityFetch( + return new DelayedEntityFetchImpl( fetchParent, this, lockMode, - !selected, + isNullable, fetchablePath, + foreignKeyDescriptor.createDomainResult( fetchablePath, lhsTableGroup, creationState ), creationState ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityAssembler.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityAssembler.java deleted file mode 100644 index 3d7c175f9f..0000000000 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityAssembler.java +++ /dev/null @@ -1,13 +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 http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.sql.results.internal.domain.entity; - -/** - * @author Andrea Boriero - */ -public class DelayedEntityAssembler { -} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetch.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetch.java deleted file mode 100644 index 6eb2e7e0c9..0000000000 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetch.java +++ /dev/null @@ -1,192 +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 http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.sql.results.internal.domain.entity; - -import java.util.function.Consumer; - -import org.hibernate.LockMode; -import org.hibernate.NotYetImplementedFor6Exception; -import org.hibernate.engine.spi.EntityKey; -import org.hibernate.metamodel.mapping.internal.SingularAssociationAttributeMapping; -import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.query.NavigablePath; -import org.hibernate.sql.results.internal.domain.AbstractFetchParentAccess; -import org.hibernate.sql.results.spi.AssemblerCreationState; -import org.hibernate.sql.results.spi.DomainResultAssembler; -import org.hibernate.sql.results.spi.DomainResultCreationState; -import org.hibernate.sql.results.spi.EntityInitializer; -import org.hibernate.sql.results.spi.Fetch; -import org.hibernate.sql.results.spi.FetchParent; -import org.hibernate.sql.results.spi.FetchParentAccess; -import org.hibernate.sql.results.spi.Fetchable; -import org.hibernate.sql.results.spi.Initializer; -import org.hibernate.sql.results.spi.RowProcessingState; - -/** - * @author Andrea Boriero - */ -public class DelayedEntityFetch implements Fetch { - - private FetchParent fetchParent; - private SingularAssociationAttributeMapping fetchedAttribute; - private final LockMode lockMode; - private final NavigablePath navigablePath; - private final boolean nullable; - private final DomainResultCreationState creationState; - - public DelayedEntityFetch( - FetchParent fetchParent, - SingularAssociationAttributeMapping fetchedAttribute, - LockMode lockMode, - boolean nullable, - NavigablePath navigablePath, - DomainResultCreationState creationState) { - this.fetchParent = fetchParent; - this.fetchedAttribute = fetchedAttribute; - this.lockMode = lockMode; - this.nullable = nullable; - this.navigablePath = navigablePath; - this.creationState = creationState; - } - - @Override - public FetchParent getFetchParent() { - return fetchParent; - } - - @Override - public Fetchable getFetchedMapping() { - return fetchedAttribute; - } - - @Override - public NavigablePath getNavigablePath() { - return navigablePath; - } - - @Override - public boolean isNullable() { - return nullable; - } - - @Override - public DomainResultAssembler createAssembler( - FetchParentAccess parentAccess, - Consumer collector, - AssemblerCreationState creationState) { - EntityInitializer entityInitializer = new DelayedEntityFetchInitializer( - parentAccess, - navigablePath, - (EntityPersister) fetchedAttribute.getMappedTypeDescriptor() - ); - collector.accept( entityInitializer ); - return new EntityAssembler( fetchedAttribute.getJavaTypeDescriptor(), entityInitializer ); - - } - - private static class DelayedEntityFetchInitializer extends AbstractFetchParentAccess implements EntityInitializer { - - private final FetchParentAccess parentAccess; - private final NavigablePath navigablePath; - private final EntityPersister concreteDescriptor; - - private Object entityInstance; - - protected DelayedEntityFetchInitializer( - FetchParentAccess parentAccess, - NavigablePath fetchedNavigable, - EntityPersister concreteDescriptor - ) { - this.parentAccess = parentAccess; - this.navigablePath = fetchedNavigable; - this.concreteDescriptor = concreteDescriptor; - } - - @Override - public NavigablePath getNavigablePath() { - return navigablePath; - } - - @Override - public void resolveKey(RowProcessingState rowProcessingState) { - // nothing to do - } - - @Override - public void resolveInstance(RowProcessingState rowProcessingState) { - final EntityKey entityKey = new EntityKey( - parentAccess.getParentKey(), - concreteDescriptor - ); - Object fkValue = entityKey.getIdentifierValue(); - - // todo (6.0) : technically the entity could be managed or cached already. who/what handles that? - - // todo (6.0) : could also be getting loaded elsewhere (LoadingEntityEntry) - if ( fkValue == null ) { - // todo (6.0) : check this is the correct behaviour - entityInstance = null; - } - else { - if ( concreteDescriptor.hasProxy() ) { - entityInstance = concreteDescriptor.createProxy( - fkValue, - rowProcessingState.getSession() - ); - } - else if ( concreteDescriptor - .getBytecodeEnhancementMetadata() - .isEnhancedForLazyLoading() ) { - entityInstance = concreteDescriptor.instantiate( - fkValue, - rowProcessingState.getSession() - ); - } - - notifyParentResolutionListeners( entityInstance ); - } - } - - @Override - public void initializeInstance(RowProcessingState rowProcessingState) { - // nothing to do - } - - @Override - public void finishUpRow(RowProcessingState rowProcessingState) { - entityInstance = null; - - clearParentResolutionListeners(); - } - - @Override - public EntityPersister getEntityDescriptor() { - return concreteDescriptor; - } - - @Override - public Object getEntityInstance() { - return entityInstance; - } - - @Override - public Object getParentKey() { - throw new NotYetImplementedFor6Exception( getClass() ); - } - - @Override - public void registerResolutionListener(Consumer listener) { - if ( entityInstance != null ) { - listener.accept( entityInstance ); - } - else { - super.registerResolutionListener( listener ); - } - } - - } -} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetchImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetchImpl.java new file mode 100644 index 0000000000..6acde261f8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetchImpl.java @@ -0,0 +1,92 @@ +/* + * 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.sql.results.internal.domain.entity; + +import java.util.function.Consumer; + +import org.hibernate.LockMode; +import org.hibernate.metamodel.mapping.internal.SingularAssociationAttributeMapping; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.NavigablePath; +import org.hibernate.sql.results.spi.AssemblerCreationState; +import org.hibernate.sql.results.spi.DomainResult; +import org.hibernate.sql.results.spi.DomainResultAssembler; +import org.hibernate.sql.results.spi.DomainResultCreationState; +import org.hibernate.sql.results.spi.EntityInitializer; +import org.hibernate.sql.results.spi.Fetch; +import org.hibernate.sql.results.spi.FetchParent; +import org.hibernate.sql.results.spi.FetchParentAccess; +import org.hibernate.sql.results.spi.Fetchable; +import org.hibernate.sql.results.spi.Initializer; + +/** + * @author Andrea Boriero + * @author Steve Ebersole + */ +public class DelayedEntityFetchImpl implements Fetch { + + private FetchParent fetchParent; + private SingularAssociationAttributeMapping fetchedAttribute; + private final LockMode lockMode; + private final NavigablePath navigablePath; + private final boolean nullable; + private DomainResult fkResult; + private final DomainResultCreationState creationState; + + public DelayedEntityFetchImpl( + FetchParent fetchParent, + SingularAssociationAttributeMapping fetchedAttribute, + LockMode lockMode, + boolean nullable, + NavigablePath navigablePath, + DomainResult fkResult, + DomainResultCreationState creationState) { + this.fetchParent = fetchParent; + this.fetchedAttribute = fetchedAttribute; + this.lockMode = lockMode; + this.nullable = nullable; + this.navigablePath = navigablePath; + this.fkResult = fkResult; + this.creationState = creationState; + + } + + @Override + public FetchParent getFetchParent() { + return fetchParent; + } + + @Override + public Fetchable getFetchedMapping() { + return fetchedAttribute; + } + + @Override + public NavigablePath getNavigablePath() { + return navigablePath; + } + + @Override + public boolean isNullable() { + return nullable; + } + + @Override + public DomainResultAssembler createAssembler( + FetchParentAccess parentAccess, + Consumer collector, + AssemblerCreationState creationState) { + EntityInitializer entityInitializer = new DelayedEntityFetchInitializer( + parentAccess, + navigablePath, + (EntityPersister) fetchedAttribute.getMappedTypeDescriptor(), + fkResult.createResultAssembler( collector, creationState ) + ); + collector.accept( entityInitializer ); + return new EntityAssembler( fetchedAttribute.getJavaTypeDescriptor(), entityInitializer ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetchInitializer.java new file mode 100644 index 0000000000..800e5a94e8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/DelayedEntityFetchInitializer.java @@ -0,0 +1,125 @@ +/* + * 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.sql.results.internal.domain.entity; + +import java.util.function.Consumer; + +import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.NavigablePath; +import org.hibernate.sql.results.internal.domain.AbstractFetchParentAccess; +import org.hibernate.sql.results.spi.DomainResultAssembler; +import org.hibernate.sql.results.spi.EntityInitializer; +import org.hibernate.sql.results.spi.FetchParentAccess; +import org.hibernate.sql.results.spi.RowProcessingState; + +/** + * @author Andrea Boriero + * @author Steve Ebersole + */ +public class DelayedEntityFetchInitializer extends AbstractFetchParentAccess implements EntityInitializer { + + private final FetchParentAccess parentAccess; + private final NavigablePath navigablePath; + private final EntityPersister concreteDescriptor; + private final DomainResultAssembler fkValueAssembler; + + + private Object entityInstance; + private Object fkValue; + + protected DelayedEntityFetchInitializer( + FetchParentAccess parentAccess, + NavigablePath fetchedNavigable, + EntityPersister concreteDescriptor, + DomainResultAssembler fkValueAssembler + ) { + this.parentAccess = parentAccess; + this.navigablePath = fetchedNavigable; + this.concreteDescriptor = concreteDescriptor; + this.fkValueAssembler = fkValueAssembler; + } + + @Override + public NavigablePath getNavigablePath() { + return navigablePath; + } + + @Override + public void resolveKey(RowProcessingState rowProcessingState) { + // nothing to do + } + + @Override + public void resolveInstance(RowProcessingState rowProcessingState) { + fkValue = fkValueAssembler.assemble( rowProcessingState ); + + // todo (6.0) : technically the entity could be managed or cached already. who/what handles that? + + // todo (6.0) : could also be getting loaded elsewhere (LoadingEntityEntry) + if ( fkValue == null ) { + // todo (6.0) : check this is the correct behaviour + entityInstance = null; + } + else { + if ( concreteDescriptor.hasProxy() ) { + entityInstance = concreteDescriptor.createProxy( + fkValue, + rowProcessingState.getSession() + ); + } + else if ( concreteDescriptor + .getBytecodeEnhancementMetadata() + .isEnhancedForLazyLoading() ) { + entityInstance = concreteDescriptor.instantiate( + fkValue, + rowProcessingState.getSession() + ); + } + + notifyParentResolutionListeners( entityInstance ); + } + } + + @Override + public void initializeInstance(RowProcessingState rowProcessingState) { + // nothing to do + } + + @Override + public void finishUpRow(RowProcessingState rowProcessingState) { + entityInstance = null; + fkValue = null; + + clearParentResolutionListeners(); + } + + @Override + public EntityPersister getEntityDescriptor() { + return concreteDescriptor; + } + + @Override + public Object getEntityInstance() { + return entityInstance; + } + + @Override + public Object getParentKey() { + throw new NotYetImplementedFor6Exception( getClass() ); + } + + @Override + public void registerResolutionListener(Consumer listener) { + if ( entityInstance != null ) { + listener.accept( entityInstance ); + } + else { + super.registerResolutionListener( listener ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/LazyManyToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/ManyToOneTest.java similarity index 67% rename from hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/LazyManyToOneTest.java rename to hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/ManyToOneTest.java index 32c44caaef..ba8ae75881 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/LazyManyToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/ManyToOneTest.java @@ -35,17 +35,17 @@ import static org.junit.Assert.assertTrue; */ @DomainModel( annotatedClasses = { - LazyManyToOneTest.SimpleEntity.class, - LazyManyToOneTest.OtherEntity.class, - LazyManyToOneTest.AnotherSimpleEntity.class + ManyToOneTest.SimpleEntity.class, + ManyToOneTest.OtherEntity.class, + ManyToOneTest.AnotherSimpleEntity.class } ) @ServiceRegistry @SessionFactory -public class LazyManyToOneTest { +public class ManyToOneTest { @Test - public void testSelect(SessionFactoryScope scope) { + public void testHqlSelect(SessionFactoryScope scope) { scope.inTransaction( session -> { OtherEntity otherEntity = session. @@ -53,14 +53,51 @@ public class LazyManyToOneTest { .uniqueResult(); assertThat( otherEntity.getName(), is( "Bar" ) ); - assertFalse( Hibernate.isInitialized( otherEntity.getSimpleEntity() ) ); - assertFalse( Hibernate.isInitialized( otherEntity.getAnotherSimpleEntity() ) ); + SimpleEntity simpleEntity = otherEntity.getSimpleEntity(); + assertFalse( Hibernate.isInitialized( simpleEntity ) ); + + AnotherSimpleEntity anotherSimpleEntity = otherEntity.getAnotherSimpleEntity(); + // the ManyToOne is eager but the value is null so a second query is not executed + assertTrue( Hibernate.isInitialized( anotherSimpleEntity ) ); + + assertThat( simpleEntity.getName(), is( "Fab" ) ); + + assertTrue( Hibernate.isInitialized( simpleEntity ) ); + } + ); + + scope.inTransaction( + session -> { + OtherEntity otherEntity = session. + createQuery( "from OtherEntity", OtherEntity.class ) + .uniqueResult(); + AnotherSimpleEntity anotherSimpleEntity = new AnotherSimpleEntity(); + anotherSimpleEntity.setId( 3 ); + anotherSimpleEntity.setName( "other" ); + session.save( anotherSimpleEntity ); + otherEntity.setAnotherSimpleEntity( anotherSimpleEntity ); + } + ); + + scope.inTransaction( + session -> { + OtherEntity otherEntity = session. + createQuery( "from OtherEntity", OtherEntity.class ) + .uniqueResult(); + + assertThat( otherEntity.getName(), is( "Bar" ) ); + SimpleEntity simpleEntity = otherEntity.getSimpleEntity(); + assertFalse( Hibernate.isInitialized( simpleEntity ) ); + + AnotherSimpleEntity anotherSimpleEntity = otherEntity.getAnotherSimpleEntity(); + // the ManyToOne is eager but the value is not null so a second query is executed + assertTrue( Hibernate.isInitialized( anotherSimpleEntity ) ); } ); } @Test - public void testSelectWithFetchJoin(SessionFactoryScope scope) { + public void testHQLSelectWithFetchJoin(SessionFactoryScope scope) { scope.inTransaction( session -> { OtherEntity otherEntity = session. @@ -71,7 +108,8 @@ public class LazyManyToOneTest { assertTrue( Hibernate.isInitialized( otherEntity.getSimpleEntity() ) ); assertThat( otherEntity.getSimpleEntity(), notNullValue() ); assertThat( otherEntity.getSimpleEntity().getName(), is( "Fab" ) ); - assertFalse( Hibernate.isInitialized( otherEntity.getAnotherSimpleEntity() ) ); + AnotherSimpleEntity anotherSimpleEntity = otherEntity.getAnotherSimpleEntity(); + assertTrue( Hibernate.isInitialized( anotherSimpleEntity ) ); } ); } @@ -81,20 +119,22 @@ public class LazyManyToOneTest { scope.inTransaction( session -> { OtherEntity otherEntity = session. - createQuery( "from OtherEntity o join fetch o.simpleEntity left join fetch o.anotherSimpleEntity", OtherEntity.class ) + createQuery( + "from OtherEntity o join fetch o.simpleEntity left join fetch o.anotherSimpleEntity", + OtherEntity.class + ) .uniqueResult(); assertThat( otherEntity.getName(), is( "Bar" ) ); assertTrue( Hibernate.isInitialized( otherEntity.getSimpleEntity() ) ); assertThat( otherEntity.getSimpleEntity(), notNullValue() ); assertThat( otherEntity.getSimpleEntity().getName(), is( "Fab" ) ); - assertTrue (Hibernate.isInitialized( otherEntity.getAnotherSimpleEntity() ) ); - assertThat( otherEntity.getAnotherSimpleEntity(), nullValue( ) ); + assertTrue( Hibernate.isInitialized( otherEntity.getAnotherSimpleEntity() ) ); + assertThat( otherEntity.getAnotherSimpleEntity(), nullValue() ); } ); } - @Test public void testGet(SessionFactoryScope scope) { scope.inTransaction( @@ -108,6 +148,23 @@ public class LazyManyToOneTest { ); } + @Test + public void testDelete(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.remove( session.get( OtherEntity.class, 2 ) ); + } + + ); + scope.inTransaction( + session -> { + assertThat( session.get( OtherEntity.class, 2 ), nullValue() ); + assertThat( session.get( SimpleEntity.class, 1 ), notNullValue() ); + assertThat( session.get( AnotherSimpleEntity.class, 3 ), notNullValue() ); + } + ); + } + @BeforeEach public void setUp(SessionFactoryScope scope) { scope.inTransaction( @@ -145,6 +202,7 @@ public class LazyManyToOneTest { public static class OtherEntity { private Integer id; private String name; + private SimpleEntity simpleEntity; private AnotherSimpleEntity anotherSimpleEntity;