HHH-17818 Introduce new `@ConcreteProxy` annotation

Also, preserve laziness for optional + ConcreteType associations
This commit is contained in:
Marco Belladelli 2024-03-07 10:39:07 +01:00 committed by Marco Belladelli
parent 2bc78d50b0
commit 84cb94b990
22 changed files with 1277 additions and 46 deletions

View File

@ -0,0 +1,52 @@
/*
* 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.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.hibernate.Incubating;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Annotating {@link ConcreteProxy} on the root entity class of an inheritance hierarchy
* will allow types of that hierarchy to always produce proxies that resolve to the concrete
* subtype class. This means both {@linkplain jakarta.persistence.FetchType#LAZY lazy associations}
* and {@linkplain org.hibernate.Session#getReference plain references} can safely be used
* with {@code instanceof} checks and type-casts.
* <p>
* Note that the table(s) of an entity <strong>might need to be accessed</strong> to
* determine the concrete proxy type:
* <ul>
* <li>
* With {@linkplain jakarta.persistence.InheritanceType#SINGLE_TABLE single table} inheritance,
* the {@linkplain jakarta.persistence.DiscriminatorColumn discriminator column} value
* will be {@code left join}ed when fetching associations or simply read from the entity
* table when getting references.
* </li>
* <li>
* When using {@linkplain jakarta.persistence.InheritanceType#JOINED joined} inheritance,
* all subtype tables will need to be {@code left join}ed to determine the concrete type.
* Note however that when using an explicit {@linkplain jakarta.persistence.DiscriminatorColumn
* discriminator column}, the behavior is the same as for single-table inheritance.
* </li>
* <li>
* Finally, for {@linkplain jakarta.persistence.InheritanceType#TABLE_PER_CLASS table-per-class}
* inheritance, all subtype tables will need to be (union) queried to determine the concrete type.
* </li>
* </ul>
*
* @author Marco Belladelli
* @since 6.6
*/
@Target( TYPE )
@Retention( RUNTIME )
@Incubating
public @interface ConcreteProxy {
}

View File

@ -29,6 +29,7 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.CacheLayout;
import org.hibernate.annotations.Check;
import org.hibernate.annotations.Checks;
import org.hibernate.annotations.ConcreteProxy;
import org.hibernate.annotations.DiscriminatorFormula;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
@ -1303,6 +1304,7 @@ public class EntityBinder {
bindOptimisticLocking();
bindPolymorphism();
bindProxy();
bindConcreteProxy();
bindWhere();
bindCache();
bindNaturalIdCache();
@ -1591,6 +1593,17 @@ public class EntityBinder {
return isDefault( proxyClass, context ) ? annotatedClass : proxyClass;
}
public void bindConcreteProxy() {
final ConcreteProxy concreteProxy = annotatedClass.getAnnotation( ConcreteProxy.class );
if ( concreteProxy != null ) {
if ( persistentClass.getSuperclass() != null ) {
throw new AnnotationException( "Entity class '" + persistentClass.getClassName()
+ "' is annotated '@ConcreteProxy' but it is not the root of the entity inheritance hierarchy" );
}
persistentClass.getRootClass().setConcreteProxy( true );
}
}
public void bindWhere() {
final Where where = getOverridableAnnotation( annotatedClass, Where.class, context );
if ( where != null ) {

View File

@ -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
*/
/*
* 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.loader.ast.internal;
import java.util.List;
import org.hibernate.LockOptions;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.WrongClassException;
import org.hibernate.annotations.ConcreteProxy;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
import org.hibernate.sql.ast.tree.select.SelectStatement;
import org.hibernate.sql.exec.internal.BaseExecutionContext;
import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl;
import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect;
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
import org.hibernate.sql.exec.spi.JdbcParametersList;
import org.hibernate.sql.results.internal.RowTransformerStandardImpl;
import org.hibernate.sql.results.spi.ListResultsConsumer;
import static java.util.Collections.singletonList;
/**
* Utility class that caches the SQL AST needed to read the discriminator value
* associated with the provided {@link EntityPersister} and returns the
* resolved concrete entity type.
*
* @author Marco Belladelli
* @see ConcreteProxy
*/
public class EntityConcreteTypeLoader {
private final EntityMappingType entityDescriptor;
private final SelectStatement sqlSelect;
private final JdbcParametersList jdbcParameters;
public EntityConcreteTypeLoader(EntityMappingType entityDescriptor, SessionFactoryImplementor sessionFactory) {
this.entityDescriptor = entityDescriptor;
final EntityDiscriminatorMapping discriminatorMapping = entityDescriptor.getDiscriminatorMapping();
final JdbcParametersList.Builder jdbcParametersBuilder = JdbcParametersList.newBuilder();
sqlSelect = LoaderSelectBuilder.createSelect(
entityDescriptor,
singletonList( discriminatorMapping ),
entityDescriptor.getIdentifierMapping(),
null,
1,
new LoadQueryInfluencers( sessionFactory ),
LockOptions.NONE,
jdbcParametersBuilder::add,
sessionFactory
);
jdbcParameters = jdbcParametersBuilder.build();
}
public EntityMappingType getConcreteType(Object id, SharedSessionContractImplementor session) {
final SessionFactoryImplementor sessionFactory = session.getSessionFactory();
final SqlAstTranslatorFactory sqlAstTranslatorFactory = sessionFactory.getJdbcServices()
.getJdbcEnvironment()
.getSqlAstTranslatorFactory();
final JdbcParameterBindings jdbcParamBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() );
int offset = jdbcParamBindings.registerParametersForEachJdbcValue(
id,
entityDescriptor.getIdentifierMapping(),
jdbcParameters,
session
);
assert offset == jdbcParameters.size();
final JdbcOperationQuerySelect jdbcSelect =
sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory, sqlSelect )
.translate( jdbcParamBindings, QueryOptions.NONE );
final List<Object> results = session.getFactory().getJdbcServices().getJdbcSelectExecutor().list(
jdbcSelect,
jdbcParamBindings,
new BaseExecutionContext( session ),
RowTransformerStandardImpl.instance(),
ListResultsConsumer.UniqueSemantic.NONE
);
if ( results.isEmpty() ) {
throw new ObjectNotFoundException( entityDescriptor.getEntityName(), id );
}
else {
assert results.size() == 1;
final Object result = results.get( 0 );
final MappingMetamodelImplementor mappingMetamodel = sessionFactory.getRuntimeMetamodels()
.getMappingMetamodel();
final EntityPersister concreteType = result instanceof Class<?>
? mappingMetamodel.getEntityDescriptor( (Class<?>) result )
: mappingMetamodel.getEntityDescriptor( (String) result );
if ( !concreteType.isTypeOrSuperType( entityDescriptor ) ) {
throw new WrongClassException(
concreteType.getEntityName(),
id,
entityDescriptor.getEntityName(),
result
);
}
return concreteType;
}
}
}

View File

@ -29,6 +29,7 @@ import org.hibernate.event.spi.LoadEventListener;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.graph.RootGraph;
import org.hibernate.graph.spi.RootGraphImplementor;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.type.descriptor.java.JavaType;
@ -128,16 +129,16 @@ public class IdentifierLoadAccessImpl<T> implements IdentifierLoadAccess<T>, Jav
protected T doGetReference(Object id) {
final SessionImplementor session = context.getSession();
final SessionFactoryImplementor factory = session.getFactory();
final EntityMappingType concreteType = entityPersister.resolveConcreteProxyTypeForId( id, session );
return (T) getReference(
coerceId( id, factory ),
session.asEventSource(),
factory,
entityPersister.getEntityName(),
concreteType.getEntityName(),
isReadOnly( session )
);
}
private Boolean isReadOnly(SessionImplementor session) {
return readOnly != null
? readOnly

View File

@ -415,6 +415,8 @@ public abstract class PersistentClass implements IdentifiableTypeClass, Attribut
this.lazy = lazy;
}
public abstract boolean isConcreteProxy();
public abstract boolean hasEmbeddedIdentifier();
public abstract Class<? extends EntityPersister> getEntityPersisterClass();

View File

@ -51,6 +51,7 @@ public class RootClass extends PersistentClass implements TableOwner, SoftDeleta
private boolean explicitPolymorphism;
private Class<? extends EntityPersister> entityPersisterClass;
private boolean forceDiscriminator;
private boolean concreteProxy;
private String where;
private Table table;
private boolean discriminatorInsertable = true;
@ -262,6 +263,15 @@ public class RootClass extends PersistentClass implements TableOwner, SoftDeleta
this.forceDiscriminator = forceDiscriminator;
}
@Override
public boolean isConcreteProxy() {
return concreteProxy;
}
public void setConcreteProxy(boolean concreteProxy) {
this.concreteProxy = concreteProxy;
}
@Override
public String getWhere() {
return where;

View File

@ -203,6 +203,11 @@ public class Subclass extends PersistentClass {
return getSuperclass().isExplicitPolymorphism();
}
@Override
public boolean isConcreteProxy() {
return getRootClass().isConcreteProxy();
}
public void setSuperclass(PersistentClass superclass) {
this.superclass = superclass;
}

View File

@ -15,7 +15,9 @@ import java.util.function.Consumer;
import java.util.function.Supplier;
import org.hibernate.Filter;
import org.hibernate.Incubating;
import org.hibernate.Internal;
import org.hibernate.annotations.ConcreteProxy;
import org.hibernate.boot.jaxb.mapping.JaxbEntity;
import org.hibernate.engine.OptimisticLockStyle;
import org.hibernate.engine.spi.LoadQueryInfluencers;
@ -243,7 +245,6 @@ public interface EntityMappingType
return getDiscriminatorValue().toString();
}
default EntityMappingType getRootEntityDescriptor() {
final EntityMappingType superMappingType = getSuperMappingType();
if ( superMappingType == null ) {
@ -326,6 +327,32 @@ public interface EntityMappingType
*/
EntityDiscriminatorMapping getDiscriminatorMapping();
/**
* Returns {@code true} if this entity type's hierarchy is configured to return
* {@linkplain ConcreteProxy concrete-typed} proxies.
*
* @see ConcreteProxy
* @since 6.6
*/
@Incubating
default boolean isConcreteProxy() {
return false;
}
/**
* If this entity is configured to return {@linkplain ConcreteProxy concrete-typed}
* proxies, this method queries the entity table(s) do determine the concrete entity type
* associated with the provided id and returns its persister. Otherwise, this method
* simply returns this entity persister.
*
* @see #isConcreteProxy()
* @since 6.6
*/
@Incubating
default EntityMappingType resolveConcreteProxyTypeForId(Object id, SharedSessionContractImplementor session) {
return this;
}
/**
* Mapping details for the entity's version when using the
* {@linkplain OptimisticLockStyle#VERSION version strategy}.

View File

@ -11,6 +11,7 @@ import java.util.LinkedHashMap;
import org.hibernate.engine.FetchTiming;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.metamodel.mapping.DiscriminatorType;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
import org.hibernate.persister.entity.JoinedSubclassEntityPersister;
@ -28,6 +29,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.ast.tree.predicate.NullnessPredicate;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.basic.BasicFetch;
@ -76,6 +78,17 @@ public class CaseStatementDiscriminatorMappingImpl extends AbstractDiscriminator
return false;
}
@SuppressWarnings( "rawtypes" )
@Override
public DomainResult createDomainResult(
NavigablePath navigablePath,
TableGroup tableGroup,
String resultVariable,
DomainResultCreationState creationState) {
resolveSubTypeTableReferences( tableGroup, navigablePath );
return super.createDomainResult( navigablePath, tableGroup, resultVariable, creationState );
}
@Override
public BasicFetch<?> generateFetch(
FetchParent fetchParent,
@ -88,18 +101,23 @@ public class CaseStatementDiscriminatorMappingImpl extends AbstractDiscriminator
final TableGroup tableGroup = sqlAstCreationState.getFromClauseAccess().getTableGroup(
fetchParent.getNavigablePath()
);
// Since the expression is lazy, based on the available table reference joins,
// we need to force the initialization in case this is a fetch
tableDiscriminatorDetailsMap.forEach(
(tableName, tableDiscriminatorDetails) -> tableGroup.getTableReference(
fetchablePath,
tableName,
true
)
);
resolveSubTypeTableReferences( tableGroup, fetchablePath );
return super.generateFetch( fetchParent, fetchablePath, fetchTiming, selected, resultVariable, creationState );
}
private void resolveSubTypeTableReferences(TableGroup tableGroup, NavigablePath navigablePath) {
final EntityMappingType entityDescriptor = (EntityMappingType) tableGroup.getModelPart().getPartMappingType();
// Since the expression is lazy, based on the available table reference joins,
// we need to force the initialization in case this is selected
for ( EntityMappingType subMappingType : entityDescriptor.getSubMappingTypes() ) {
tableGroup.getTableReference(
navigablePath,
subMappingType.getMappedTableDetails().getTableName(),
true
);
}
}
@Override
public Expression resolveSqlExpression(
NavigablePath navigablePath,

View File

@ -1071,7 +1071,8 @@ public class ToOneAttributeMapping
fetchParent,
isSelectByUniqueKey( sideNature ),
parentNavigablePath,
foreignKeyDomainResult
foreignKeyDomainResult,
creationState
);
}
return null;
@ -1357,12 +1358,17 @@ public class ToOneAttributeMapping
);
}
if ( entityMappingType.isConcreteProxy() && sideNature == ForeignKeyDescriptor.Nature.TARGET ) {
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState );
}
return buildEntityDelayedFetch(
fetchParent,
this,
fetchablePath,
domainResult,
isSelectByUniqueKey( sideNature )
isSelectByUniqueKey( sideNature ),
creationState
);
}
}
@ -1375,8 +1381,16 @@ public class ToOneAttributeMapping
ToOneAttributeMapping fetchedAttribute,
NavigablePath navigablePath,
DomainResult<?> keyResult,
boolean selectByUniqueKey) {
return new EntityDelayedFetchImpl( fetchParent, fetchedAttribute, navigablePath, keyResult, selectByUniqueKey );
boolean selectByUniqueKey,
DomainResultCreationState creationState) {
return new EntityDelayedFetchImpl(
fetchParent,
fetchedAttribute,
navigablePath,
keyResult,
selectByUniqueKey,
creationState
);
}
/**
@ -1641,13 +1655,7 @@ public class ToOneAttributeMapping
}
final boolean selectByUniqueKey = isSelectByUniqueKey( side );
// Consider all associations annotated with @NotFound as EAGER
// and LAZY one-to-one that are not instrumented and not optional
if ( fetchTiming == FetchTiming.IMMEDIATE
|| hasNotFoundAction()
|| getAssociatedEntityMappingType().getSoftDeleteMapping() != null
|| ( !entityMappingType.getEntityPersister().isInstrumented()
&& cardinality == Cardinality.ONE_TO_ONE && isOptional ) ) {
if ( needsImmediateFetch( fetchTiming ) ) {
return buildEntityFetchSelect(
fetchParent,
this,
@ -1658,15 +1666,39 @@ public class ToOneAttributeMapping
);
}
if ( entityMappingType.isConcreteProxy() && sideNature == ForeignKeyDescriptor.Nature.TARGET ) {
createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState );
}
return buildEntityDelayedFetch(
fetchParent,
this,
fetchablePath,
keyResult,
selectByUniqueKey
selectByUniqueKey,
creationState
);
}
private boolean needsImmediateFetch(FetchTiming fetchTiming) {
if ( fetchTiming == FetchTiming.IMMEDIATE ) {
return true;
}
else if ( !entityMappingType.isConcreteProxy() ) {
// Consider all associations annotated with @NotFound as EAGER
// and LAZY one-to-one that are not instrumented and not optional.
// When resolving the concrete entity type we can preserve laziness
// and handle not found actions based on the discriminator value
return hasNotFoundAction()
|| entityMappingType.getSoftDeleteMapping() != null
|| ( !entityMappingType.getEntityPersister().isInstrumented()
&& cardinality == Cardinality.ONE_TO_ONE && isOptional );
}
else {
return false;
}
}
private TableGroup determineTableGroupForFetch(
NavigablePath fetchablePath,
FetchParent fetchParent,

View File

@ -195,6 +195,7 @@ import org.hibernate.metamodel.mapping.internal.CompoundNaturalIdMapping;
import org.hibernate.metamodel.mapping.internal.DiscriminatedAssociationAttributeMapping;
import org.hibernate.metamodel.mapping.internal.DiscriminatorTypeImpl;
import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping;
import org.hibernate.loader.ast.internal.EntityConcreteTypeLoader;
import org.hibernate.metamodel.mapping.internal.EntityRowIdMappingImpl;
import org.hibernate.metamodel.mapping.internal.EntityVersionMappingImpl;
import org.hibernate.metamodel.mapping.internal.ExplicitColumnDiscriminatorMappingImpl;
@ -452,6 +453,8 @@ public abstract class AbstractEntityPersister
private EntityMappingType superMappingType;
private SortedMap<String, EntityMappingType> subclassMappingTypes;
private final boolean concreteProxy;
private EntityConcreteTypeLoader concreteTypeLoader;
private EntityIdentifierMapping identifierMapping;
private NaturalIdMapping naturalIdMapping;
@ -542,6 +545,10 @@ public abstract class AbstractEntityPersister
assert javaType != null;
this.implementsLifecycle = Lifecycle.class.isAssignableFrom( javaType.getJavaTypeClass() );
concreteProxy = isPolymorphic()
&& ( getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() || hasProxy() )
&& persistentClass.isConcreteProxy();
final Dialect dialect = creationContext.getDialect();
batchSize = persistentClass.getBatchSize() < 0
@ -4288,6 +4295,24 @@ public abstract class AbstractEntityPersister
return entityMetamodel.isExplicitPolymorphism();
}
@Override
public boolean isConcreteProxy() {
return concreteProxy;
}
@Override
public EntityMappingType resolveConcreteProxyTypeForId(Object id, SharedSessionContractImplementor session) {
if ( !concreteProxy ) {
return this;
}
EntityConcreteTypeLoader concreteTypeLoader = this.concreteTypeLoader;
if ( concreteTypeLoader == null ) {
this.concreteTypeLoader = concreteTypeLoader = new EntityConcreteTypeLoader( this, session.getFactory() );
}
return concreteTypeLoader.getConcreteType( id, session );
}
@Override
public String[] getKeyColumnNames() {
return getIdentifierColumnNames();

View File

@ -1240,7 +1240,8 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister {
@Override
public TableDetails getMappedTableDetails() {
return getTableMapping( getTableMappings().length - 1 );
// Subtract the number of secondary tables (tableSpan - coreTableSpan) and get the last table mapping
return getTableMapping( getTableMappings().length - ( tableSpan - coreTableSpan ) - 1 );
}
@Override

View File

@ -359,7 +359,7 @@ public abstract class AbstractEntityInitializer extends AbstractFetchParentAcces
);
}
else if ( concreteDescriptor != null
|| ( concreteDescriptor = determineConcreteEntityDescriptor( rowProcessingState ) ) != null ) {
|| ( concreteDescriptor = determineConcreteEntityDescriptor( rowProcessingState, discriminatorAssembler, entityDescriptor ) ) != null ) {
// 2) build the EntityKey
entityKey = new EntityKey( id, concreteDescriptor );
state = State.KEY_RESOLVED;
@ -375,7 +375,10 @@ public abstract class AbstractEntityInitializer extends AbstractFetchParentAcces
}
}
private EntityPersister determineConcreteEntityDescriptor(RowProcessingState rowProcessingState)
public static @Nullable EntityPersister determineConcreteEntityDescriptor(
RowProcessingState rowProcessingState,
BasicResultAssembler<?> discriminatorAssembler,
EntityPersister entityDescriptor)
throws WrongClassException {
if ( discriminatorAssembler == null
|| rowProcessingState.isQueryCacheHit() && !entityDescriptor.storeDiscriminatorInShallowQueryCacheLayout() ) {
@ -389,7 +392,8 @@ public abstract class AbstractEntityInitializer extends AbstractFetchParentAcces
final DiscriminatorValueDetails discriminatorDetails =
discriminatorMapping.resolveDiscriminatorValue( discriminator );
if ( discriminatorDetails == null ) {
return entityDescriptor;
assert discriminator == null : "Discriminator details should only be null for null values";
return null;
}
else {
final EntityMappingType indicatedEntity = discriminatorDetails.getIndicatedEntity();
@ -407,6 +411,7 @@ public abstract class AbstractEntityInitializer extends AbstractFetchParentAcces
}
}
}
private Object initializeIdentifier(RowProcessingState rowProcessingState) {
final JdbcValuesSourceProcessingState jdbcValuesSourceProcessingState =
rowProcessingState.getJdbcValuesSourceProcessingState();
@ -432,7 +437,7 @@ public abstract class AbstractEntityInitializer extends AbstractFetchParentAcces
// The id can only be the entity instance if this is a non-aggregated id that has no containing class
&& entityDescriptor.getIdentifierMapping() instanceof CompositeIdentifierMapping
&& !( (CompositeIdentifierMapping) entityDescriptor.getIdentifierMapping() ).hasContainingClass()
&& ( concreteDescriptor = determineConcreteEntityDescriptor( rowProcessingState ) ) != null
&& ( concreteDescriptor = determineConcreteEntityDescriptor( rowProcessingState, discriminatorAssembler, entityDescriptor ) ) != null
&& concreteDescriptor.isInstance( id );
}

View File

@ -14,17 +14,17 @@ import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.results.graph.AssemblerCreationState;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultAssembler;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.FetchParentAccess;
import org.hibernate.sql.results.graph.Fetchable;
import org.hibernate.sql.results.graph.InitializerProducer;
import org.hibernate.sql.results.graph.basic.BasicFetch;
import org.hibernate.sql.results.graph.entity.EntityFetch;
import org.hibernate.sql.results.graph.entity.EntityInitializer;
import org.hibernate.sql.results.graph.internal.ImmutableFetchList;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* @author Steve Ebersole
*/
@ -34,6 +34,7 @@ public abstract class AbstractNonJoinedEntityFetch implements EntityFetch,
private final ToOneAttributeMapping fetchedModelPart;
private final FetchParent fetchParent;
private final DomainResult<?> keyResult;
private final BasicFetch<?> discriminatorFetch;
private final boolean selectByUniqueKey;
public AbstractNonJoinedEntityFetch(
@ -41,11 +42,29 @@ public abstract class AbstractNonJoinedEntityFetch implements EntityFetch,
ToOneAttributeMapping fetchedModelPart,
FetchParent fetchParent,
DomainResult<?> keyResult,
boolean selectDiscriminator,
boolean selectByUniqueKey,
DomainResultCreationState creationState) {
this.navigablePath = navigablePath;
this.fetchedModelPart = fetchedModelPart;
this.fetchParent = fetchParent;
this.keyResult = keyResult;
this.discriminatorFetch = selectDiscriminator ? creationState.visitDiscriminatorFetch( this ) : null;
this.selectByUniqueKey = selectByUniqueKey;
}
protected AbstractNonJoinedEntityFetch(
NavigablePath navigablePath,
ToOneAttributeMapping fetchedModelPart,
FetchParent fetchParent,
DomainResult<?> keyResult,
BasicFetch<?> discriminatorFetch,
boolean selectByUniqueKey) {
this.navigablePath = navigablePath;
this.fetchedModelPart = fetchedModelPart;
this.fetchParent = fetchParent;
this.keyResult = keyResult;
this.discriminatorFetch = discriminatorFetch;
this.selectByUniqueKey = selectByUniqueKey;
}
@ -99,6 +118,9 @@ public abstract class AbstractNonJoinedEntityFetch implements EntityFetch,
if ( keyResult != null ) {
keyResult.collectValueIndexesToCache( valueIndexes );
}
if ( discriminatorFetch != null ) {
discriminatorFetch.collectValueIndexesToCache( valueIndexes );
}
}
@Override
@ -110,6 +132,10 @@ public abstract class AbstractNonJoinedEntityFetch implements EntityFetch,
return keyResult;
}
public BasicFetch<?> getDiscriminatorFetch() {
return discriminatorFetch;
}
public boolean isSelectByUniqueKey() {
return selectByUniqueKey;
}

View File

@ -11,8 +11,10 @@ import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.results.graph.AssemblerCreationState;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.FetchParentAccess;
import org.hibernate.sql.results.graph.basic.BasicResultAssembler;
import org.hibernate.sql.results.graph.entity.EntityInitializer;
/**
@ -20,14 +22,22 @@ import org.hibernate.sql.results.graph.entity.EntityInitializer;
* @author Steve Ebersole
*/
public class EntityDelayedFetchImpl extends AbstractNonJoinedEntityFetch {
public EntityDelayedFetchImpl(
FetchParent fetchParent,
ToOneAttributeMapping fetchedAttribute,
NavigablePath navigablePath,
DomainResult<?> keyResult,
boolean selectByUniqueKey) {
super( navigablePath, fetchedAttribute, fetchParent, keyResult, selectByUniqueKey );
boolean selectByUniqueKey,
DomainResultCreationState creationState) {
super(
navigablePath,
fetchedAttribute,
fetchParent,
keyResult,
fetchedAttribute.getEntityMappingType().getEntityPersister().isConcreteProxy(),
selectByUniqueKey,
creationState
);
}
@Override
@ -42,7 +52,10 @@ public class EntityDelayedFetchImpl extends AbstractNonJoinedEntityFetch {
getNavigablePath(),
getEntityValuedModelPart(),
isSelectByUniqueKey(),
getKeyResult().createResultAssembler( parentAccess, creationState )
getKeyResult().createResultAssembler( parentAccess, creationState ),
getDiscriminatorFetch() != null
? (BasicResultAssembler<?>) getDiscriminatorFetch().createResultAssembler( parentAccess, creationState )
: null
);
}
}

View File

@ -8,6 +8,7 @@ package org.hibernate.sql.results.graph.entity.internal;
import java.util.function.Consumer;
import org.hibernate.FetchNotFoundException;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.engine.spi.EntityHolder;
import org.hibernate.engine.spi.EntityKey;
@ -26,12 +27,15 @@ import org.hibernate.sql.exec.spi.ExecutionContext;
import org.hibernate.sql.results.graph.DomainResultAssembler;
import org.hibernate.sql.results.graph.FetchParentAccess;
import org.hibernate.sql.results.graph.Initializer;
import org.hibernate.sql.results.graph.basic.BasicResultAssembler;
import org.hibernate.sql.results.graph.entity.EntityInitializer;
import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
import org.hibernate.type.Type;
import org.checkerframework.checker.nullness.qual.Nullable;
import static org.hibernate.sql.results.graph.entity.AbstractEntityInitializer.determineConcreteEntityDescriptor;
/**
* @author Andrea Boriero
* @author Steve Ebersole
@ -46,6 +50,7 @@ public class EntityDelayedFetchInitializer implements EntityInitializer {
private final ToOneAttributeMapping referencedModelPart;
private final boolean selectByUniqueKey;
private final DomainResultAssembler<?> identifierAssembler;
private final BasicResultAssembler<?> discriminatorAssembler;
protected boolean parentShallowCached;
@ -59,9 +64,10 @@ public class EntityDelayedFetchInitializer implements EntityInitializer {
NavigablePath fetchedNavigable,
ToOneAttributeMapping referencedModelPart,
boolean selectByUniqueKey,
DomainResultAssembler<?> identifierAssembler) {
// associations marked with `@NotFound` are ALWAYS eagerly fetched
assert referencedModelPart.getNotFoundAction() == null;
DomainResultAssembler<?> identifierAssembler,
BasicResultAssembler<?> discriminatorAssembler) {
// associations marked with `@NotFound` are ALWAYS eagerly fetched, unless we're resolving the concrete type
assert !referencedModelPart.hasNotFoundAction() || referencedModelPart.getEntityMappingType().isConcreteProxy();
this.parentAccess = parentAccess;
this.navigablePath = fetchedNavigable;
@ -71,6 +77,7 @@ public class EntityDelayedFetchInitializer implements EntityInitializer {
this.referencedModelPart = referencedModelPart;
this.selectByUniqueKey = selectByUniqueKey;
this.identifierAssembler = identifierAssembler;
this.discriminatorAssembler = discriminatorAssembler;
}
@Override
@ -109,7 +116,28 @@ public class EntityDelayedFetchInitializer implements EntityInitializer {
}
else {
final SharedSessionContractImplementor session = rowProcessingState.getSession();
final EntityPersister concreteDescriptor = referencedModelPart.getEntityMappingType().getEntityPersister();
final EntityPersister entityPersister = referencedModelPart.getEntityMappingType().getEntityPersister();
final EntityPersister concreteDescriptor;
if ( discriminatorAssembler != null ) {
concreteDescriptor = determineConcreteEntityDescriptor(
rowProcessingState,
discriminatorAssembler,
entityPersister
);
if ( concreteDescriptor == null ) {
// If we find no discriminator it means there's no entity in the target table
if ( !referencedModelPart.isOptional() ) {
throw new FetchNotFoundException( entityPersister.getEntityName(), identifier );
}
entityInstance = null;
return;
}
}
else {
concreteDescriptor = entityPersister;
}
final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
if ( selectByUniqueKey ) {
final String uniqueKeyPropertyName = referencedModelPart.getReferencedPropertyName();

View File

@ -29,8 +29,8 @@ public class EntityFetchSelectImpl extends AbstractNonJoinedEntityFetch {
NavigablePath navigablePath,
DomainResult<?> keyResult,
boolean selectByUniqueKey,
@SuppressWarnings("unused") DomainResultCreationState creationState) {
super( navigablePath, fetchedAttribute, fetchParent, keyResult, selectByUniqueKey );
DomainResultCreationState creationState) {
super( navigablePath, fetchedAttribute, fetchParent, keyResult, false, selectByUniqueKey, creationState );
}
/**
@ -42,6 +42,7 @@ public class EntityFetchSelectImpl extends AbstractNonJoinedEntityFetch {
original.getFetchedMapping(),
original.getFetchParent(),
original.getKeyResult(),
original.getDiscriminatorFetch(),
original.isSelectByUniqueKey()
);
}

View File

@ -14,9 +14,11 @@ import org.hibernate.sql.results.graph.AssemblerCreationState;
import org.hibernate.sql.results.graph.BiDirectionalFetch;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultAssembler;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.FetchParentAccess;
import org.hibernate.sql.results.graph.Initializer;
import org.hibernate.sql.results.graph.basic.BasicResultAssembler;
import org.hibernate.sql.results.graph.entity.EntityInitializer;
import org.hibernate.sql.results.graph.entity.internal.AbstractNonJoinedEntityFetch;
import org.hibernate.sql.results.graph.entity.internal.EntityDelayedFetchInitializer;
@ -41,8 +43,19 @@ public class CircularFetchImpl extends AbstractNonJoinedEntityFetch implements B
FetchParent fetchParent,
boolean selectByUniqueKey,
NavigablePath referencedNavigablePath,
DomainResult<?> keyResult) {
super( navigablePath, referencedModelPart, fetchParent, keyResult, selectByUniqueKey );
DomainResult<?> keyResult,
DomainResultCreationState creationState) {
super(
navigablePath,
referencedModelPart,
fetchParent,
keyResult,
timing == FetchTiming.DELAYED && referencedModelPart.getEntityMappingType()
.getEntityPersister()
.isConcreteProxy(),
selectByUniqueKey,
creationState
);
this.timing = timing;
this.referencedNavigablePath = referencedNavigablePath;
}
@ -53,6 +66,7 @@ public class CircularFetchImpl extends AbstractNonJoinedEntityFetch implements B
original.getFetchedMapping(),
original.getFetchParent(),
original.getKeyResult(),
original.getDiscriminatorFetch(),
original.isSelectByUniqueKey()
);
this.timing = original.timing;
@ -103,7 +117,10 @@ public class CircularFetchImpl extends AbstractNonJoinedEntityFetch implements B
getNavigablePath(),
getFetchedMapping(),
isSelectByUniqueKey(),
getKeyResult().createResultAssembler( parentAccess, creationState )
getKeyResult().createResultAssembler( parentAccess, creationState ),
getDiscriminatorFetch() != null
? (BasicResultAssembler<?>) getDiscriminatorFetch().createResultAssembler( parentAccess, creationState )
: null
);
}
}
@ -132,13 +149,15 @@ public class CircularFetchImpl extends AbstractNonJoinedEntityFetch implements B
NavigablePath referencedPath,
ToOneAttributeMapping fetchable,
boolean selectByUniqueKey,
DomainResultAssembler<?> resultAssembler) {
DomainResultAssembler<?> resultAssembler,
BasicResultAssembler<?> discriminatorAssembler) {
return new EntityDelayedFetchInitializer(
parentAccess,
referencedPath,
fetchable,
selectByUniqueKey,
resultAssembler
resultAssembler,
discriminatorAssembler
);
}

View File

@ -0,0 +1,598 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.orm.test.proxy.concrete;
import org.hibernate.Hibernate;
import org.hibernate.annotations.ConcreteProxy;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import jakarta.persistence.CascadeType;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.ManyToOne;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
/**
* @author Marco Belladelli
*/
public abstract class AbstractConcreteProxyTest extends BaseNonConfigCoreFunctionalTestCase {
@Test
public void testSingleTable() {
final SQLStatementInspector inspector = getStatementInspector();
inspector.clear();
// test find and association
inSession( session -> {
final SingleParent parent1 = session.find( SingleParent.class, 1L );
assertThat( parent1.getSingle(), instanceOf( SingleSubChild1.class ) );
assertThat( Hibernate.isInitialized( parent1.getSingle() ), is( false ) );
final SingleSubChild1 proxy = (SingleSubChild1) parent1.getSingle();
assertThat( Hibernate.isInitialized( proxy ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 1 );
inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 1 );
} );
inspector.clear();
// test query and association
inSession( session -> {
final SingleParent parent2 = session.createQuery(
"from SingleParent where id = 2",
SingleParent.class
).getSingleResult();
assertThat( parent2.getSingle(), instanceOf( SingleChild2.class ) );
assertThat( Hibernate.isInitialized( parent2.getSingle() ), is( false ) );
final SingleChild2 proxy = (SingleChild2) parent2.getSingle();
assertThat( Hibernate.isInitialized( proxy ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 1 );
inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 1 );
} );
inspector.clear();
// test get reference
inSession( session -> {
final SingleChild1 proxy1 = session.getReference( SingleChild1.class, 1L );
assertThat( proxy1, instanceOf( SingleSubChild1.class ) );
assertThat( Hibernate.isInitialized( proxy1 ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 2 );
inspector.clear();
final SingleBase proxy2 = session.byId( SingleBase.class ).getReference( 2L );
assertThat( proxy2, instanceOf( SingleChild2.class ) );
assertThat( Hibernate.isInitialized( proxy2 ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 1 );
} );
}
@Test
public void testJoined() {
final SQLStatementInspector inspector = getStatementInspector();
inspector.clear();
// test find and association
inSession( session -> {
final JoinedParent parent1 = session.find( JoinedParent.class, 1L );
assertThat( Hibernate.isInitialized( parent1.getJoined() ), is( false ) );
assertThat( parent1.getJoined(), instanceOf( JoinedSubChild1.class ) );
final JoinedSubChild1 proxy = (JoinedSubChild1) parent1.getJoined();
assertThat( Hibernate.isInitialized( proxy ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 4 );
} );
inspector.clear();
// test query and association
inSession( session -> {
final JoinedParent parent2 = session.createQuery(
"from JoinedParent where id = 2",
JoinedParent.class
).getSingleResult();
assertThat( Hibernate.isInitialized( parent2.getJoined() ), is( false ) );
assertThat( parent2.getJoined(), instanceOf( JoinedChild2.class ) );
final JoinedChild2 proxy = (JoinedChild2) parent2.getJoined();
assertThat( Hibernate.isInitialized( proxy ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 4 );
} );
inspector.clear();
// test get reference
inSession( session -> {
final JoinedChild1 proxy1 = session.getReference( JoinedChild1.class, 1L );
assertThat( proxy1, instanceOf( JoinedSubChild1.class ) );
assertThat( Hibernate.isInitialized( proxy1 ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 1 );
inspector.clear();
final JoinedBase proxy2 = session.byId( JoinedBase.class ).getReference( 2L );
assertThat( proxy2, instanceOf( JoinedChild2.class ) );
assertThat( Hibernate.isInitialized( proxy2 ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 3 );
} );
}
@Test
public void testJoinedDisc() {
final SQLStatementInspector inspector = getStatementInspector();
inspector.clear();
// test find and association
inSession( session -> {
final JoinedDiscParent parent1 = session.find( JoinedDiscParent.class, 1L );
assertThat( Hibernate.isInitialized( parent1.getJoinedDisc() ), is( false ) );
assertThat( parent1.getJoinedDisc(), instanceOf( JoinedDiscSubChild1.class ) );
final JoinedDiscSubChild1 proxy = (JoinedDiscSubChild1) parent1.getJoinedDisc();
assertThat( Hibernate.isInitialized( proxy ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 1 );
inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 1 );
} );
inspector.clear();
// test query and association
inSession( session -> {
final JoinedDiscParent parent2 = session.createQuery(
"from JoinedDiscParent where id = 2",
JoinedDiscParent.class
).getSingleResult();
assertThat( Hibernate.isInitialized( parent2.getJoinedDisc() ), is( false ) );
assertThat( parent2.getJoinedDisc(), instanceOf( JoinedDiscChild2.class ) );
final JoinedDiscChild2 proxy = (JoinedDiscChild2) parent2.getJoinedDisc();
assertThat( Hibernate.isInitialized( proxy ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 1 );
inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 1 );
} );
inspector.clear();
// test get reference
inSession( session -> {
final JoinedDiscChild1 proxy1 = session.getReference( JoinedDiscChild1.class, 1L );
assertThat( proxy1, instanceOf( JoinedDiscSubChild1.class ) );
assertThat( Hibernate.isInitialized( proxy1 ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 0 );
inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 1 );
inspector.clear();
final JoinedDiscBase proxy2 = session.byId( JoinedDiscBase.class ).getReference( 2L );
assertThat( proxy2, instanceOf( JoinedDiscChild2.class ) );
assertThat( Hibernate.isInitialized( proxy2 ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, 0 );
inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 1 );
} );
}
@Test
public void testUnion() {
final SQLStatementInspector inspector = getStatementInspector();
inspector.clear();
// test find and association
inSession( session -> {
final UnionParent parent1 = session.find( UnionParent.class, 1L );
assertThat( Hibernate.isInitialized( parent1.getUnion() ), is( false ) );
assertThat( parent1.getUnion(), instanceOf( UnionSubChild1.class ) );
final UnionSubChild1 proxy = (UnionSubChild1) parent1.getUnion();
assertThat( Hibernate.isInitialized( proxy ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 1 );
inspector.assertNumberOfOccurrenceInQuery( 0, "union", 3 );
} );
inspector.clear();
// test query and association
inSession( session -> {
final UnionParent parent2 = session.createQuery(
"from UnionParent where id = 2",
UnionParent.class
).getSingleResult();
assertThat( Hibernate.isInitialized( parent2.getUnion() ), is( false ) );
assertThat( parent2.getUnion(), instanceOf( UnionChild2.class ) );
final UnionChild2 proxy = (UnionChild2) parent2.getUnion();
assertThat( Hibernate.isInitialized( proxy ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 1 );
inspector.assertNumberOfOccurrenceInQuery( 0, "union", 3 );
} );
inspector.clear();
// test get reference
inSession( session -> {
final UnionChild1 proxy1 = session.getReference( UnionChild1.class, 1L );
assertThat( proxy1, instanceOf( UnionSubChild1.class ) );
assertThat( Hibernate.isInitialized( proxy1 ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfOccurrenceInQuery( 0, "union", 1 );
inspector.clear();
final UnionBase proxy2 = session.byId( UnionBase.class ).getReference( 2L );
assertThat( proxy2, instanceOf( UnionChild2.class ) );
assertThat( Hibernate.isInitialized( proxy2 ), is( false ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfOccurrenceInQuery( 0, "union", 3 );
} );
}
@Before
public void setUp() {
inTransaction( session -> {
session.persist( new SingleParent( 1L, new SingleSubChild1( 1L, "1", "1" ) ) );
session.persist( new JoinedParent( 1L, new JoinedSubChild1( 1L, "1", "1" ) ) );
session.persist( new JoinedDiscParent( 1L, new JoinedDiscSubChild1( 1L, "1", "1" ) ) );
session.persist( new UnionParent( 1L, new UnionSubChild1( 1L, "1", "1" ) ) );
session.persist( new SingleParent( 2L, new SingleChild2( 2L, 2 ) ) );
session.persist( new JoinedParent( 2L, new JoinedChild2( 2L, 2 ) ) );
session.persist( new JoinedDiscParent( 2L, new JoinedDiscChild2( 2L, 2 ) ) );
session.persist( new UnionParent( 2L, new UnionChild2( 2L, 2 ) ) );
} );
}
@After
public void tearDown() {
inTransaction( session -> {
session.createMutationQuery( "delete from SingleParent" ).executeUpdate();
session.createMutationQuery( "delete from SingleBase" ).executeUpdate();
session.createMutationQuery( "delete from JoinedParent" ).executeUpdate();
session.createMutationQuery( "delete from JoinedBase" ).executeUpdate();
session.createMutationQuery( "delete from JoinedDiscParent" ).executeUpdate();
session.createMutationQuery( "delete from JoinedDiscBase" ).executeUpdate();
session.createMutationQuery( "delete from UnionParent" ).executeUpdate();
session.createMutationQuery( "delete from UnionBase" ).executeUpdate();
} );
}
@Override
protected void applyMetadataSources(MetadataSources sources) {
sources.addAnnotatedClasses(
SingleParent.class,
SingleBase.class,
SingleChild1.class,
SingleSubChild1.class,
SingleChild2.class,
JoinedParent.class,
JoinedBase.class,
JoinedChild1.class,
JoinedSubChild1.class,
JoinedChild2.class,
JoinedDiscParent.class,
JoinedDiscBase.class,
JoinedDiscChild1.class,
JoinedDiscSubChild1.class,
JoinedDiscChild2.class,
UnionParent.class,
UnionBase.class,
UnionChild1.class,
UnionSubChild1.class,
UnionChild2.class
);
}
@Override
protected void configureSessionFactoryBuilder(SessionFactoryBuilder sfb) {
sfb.applyStatementInspector( new SQLStatementInspector() );
}
protected SQLStatementInspector getStatementInspector() {
return (SQLStatementInspector) sessionFactory().getSessionFactoryOptions().getStatementInspector();
}
@Entity( name = "SingleParent" )
public static class SingleParent {
@Id
private Long id;
@ManyToOne( fetch = FetchType.LAZY, cascade = CascadeType.PERSIST )
private SingleBase single;
public SingleParent() {
}
public SingleParent(Long id, SingleBase single) {
this.id = id;
this.single = single;
}
public SingleBase getSingle() {
return single;
}
}
// InheritanceType.SINGLE_TABLE
@Entity( name = "SingleBase" )
@Inheritance( strategy = InheritanceType.SINGLE_TABLE )
@DiscriminatorColumn( name = "disc_col" )
@ConcreteProxy
public static class SingleBase {
@Id
private Long id;
public SingleBase() {
}
public SingleBase(Long id) {
this.id = id;
}
}
@Entity( name = "SingleChild1" )
public static class SingleChild1 extends SingleBase {
private String child1Prop;
public SingleChild1() {
}
public SingleChild1(Long id, String child1Prop) {
super( id );
this.child1Prop = child1Prop;
}
}
@Entity( name = "SingleSubChild1" )
public static class SingleSubChild1 extends SingleChild1 {
private String subChild1Prop;
public SingleSubChild1() {
}
public SingleSubChild1(Long id, String child1Prop, String subChild1Prop) {
super( id, child1Prop );
this.subChild1Prop = subChild1Prop;
}
}
@Entity( name = "SingleChild2" )
public static class SingleChild2 extends SingleBase {
private Integer child2Prop;
public SingleChild2() {
}
public SingleChild2(Long id, Integer child2Prop) {
super( id );
this.child2Prop = child2Prop;
}
}
// InheritanceType.JOINED
@Entity( name = "JoinedParent" )
public static class JoinedParent {
@Id
private Long id;
@ManyToOne( fetch = FetchType.LAZY, cascade = CascadeType.PERSIST )
private JoinedBase joined;
public JoinedParent() {
}
public JoinedParent(Long id, JoinedBase joined) {
this.id = id;
this.joined = joined;
}
public JoinedBase getJoined() {
return joined;
}
}
@Entity( name = "JoinedBase" )
@Inheritance( strategy = InheritanceType.JOINED )
@ConcreteProxy
public static class JoinedBase {
@Id
private Long id;
public JoinedBase() {
}
public JoinedBase(Long id) {
this.id = id;
}
}
@Entity( name = "JoinedChild1" )
public static class JoinedChild1 extends JoinedBase {
private String child1Prop;
public JoinedChild1() {
}
public JoinedChild1(Long id, String child1Prop) {
super( id );
this.child1Prop = child1Prop;
}
}
@Entity( name = "JoinedSubChild1" )
public static class JoinedSubChild1 extends JoinedChild1 {
private String subChild1Prop;
public JoinedSubChild1() {
}
public JoinedSubChild1(Long id, String child1Prop, String subChild1Prop) {
super( id, child1Prop );
this.subChild1Prop = subChild1Prop;
}
}
@Entity( name = "JoinedChild2" )
public static class JoinedChild2 extends JoinedBase {
private Integer child2Prop;
public JoinedChild2() {
}
public JoinedChild2(Long id, Integer child2Prop) {
super( id );
this.child2Prop = child2Prop;
}
}
// InheritanceType.JOINED + @DiscriminatorColumn
@Entity( name = "JoinedDiscParent" )
public static class JoinedDiscParent {
@Id
private Long id;
@ManyToOne( fetch = FetchType.LAZY, cascade = CascadeType.PERSIST )
private JoinedDiscBase joinedDisc;
public JoinedDiscParent() {
}
public JoinedDiscParent(Long id, JoinedDiscBase joinedDisc) {
this.id = id;
this.joinedDisc = joinedDisc;
}
public JoinedDiscBase getJoinedDisc() {
return joinedDisc;
}
}
@Entity( name = "JoinedDiscBase" )
@Inheritance( strategy = InheritanceType.JOINED )
@DiscriminatorColumn( name = "disc_col" )
@ConcreteProxy
public static class JoinedDiscBase {
@Id
private Long id;
public JoinedDiscBase() {
}
public JoinedDiscBase(Long id) {
this.id = id;
}
}
@Entity( name = "JoinedDiscChild1" )
public static class JoinedDiscChild1 extends JoinedDiscBase {
private String child1Prop;
public JoinedDiscChild1() {
}
public JoinedDiscChild1(Long id, String child1Prop) {
super( id );
this.child1Prop = child1Prop;
}
}
@Entity( name = "JoinedDiscSubChild1" )
public static class JoinedDiscSubChild1 extends JoinedDiscChild1 {
private String subChild1Prop;
public JoinedDiscSubChild1() {
}
public JoinedDiscSubChild1(Long id, String child1Prop, String subChild1Prop) {
super( id, child1Prop );
this.subChild1Prop = subChild1Prop;
}
}
@Entity( name = "JoinedDiscChild2" )
public static class JoinedDiscChild2 extends JoinedDiscBase {
private Integer child2Prop;
public JoinedDiscChild2() {
}
public JoinedDiscChild2(Long id, Integer child2Prop) {
super( id );
this.child2Prop = child2Prop;
}
}
// InheritanceType.TABLE_PER_CLASS
@Entity( name = "UnionParent" )
public static class UnionParent {
@Id
private Long id;
@ManyToOne( fetch = FetchType.LAZY, cascade = CascadeType.PERSIST )
private UnionBase union;
public UnionParent() {
}
public UnionParent(Long id, UnionBase union) {
this.id = id;
this.union = union;
}
public UnionBase getUnion() {
return union;
}
}
@Entity( name = "UnionBase" )
@Inheritance( strategy = InheritanceType.TABLE_PER_CLASS )
@ConcreteProxy
public static class UnionBase {
@Id
private Long id;
public UnionBase() {
}
public UnionBase(Long id) {
this.id = id;
}
}
@Entity( name = "UnionChild1" )
public static class UnionChild1 extends UnionBase {
private String child1Prop;
public UnionChild1() {
}
public UnionChild1(Long id, String child1Prop) {
super( id );
this.child1Prop = child1Prop;
}
}
@Entity( name = "UnionSubChild1" )
public static class UnionSubChild1 extends UnionChild1 {
private String subChild1Prop;
public UnionSubChild1() {
}
public UnionSubChild1(Long id, String child1Prop, String subChild1Prop) {
super( id, child1Prop );
this.subChild1Prop = subChild1Prop;
}
}
@Entity( name = "UnionChild2" )
public static class UnionChild2 extends UnionBase {
private Integer child2Prop;
public UnionChild2() {
}
public UnionChild2(Long id, Integer child2Prop) {
super( id );
this.child2Prop = child2Prop;
}
}
}

View File

@ -0,0 +1,19 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.orm.test.proxy.concrete;
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
import org.junit.runner.RunWith;
/**
* Version of {@link AbstractConcreteProxyTest} using bytecode-enhanced {@linkplain org.hibernate.proxy.HibernateProxy proxies}.
*
* @author Marco Belladelli
*/
@RunWith( BytecodeEnhancerRunner.class )
public class BytecodeConcreteProxyTest extends AbstractConcreteProxyTest {
}

View File

@ -0,0 +1,200 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.orm.test.proxy.concrete;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.hibernate.Hibernate;
import org.hibernate.annotations.ConcreteProxy;
import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.annotation.Nullable;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.jpa.SpecHints.HINT_SPEC_FETCH_GRAPH;
/**
* @author Marco Belladelli
*/
@Jira( "https://hibernate.atlassian.net/browse/HHH-16960" )
@Jira( "https://hibernate.atlassian.net/browse/HHH-17818" )
@DomainModel( annotatedClasses = {
LazyOptionalOneToOneConcreteProxyTest.Person.class,
LazyOptionalOneToOneConcreteProxyTest.PersonContact.class,
LazyOptionalOneToOneConcreteProxyTest.BusinessContact.class,
} )
@SessionFactory( useCollectingStatementInspector = true )
public class LazyOptionalOneToOneConcreteProxyTest {
@Test
public void testFindChildrenContact(SessionFactoryScope scope) {
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
inspector.clear();
scope.inTransaction( session -> {
final Person person = session.find(
Person.class,
1L,
Map.of( HINT_SPEC_FETCH_GRAPH, session.getEntityGraph( "Person.children" ) )
);
assertThat( person.getPersonContact() ).isNull();
assertThat( person.getChildren() ).matches( Hibernate::isInitialized )
.hasSize( 1 )
.element( 0 )
.extracting( Person::getPersonContact )
.isInstanceOf( PersonContact.class )
.matches( contact -> !Hibernate.isInitialized( contact ) );
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 3 );
} );
}
@Test
public void testFindParentContact(SessionFactoryScope scope) {
final SQLStatementInspector inspector = scope.getCollectingStatementInspector();
inspector.clear();
scope.inTransaction( session -> {
final Person person = session.find(
Person.class,
3L,
Map.of( HINT_SPEC_FETCH_GRAPH, session.getEntityGraph( "Person.children" ) )
);
assertThat( person.getPersonContact() )
.isInstanceOf( BusinessContact.class )
.matches( contact -> !Hibernate.isInitialized( contact ) );
assertThat( person.getChildren() ).matches( Hibernate::isInitialized )
.hasSize( 2 )
.extracting( Person::getPersonContact )
.containsOnlyNulls();
inspector.assertExecutedCount( 1 );
inspector.assertNumberOfJoins( 0, SqlAstJoinType.LEFT, 3 );
} );
}
@BeforeAll
public void setUp(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final Person parent1 = new Person( 1L, "parent1" );
final Person child1 = new Person( 2L, "child1" );
child1.setParent( parent1 );
session.persist( parent1 );
session.persist( child1 );
session.persist( new PersonContact( 2L, child1 ) );
final Person parent2 = new Person( 3L, "parent2" );
final Person child2 = new Person( 4L, "child2" );
final Person child3 = new Person( 5L, "child3" );
child2.setParent( parent2 );
child3.setParent( parent2 );
session.persist( parent2 );
session.persist( child2 );
session.persist( child3 );
session.persist( new BusinessContact( 3L, parent2, "business2" ) );
} );
}
@AfterAll
public void tearDown(SessionFactoryScope scope) {
scope.inTransaction( session -> {
session.createMutationQuery( "delete from PersonContact" ).executeUpdate();
session.createQuery( "from Person", Person.class ).getResultList().forEach( p -> {
p.setParent( null );
session.remove( p );
} );
} );
}
@Entity( name = "Person" )
@NamedEntityGraph( name = "Person.children", attributeNodes = @NamedAttributeNode( "children" ) )
public static class Person {
@Id
private Long id;
private String name;
@ManyToOne
private Person parent;
@OneToOne( mappedBy = "person" )
private PersonContact personContact;
@OneToMany( mappedBy = "parent" )
private Set<Person> children = new HashSet<>();
public Person() {
}
public Person(Long id, String name) {
this.id = id;
this.name = name;
}
public void setParent(@Nullable Person parent) {
this.parent = parent;
if ( parent != null ) {
parent.getChildren().add( this );
}
}
public PersonContact getPersonContact() {
return personContact;
}
public Set<Person> getChildren() {
return children;
}
}
@Entity( name = "PersonContact" )
@ConcreteProxy
public static class PersonContact {
@Id
private Long id;
@OneToOne( optional = false, fetch = FetchType.LAZY )
@MapsId
private Person person;
public PersonContact() {
}
public PersonContact(Long id, Person person) {
this.id = id;
this.person = person;
}
}
@Entity( name = "BusinessContact" )
public static class BusinessContact extends PersonContact {
private String business;
public BusinessContact() {
}
public BusinessContact(Long id, Person person, String business) {
super( id, person );
this.business = business;
}
}
}

View File

@ -0,0 +1,15 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.orm.test.proxy.concrete;
/**
* Version of {@link AbstractConcreteProxyTest} using normal {@linkplain org.hibernate.proxy.HibernateProxy proxies}.
*
* @author Marco Belladelli
*/
public class ProxyConcreteProxyTest extends AbstractConcreteProxyTest {
}