From 8a196bc0e59c3da599381106232399f38499e66c Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Fri, 22 Nov 2019 19:05:43 +0000 Subject: [PATCH] HHH-13725: Implement ToOne Associations support --- .../SingularAssociationAttributeMapping.java | 30 ++++- .../walking/internal/FetchStrategyHelper.java | 9 ++ .../internal/StandardSqmSelectTranslator.java | 5 +- .../domain/entity/DelayedEntityFetchImpl.java | 8 +- .../entity/DelayedEntityFetchInitializer.java | 62 ++++----- .../domain/entity/SelectEntityFetchImpl.java | 57 +++++++++ .../entity/SelectEntityInitializer.java | 121 ++++++++++++++++++ .../sql/exec/manytoone/ManyToOneTest.java | 43 +++++++ 8 files changed, 284 insertions(+), 51 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/SelectEntityFetchImpl.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/SelectEntityInitializer.java 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 aaa4a3c752..0ea06e2cb9 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 @@ -35,6 +35,7 @@ import org.hibernate.sql.ast.tree.from.TableReferenceCollector; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.results.internal.domain.entity.DelayedEntityFetchImpl; import org.hibernate.sql.results.internal.domain.entity.EntityFetch; +import org.hibernate.sql.results.internal.domain.entity.SelectEntityFetchImpl; import org.hibernate.sql.results.spi.DomainResult; import org.hibernate.sql.results.spi.DomainResultCreationState; import org.hibernate.sql.results.spi.Fetch; @@ -47,10 +48,12 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu implements EntityValuedModelPart, TableGroupJoinProducer { private final String sqlAliasStem; private final boolean isNullable; + private final boolean referringPrimaryKey; + final protected boolean unwrapProxy; + private final String referencedPropertyName; + private ForeignKeyDescriptor foreignKeyDescriptor; - private final String referencedPropertyName; - private final boolean referringPrimaryKey; public SingularAssociationAttributeMapping( String name, @@ -74,6 +77,7 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu this.isNullable = value.isNullable(); referencedPropertyName = value.getReferencedPropertyName(); referringPrimaryKey = value.isReferenceToPrimaryKey(); + unwrapProxy = value.isUnwrapProxy(); } public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) { @@ -144,7 +148,7 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu fetchParent, this, lockMode, - !selected, + true, fetchablePath, creationState ); @@ -160,14 +164,23 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu .createDomainResult( fetchablePath, lhsTableGroup, null, creationState ); } + if ( fetchTiming == FetchTiming.IMMEDIATE && !selected ) { + return new SelectEntityFetchImpl( + fetchParent, + this, + lockMode, + fetchablePath, + result + ); + } + return new DelayedEntityFetchImpl( fetchParent, this, lockMode, isNullable, fetchablePath, - result, - creationState + result ); } @@ -280,4 +293,11 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu return false; } + public boolean isNullable() { + return isNullable; + } + + public boolean isUnwrapProxy() { + return unwrapProxy; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/walking/internal/FetchStrategyHelper.java b/hibernate-core/src/main/java/org/hibernate/persister/walking/internal/FetchStrategyHelper.java index 70d46f62ab..02bae3d67d 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/walking/internal/FetchStrategyHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/walking/internal/FetchStrategyHelper.java @@ -15,6 +15,7 @@ import org.hibernate.engine.profile.FetchProfile; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.loader.PropertyPath; +import org.hibernate.mapping.ToOne; import org.hibernate.persister.collection.AbstractCollectionPersister; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; @@ -119,6 +120,14 @@ public final class FetchStrategyHelper { FetchStyle style, AssociationType type, SessionFactoryImplementor sessionFactory) { + if ( type instanceof ToOne ) { + if ( ( (ToOne) type ).isLazy() ) { + return FetchTiming.DELAYED; + } + else { + return FetchTiming.IMMEDIATE; + } + } switch ( style ) { case JOIN: { return FetchTiming.IMMEDIATE; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/StandardSqmSelectTranslator.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/StandardSqmSelectTranslator.java index d33db3b39a..8e47fb567f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/StandardSqmSelectTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/StandardSqmSelectTranslator.java @@ -229,7 +229,10 @@ public class StandardSqmSelectTranslator // because it assert getFromClauseIndex().getTableGroup( fetchablePath ) != null; - fetchTiming = FetchTiming.IMMEDIATE; +// + if ( fetchedJoin.isFetched() ) { + fetchTiming = FetchTiming.IMMEDIATE; + } joined = true; alias = fetchedJoin.getExplicitAlias(); lockMode = determineLockMode( alias ); 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 index 1c753201af..e5e0a2fc6e 100644 --- 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 @@ -26,7 +26,7 @@ import org.hibernate.sql.results.spi.Initializer; */ public class DelayedEntityFetchImpl extends AbstractEntityFecth { - private DomainResult result; + private final DomainResult result; public DelayedEntityFetchImpl( FetchParent fetchParent, @@ -34,8 +34,7 @@ public class DelayedEntityFetchImpl extends AbstractEntityFecth { LockMode lockMode, boolean nullable, NavigablePath navigablePath, - DomainResult result, - DomainResultCreationState creationState) { + DomainResult result) { super( fetchParent, fetchedAttribute, navigablePath, nullable, lockMode ); this.result = result; } @@ -47,10 +46,7 @@ public class DelayedEntityFetchImpl extends AbstractEntityFecth { AssemblerCreationState creationState) { final SingularAssociationAttributeMapping fetchedAttribute = (SingularAssociationAttributeMapping) getFetchedMapping(); return new DelayedEntityFetchInitializer( - parentAccess, getNavigablePath(), - fetchedAttribute.getMappedFetchStrategy(), - getLockMode(), (EntityPersister) fetchedAttribute.getMappedTypeDescriptor(), result.createResultAssembler( collector, creationState ) ); 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 index 683264a021..23e8744e59 100644 --- 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 @@ -8,16 +8,12 @@ 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.FetchStrategy; -import org.hibernate.engine.FetchTiming; 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; /** @@ -26,31 +22,20 @@ import org.hibernate.sql.results.spi.RowProcessingState; */ public class DelayedEntityFetchInitializer extends AbstractFetchParentAccess implements EntityInitializer { - private final FetchParentAccess parentAccess; private final NavigablePath navigablePath; - private FetchStrategy mappedFetchedStrategy; - private LockMode lockMode; private final EntityPersister concreteDescriptor; - private final DomainResultAssembler fkValueAssembler; - + private final DomainResultAssembler identifierAssembler; private Object entityInstance; - private Object fkValue; + private Object identifier; protected DelayedEntityFetchInitializer( - FetchParentAccess parentAccess, NavigablePath fetchedNavigable, - FetchStrategy mappedFetchedStrategy, - LockMode lockMode, EntityPersister concreteDescriptor, - DomainResultAssembler fkValueAssembler - ) { - this.parentAccess = parentAccess; + DomainResultAssembler identifierAssembler) { this.navigablePath = fetchedNavigable; - this.mappedFetchedStrategy = mappedFetchedStrategy; - this.lockMode = lockMode; this.concreteDescriptor = concreteDescriptor; - this.fkValueAssembler = fkValueAssembler; + this.identifierAssembler = identifierAssembler; } @Override @@ -65,34 +50,32 @@ public class DelayedEntityFetchInitializer extends AbstractFetchParentAccess imp @Override public void resolveInstance(RowProcessingState rowProcessingState) { - fkValue = fkValueAssembler.assemble( rowProcessingState ); + if ( entityInstance != null ) { + return; + } + identifier = identifierAssembler.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 ) { + if ( identifier == null ) { // todo (6.0) : check this is the correct behaviour entityInstance = null; } else { - if ( mappedFetchedStrategy.getTiming() != FetchTiming.IMMEDIATE ) { - if ( concreteDescriptor.hasProxy() ) { - entityInstance = concreteDescriptor.createProxy( - fkValue, - rowProcessingState.getSession() - ); - } - else if ( concreteDescriptor - .getBytecodeEnhancementMetadata() - .isEnhancedForLazyLoading() ) { - entityInstance = concreteDescriptor.instantiate( - fkValue, - rowProcessingState.getSession() - ); - } + if ( concreteDescriptor.hasProxy() ) { + entityInstance = concreteDescriptor.createProxy( + identifier, + rowProcessingState.getSession() + ); } - else { - entityInstance = rowProcessingState.getSession().immediateLoad( concreteDescriptor.getEntityName(), fkValue ); + else if ( concreteDescriptor + .getBytecodeEnhancementMetadata() + .isEnhancedForLazyLoading() ) { + entityInstance = concreteDescriptor.instantiate( + identifier, + rowProcessingState.getSession() + ); } notifyParentResolutionListeners( entityInstance ); @@ -107,7 +90,7 @@ public class DelayedEntityFetchInitializer extends AbstractFetchParentAccess imp @Override public void finishUpRow(RowProcessingState rowProcessingState) { entityInstance = null; - fkValue = null; + identifier = null; clearParentResolutionListeners(); } @@ -136,4 +119,5 @@ public class DelayedEntityFetchInitializer extends AbstractFetchParentAccess imp super.registerResolutionListener( listener ); } } + } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/SelectEntityFetchImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/SelectEntityFetchImpl.java new file mode 100644 index 0000000000..ae544828fb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/SelectEntityFetchImpl.java @@ -0,0 +1,57 @@ +/* + * 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.EntityInitializer; +import org.hibernate.sql.results.spi.FetchParent; +import org.hibernate.sql.results.spi.FetchParentAccess; +import org.hibernate.sql.results.spi.Initializer; + +/** + * @author Andrea Boriero + */ +public class SelectEntityFetchImpl extends AbstractEntityFecth { + + private final DomainResult result; + + public SelectEntityFetchImpl( + FetchParent fetchParent, + SingularAssociationAttributeMapping fetchedAttribute, + LockMode lockMode, + NavigablePath navigablePath, + DomainResult result) { + super( fetchParent, fetchedAttribute, navigablePath, fetchedAttribute.isNullable(), lockMode ); + this.result = result; + } + + + @Override + protected EntityInitializer getEntityInitializer( + FetchParentAccess parentAccess, + Consumer collector, + AssemblerCreationState creationState) { + final SingularAssociationAttributeMapping fetchedAttribute = (SingularAssociationAttributeMapping) getFetchedMapping(); + + return new SelectEntityInitializer( + getNavigablePath(), + (EntityPersister) fetchedAttribute.getMappedTypeDescriptor(), + result.createResultAssembler( collector, creationState ), + fetchedAttribute.isUnwrapProxy(), + fetchedAttribute.isNullable() + ); + } + + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/SelectEntityInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/SelectEntityInitializer.java new file mode 100644 index 0000000000..fd54da6938 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/domain/entity/SelectEntityInitializer.java @@ -0,0 +1,121 @@ +/* + * 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.engine.spi.SharedSessionContractImplementor; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.proxy.HibernateProxy; +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.RowProcessingState; + +/** + * @author Andrea Boriero + */ +public class SelectEntityInitializer extends AbstractFetchParentAccess implements EntityInitializer { + + private final NavigablePath navigablePath; + private final EntityPersister concreteDescriptor; + private final DomainResultAssembler identifierAssembler; + private final boolean unwrapProxy; + private final boolean nullable; + + private Object entityInstance; + + protected SelectEntityInitializer( + NavigablePath fetchedNavigable, + EntityPersister concreteDescriptor, + DomainResultAssembler identifierAssembler, + boolean unwrapProxy, + boolean nullable) { + this.navigablePath = fetchedNavigable; + this.concreteDescriptor = concreteDescriptor; + this.identifierAssembler = identifierAssembler; + this.unwrapProxy = unwrapProxy; + this.nullable = nullable; + } + + @Override + public NavigablePath getNavigablePath() { + return navigablePath; + } + + @Override + public void resolveKey(RowProcessingState rowProcessingState) { + // nothing to do + } + + @Override + public void resolveInstance(RowProcessingState rowProcessingState) { + + } + + @Override + public void initializeInstance(RowProcessingState rowProcessingState) { + if ( entityInstance != null ) { + return; + } + + final Object id = identifierAssembler.assemble( rowProcessingState ); + if ( id == null ) { + return; + } + + final String entityName = concreteDescriptor.getEntityName(); + final SharedSessionContractImplementor session = rowProcessingState.getSession(); + + entityInstance = session.internalLoad( + entityName, + id, + false, + nullable + ); + + if ( entityInstance instanceof HibernateProxy ) { + final boolean isProxyUnwrapEnabled = unwrapProxy && concreteDescriptor.isInstrumented(); + + ( (HibernateProxy) entityInstance ).getHibernateLazyInitializer().setUnwrap( isProxyUnwrapEnabled ); + } + } + + @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/test/java/org/hibernate/orm/test/sql/exec/manytoone/ManyToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/ManyToOneTest.java index 6ce23c86ed..f6fbb18252 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/ManyToOneTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/exec/manytoone/ManyToOneTest.java @@ -103,6 +103,7 @@ public class ManyToOneTest { AnotherSimpleEntity anotherSimpleEntity = otherEntity.getAnotherSimpleEntity(); assertTrue( Hibernate.isInitialized( anotherSimpleEntity ) ); + assertThat( anotherSimpleEntity.getName(), is( "other" ) ); assertThat( statistics.getPrepareStatementCount(), is( 2L ) ); } @@ -118,6 +119,7 @@ public class ManyToOneTest { OtherEntity otherEntity = session. createQuery( "from OtherEntity o join o.simpleEntity", OtherEntity.class ) .uniqueResult(); + // the eager association is null assertThat( statistics.getPrepareStatementCount(), is( 1L ) ); assertThat( otherEntity.getName(), is( "Bar" ) ); @@ -134,8 +136,49 @@ public class ManyToOneTest { assertThat( statistics.getPrepareStatementCount(), is( 2L ) ); } ); + + 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 ); + } + ); + + statistics.clear(); + + scope.inTransaction( + session -> { + OtherEntity otherEntity = session. + createQuery( "from OtherEntity o join o.simpleEntity", OtherEntity.class ) + .uniqueResult(); + // the eager association is not null so a second select is executed + assertThat( statistics.getPrepareStatementCount(), is( 2L ) ); + assertThat( otherEntity.getName(), is( "Bar" ) ); + + SimpleEntity simpleEntity = otherEntity.getSimpleEntity(); + assertFalse( Hibernate.isInitialized( simpleEntity ) ); + assertThat( simpleEntity, notNullValue() ); + assertThat( simpleEntity.getName(), is( "Fab" ) ); + + assertThat( statistics.getPrepareStatementCount(), is( 3L ) ); + + AnotherSimpleEntity anotherSimpleEntity = otherEntity.getAnotherSimpleEntity(); + assertTrue( Hibernate.isInitialized( anotherSimpleEntity ) ); + assertThat( anotherSimpleEntity.getName(), is( "other" ) ); + + assertThat( statistics.getPrepareStatementCount(), is( 3L ) ); + } + ); + } + @Test public void testHQLSelectWithFetchJoin(SessionFactoryScope scope) { StatisticsImplementor statistics = scope.getSessionFactory().getStatistics();