HHH-13725 - Implement ToOne Associations support

This commit is contained in:
Andrea Boriero 2019-11-13 17:03:49 +00:00 committed by Steve Ebersole
parent a99881a103
commit 09d1dd3daf
6 changed files with 297 additions and 227 deletions

View File

@ -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
);
}

View File

@ -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 {
}

View File

@ -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<Initializer> 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<Object> listener) {
if ( entityInstance != null ) {
listener.accept( entityInstance );
}
else {
super.registerResolutionListener( listener );
}
}
}
}

View File

@ -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<Initializer> 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 );
}
}

View File

@ -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<Object> listener) {
if ( entityInstance != null ) {
listener.accept( entityInstance );
}
else {
super.registerResolutionListener( listener );
}
}
}

View File

@ -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;