HHH-16019 - @Where not consistently applied across association boundaries

HHH-16264 - Deprecate `hibernate.use_entity_where_clause_for_collections`
HHH-16265 - Remove `@Where#applyInToManyFetch`
This commit is contained in:
Steve Ebersole 2023-02-20 09:23:56 -06:00
parent 07be7731f4
commit 30f8e8d3b0
31 changed files with 1144 additions and 161 deletions

View File

@ -49,13 +49,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* </pre>
* <p>
* By default, {@code @Where} restrictions declared for an entity are
* applied when loading a collection of that entity type. This behavior is
* controlled by:
* <ol>
* <li>the annotation member {@link #applyInToManyFetch()}, and
* <li>the configuration property
* {@value org.hibernate.cfg.AvailableSettings#USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS}.
* </ol>
* applied when loading associations of that entity type. This behavior can
* be disabled using the setting {@value org.hibernate.cfg.AvailableSettings#USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS};
* note, however, that setting is disabled.
* <p>
* Note that {@code @Where} restrictions are always applied and cannot be
* disabled. Nor may they be parameterized. They're therefore <em>much</em>
@ -75,21 +71,4 @@ public @interface Where {
* A predicate, written in native SQL.
*/
String clause();
/**
* If this restriction applies to an entity type, should it also be
* applied when fetching a {@link jakarta.persistence.OneToMany} or
* {@link jakarta.persistence.ManyToOne} association that targets
* the entity type?
* <p>
* By default, the restriction is not applied unless the property
* {@value org.hibernate.cfg.AvailableSettings#USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS}
* is explicitly disabled.
*
* @return {@code true} if the restriction should be applied even
* if the configuration property is not enabled
*
* @since 6.2
*/
boolean applyInToManyFetch() default false;
}

View File

@ -77,13 +77,13 @@ import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.BootLogging;
import org.hibernate.boot.model.IdentifierGeneratorDefinition;
import org.hibernate.boot.model.TypeDefinition;
import org.hibernate.boot.model.source.internal.hbm.ModelBinder;
import org.hibernate.boot.spi.AccessType;
import org.hibernate.boot.spi.InFlightMetadataCollector;
import org.hibernate.boot.spi.InFlightMetadataCollector.CollectionTypeRegistrationDescriptor;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.boot.spi.PropertyData;
import org.hibernate.boot.spi.SecondPass;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.engine.spi.FilterDefinition;
import org.hibernate.internal.CoreMessageLogger;
@ -165,7 +165,6 @@ import static org.hibernate.boot.model.internal.BinderHelper.toAliasTableMap;
import static org.hibernate.boot.model.internal.EmbeddableBinder.fillEmbeddable;
import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators;
import static org.hibernate.boot.model.internal.PropertyHolderBuilder.buildPropertyHolder;
import static org.hibernate.cfg.AvailableSettings.USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS;
import static org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle.fromResultCheckStyle;
import static org.hibernate.internal.util.StringHelper.getNonEmptyOrConjunctionIfBothNonEmpty;
import static org.hibernate.internal.util.StringHelper.isEmpty;
@ -1789,8 +1788,7 @@ public abstract class CollectionBinder {
private String getWhereOnClassClause() {
if ( property.getElementClass() != null ) {
final Where whereOnClass = getOverridableAnnotation( property.getElementClass(), Where.class, getBuildingContext() );
return whereOnClass != null
&& ( whereOnClass.applyInToManyFetch() || useEntityWhereClauseForCollections() )
return whereOnClass != null && ModelBinder.useEntityWhereClauseForCollections( buildingContext )
? whereOnClass.clause()
: null;
}
@ -1799,18 +1797,6 @@ public abstract class CollectionBinder {
}
}
private boolean useEntityWhereClauseForCollections() {
return getBoolean(
USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS,
buildingContext
.getBuildingOptions()
.getServiceRegistry()
.getService( ConfigurationService.class )
.getSettings(),
true
);
}
private void addFilter(boolean hasAssociationTable, FilterJoinTable filter) {
if ( hasAssociationTable ) {
collection.addFilter(

View File

@ -17,6 +17,7 @@ import java.util.Properties;
import org.hibernate.AssertionFailure;
import org.hibernate.FetchMode;
import org.hibernate.Remove;
import org.hibernate.annotations.SourceType;
import org.hibernate.boot.MappingException;
import org.hibernate.boot.jaxb.Origin;
@ -160,6 +161,7 @@ import org.hibernate.usertype.CompositeUserType;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;
import static org.hibernate.cfg.AvailableSettings.USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS;
import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty;
import static org.hibernate.mapping.SimpleValue.DEFAULT_ID_GEN_STRATEGY;
@ -196,6 +198,28 @@ public class ModelBinder {
this.relationalObjectBinder = new RelationalObjectBinder( context );
}
/**
* @deprecated Interprets the setting {@value AvailableSettings#USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS},
* which itself is deprecated
*/
@SuppressWarnings("removal")
@Remove
@Deprecated( since = "6.2" )
public static boolean useEntityWhereClauseForCollections(MetadataBuildingContext buildingContext) {
final Object explicitSetting = buildingContext
.getBuildingOptions()
.getServiceRegistry()
.getService( ConfigurationService.class )
.getSettings()
.get( USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS );
if ( explicitSetting != null ) {
DeprecationLogger.DEPRECATION_LOGGER.deprecatedSettingNoReplacement( USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS );
return ConfigurationHelper.toBoolean( explicitSetting, true );
}
return true;
}
public void bindEntityHierarchy(EntityHierarchySourceImpl hierarchySource) {
final RootClass rootEntityDescriptor = new RootClass( hierarchySource.getRootEntityMappingDocument() );
bindRootEntity( hierarchySource, rootEntityDescriptor );
@ -3420,7 +3444,7 @@ public class ModelBinder {
final PersistentClass referencedEntityBinding = getReferencedEntityBinding( elementSource.getReferencedEntityName() );
if ( useEntityWhereClauseForCollections() ) {
if ( useEntityWhereClauseForCollections( metadataBuildingContext ) ) {
// For a one-to-many association, there are 2 possible sources of "where" clauses that apply
// to the associated entity table:
// 1) from the associated entity mapping; i.e., <class name="..." ... where="..." .../>
@ -3490,7 +3514,7 @@ public class ModelBinder {
// This "where" clause comes from the collection mapping; e.g., <set name="..." ... where="..." .../>
getCollectionBinding().setWhere( getPluralAttributeSource().getWhere() );
if ( useEntityWhereClauseForCollections() ) {
if ( useEntityWhereClauseForCollections( metadataBuildingContext ) ) {
// For a many-to-many association, there are 2 possible sources of "where" clauses that apply
// to the associated entity table (not the join table):
// 1) from the associated entity mapping; i.e., <class name="..." ... where="..." .../>
@ -3608,18 +3632,6 @@ public class ModelBinder {
}
}
private boolean useEntityWhereClauseForCollections() {
return ConfigurationHelper.getBoolean(
AvailableSettings.USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS,
metadataBuildingContext
.getBuildingOptions()
.getServiceRegistry()
.getService( ConfigurationService.class )
.getSettings(),
true
);
}
private class PluralAttributeListSecondPass extends AbstractPluralAttributeSecondPass {
public PluralAttributeListSecondPass(
MappingDocument sourceDocument,

View File

@ -12,6 +12,7 @@ import java.util.function.Supplier;
import org.hibernate.CustomEntityDirtinessStrategy;
import org.hibernate.Incubating;
import org.hibernate.Interceptor;
import org.hibernate.Remove;
import org.hibernate.SessionFactoryObserver;
import org.hibernate.boot.registry.selector.spi.StrategySelector;
import org.hibernate.cache.internal.NoCachingRegionFactory;
@ -2059,25 +2060,19 @@ public interface AvailableSettings {
* The {@link org.hibernate.annotations.Where} annotation specifies a restriction
* on the table rows which are visible as entity class instances or collection
* elements.
* <p>
* This setting controls whether the restriction is applied when loading a
* {@link jakarta.persistence.OneToMany one-to-many} or
* {@link jakarta.persistence.ManyToMany many-to-many} association whose target
* type defines the restriction.
* <p>
* By default, the restriction is applied. When this setting is disabled, the
* restriction is not applied.
* <p>
* The setting has no effect on a collection of {@link jakarta.persistence.Embeddable
* embeddable} values containing a {@link jakarta.persistence.ManyToOne many-to-one}
* association to the entity.
* <p>
* This behavior may now be controlled in a safer and more granular way using
* {@link org.hibernate.annotations.Where#applyInToManyFetch}, and so the use
* of this configuration property is no longer recommended.
* <p/>
* This setting controls whether the restriction applied to an entity should
* be applied to association fetches (one-to-one, many-to-one, one-to-many and many-to-many)
* targeting the entity.
*
* @see org.hibernate.annotations.Where#applyInToManyFetch
* @apiNote The setting is very misnamed - it applies across all entity associations, not just collections.
*
* @implSpec Enabled ({@code true}) by default, meaning the restriction is applied. When this setting
* is disabled ({@code false}), the restriction is not applied.
*
* @deprecated Originally added as a backwards compatibility flag
*/
@Remove @Deprecated( forRemoval = true, since = "6.2" )
String USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS = "hibernate.use_entity_where_clause_for_collections";
/**

View File

@ -266,4 +266,15 @@ public interface DeprecationLogger extends BasicLogger {
)
void deprecatedSettingForRemoval(String settingName, String defaultValue);
/**
* Different from {@link #deprecatedSetting} in that sometimes there is no
* direct alternative
*/
@LogMessage(level = WARN)
@Message(
id = 90000030,
value = "The [%s] configuration is deprecated and will be removed."
)
void deprecatedSettingNoReplacement(String settingName);
}

View File

@ -19,6 +19,7 @@ import org.hibernate.HibernateException;
import org.hibernate.LockOptions;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.loader.ast.spi.NaturalIdLoadOptions;
@ -155,6 +156,7 @@ public abstract class AbstractNaturalIdLoader<T> implements NaturalIdLoader<T> {
lockOptions,
fetchProcessor,
true,
LoadQueryInfluencers.NONE,
sessionFactory
);

View File

@ -12,6 +12,7 @@ import java.util.List;
import org.hibernate.LockOptions;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.util.collections.ArrayHelper;
@ -75,6 +76,7 @@ class DatabaseSnapshotExecutor {
LockOptions.NONE,
(fetchParent, creationState) -> ImmutableFetchList.EMPTY,
true,
LoadQueryInfluencers.NONE,
sessionFactory
);

View File

@ -380,6 +380,7 @@ public class LoaderSelectBuilder {
lockOptions,
this::visitFetches,
forceIdentifierSelection,
loadQueryInfluencers,
creationContext
);
@ -962,6 +963,7 @@ public class LoaderSelectBuilder {
lockOptions,
this::visitFetches,
numberOfKeysToLoad > 1,
loadQueryInfluencers,
creationContext
);

View File

@ -15,6 +15,7 @@ import jakarta.persistence.CacheStoreMode;
import org.hibernate.FlushMode;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.graph.spi.AppliedGraph;
import org.hibernate.metamodel.mapping.AssociationKey;
import org.hibernate.metamodel.mapping.ForeignKeyDescriptor;
@ -51,6 +52,7 @@ public class LoaderSqlAstCreationState
private final SqlAliasBaseManager sqlAliasBaseManager;
private final boolean forceIdentifierSelection;
private final LoadQueryInfluencers loadQueryInfluencers;
private final SqlAstCreationContext sf;
private final SqlAstQueryPartProcessingStateImpl processingState;
private final FromClauseAccess fromClauseAccess;
@ -68,12 +70,14 @@ public class LoaderSqlAstCreationState
LockOptions lockOptions,
FetchProcessor fetchProcessor,
boolean forceIdentifierSelection,
LoadQueryInfluencers loadQueryInfluencers,
SqlAstCreationContext sf) {
this.sqlAliasBaseManager = sqlAliasBaseManager;
this.fromClauseAccess = fromClauseAccess;
this.lockOptions = lockOptions;
this.fetchProcessor = fetchProcessor;
this.forceIdentifierSelection = forceIdentifierSelection;
this.loadQueryInfluencers = loadQueryInfluencers;
this.sf = sf;
this.processingState = new SqlAstQueryPartProcessingStateImpl(
queryPart,
@ -114,6 +118,11 @@ public class LoaderSqlAstCreationState
return sqlAliasBaseManager;
}
@Override
public LoadQueryInfluencers getLoadQueryInfluencers() {
return loadQueryInfluencers;
}
@Override
public void registerLockMode(String identificationVariable, LockMode explicitLockMode) {
throw new UnsupportedOperationException( "Registering lock modes should only be done for result set mappings" );

View File

@ -61,6 +61,7 @@ import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
import org.hibernate.metamodel.mapping.ForeignKeyDescriptor;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.ManagedMappingType;
import org.hibernate.metamodel.mapping.MappingModelCreationLogger;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.mapping.ModelPartContainer;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
@ -1596,6 +1597,15 @@ public class MappingModelCreationHelper {
|| value instanceof OneToOne && value.isNullable()
|| value instanceof ManyToOne && value.isNullable() && ( (ManyToOne) value ).isIgnoreNotFound() ) {
fetchTiming = FetchTiming.IMMEDIATE;
if ( lazy ) {
if ( MappingModelCreationLogger.DEBUG_ENABLED ) {
MappingModelCreationLogger.LOGGER.debugf(
"Forcing FetchTiming.IMMEDIATE for to-one association : %s.%s",
declaringType.getNavigableRole(),
bootProperty.getName()
);
}
}
}
else {
fetchTiming = FetchOptionsHelper.determineFetchTiming( fetchStyle, type, lazy, role, sessionFactory );

View File

@ -6,7 +6,6 @@
*/
package org.hibernate.metamodel.mapping.internal;
import java.util.ArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
@ -48,7 +47,6 @@ import org.hibernate.sql.ast.spi.FromClauseAccess;
import org.hibernate.sql.ast.spi.SqlAliasBase;
import org.hibernate.sql.ast.spi.SqlAliasStemHelper;
import org.hibernate.sql.ast.spi.SqlAstCreationState;
import org.hibernate.sql.ast.spi.SqlExpressionResolver;
import org.hibernate.sql.ast.spi.SqlSelection;
import org.hibernate.sql.ast.tree.from.CollectionTableGroup;
import org.hibernate.sql.ast.tree.from.NamedTableReference;
@ -58,6 +56,7 @@ import org.hibernate.sql.ast.tree.from.TableGroupJoin;
import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.sql.ast.tree.predicate.PredicateCollector;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.Fetch;
@ -570,6 +569,45 @@ public class PluralAttributeMappingImpl
boolean fetched,
boolean addsPredicate,
SqlAstCreationState creationState) {
final PredicateCollector predicateCollector = new PredicateCollector();
final TableGroup tableGroup = createRootTableGroupJoin(
navigablePath,
lhs,
explicitSourceAlias,
explicitSqlAliasBase,
requestedJoinType,
fetched,
predicateCollector::applyPredicate,
creationState
);
getCollectionDescriptor().applyBaseRestrictions(
predicateCollector::applyPredicate,
tableGroup,
true,
creationState.getLoadQueryInfluencers().getEnabledFilters(),
null,
creationState
);
getCollectionDescriptor().applyBaseManyToManyRestrictions(
predicateCollector::applyPredicate,
tableGroup,
true,
creationState.getLoadQueryInfluencers().getEnabledFilters(),
null,
creationState
);
return new TableGroupJoin(
navigablePath,
determineSqlJoinType( lhs, requestedJoinType, fetched ),
tableGroup,
predicateCollector.getPredicate()
);
}
private SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) {
final SqlAstJoinType joinType;
if ( requestedJoinType == null ) {
if ( fetched ) {
@ -582,25 +620,7 @@ public class PluralAttributeMappingImpl
else {
joinType = requestedJoinType;
}
final java.util.List<Predicate> predicates = new ArrayList<>( 2 );
final TableGroup tableGroup = createRootTableGroupJoin(
navigablePath,
lhs,
explicitSourceAlias,
explicitSqlAliasBase,
requestedJoinType,
fetched,
predicates::add,
creationState
);
final TableGroupJoin tableGroupJoin = new TableGroupJoin(
navigablePath,
joinType,
tableGroup,
null
);
predicates.forEach( tableGroupJoin::applyPredicate );
return tableGroupJoin;
return joinType;
}
@Override
@ -644,6 +664,7 @@ public class PluralAttributeMappingImpl
if ( predicateConsumer != null ) {
predicateConsumer.accept( getKeyDescriptor().generateJoinPredicate( lhs, tableGroup, creationState ) );
}
return tableGroup;
}

View File

@ -1672,20 +1672,8 @@ public class ToOneAttributeMapping
// This is vital for the map key property check that comes next
assert !( lhs instanceof PluralTableGroup );
final SqlAstJoinType joinType;
if ( requestedJoinType == null ) {
if ( fetched ) {
joinType = getDefaultSqlAstJoinType( lhs );
}
else {
joinType = SqlAstJoinType.INNER;
}
}
else {
joinType = requestedJoinType;
}
final FromClauseAccess fromClauseAccess = creationState.getFromClauseAccess();
final SqlAstJoinType joinType = determineSqlJoinType( lhs, requestedJoinType, fetched );
// If a parent is a collection part, there is no custom predicate and the join is INNER or LEFT
// we check if this attribute is the map key property to reuse the existing index table group
@ -1805,20 +1793,35 @@ public class ToOneAttributeMapping
creationState
) );
if ( hasNotFoundAction() ) {
getAssociatedEntityMappingType().applyWhereRestrictions(
join::applyPredicate,
tableGroup,
true,
null
);
}
// Note specifically we DO NOT apply `@Filter` restrictions
getAssociatedEntityMappingType().applyWhereRestrictions(
join::applyPredicate,
tableGroup,
true,
creationState
);
}
);
return join;
}
private SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) {
final SqlAstJoinType joinType;
if ( requestedJoinType == null ) {
if ( fetched ) {
joinType = getDefaultSqlAstJoinType( lhs );
}
else {
joinType = SqlAstJoinType.INNER;
}
}
else {
joinType = requestedJoinType;
}
return joinType;
}
@Override
public LazyTableGroup createRootTableGroupJoin(
NavigablePath navigablePath,

View File

@ -928,6 +928,7 @@ public abstract class AbstractCollectionPersister
LockOptions.NONE,
(fetchParent, creationState) -> ImmutableFetchList.EMPTY,
true,
LoadQueryInfluencers.NONE,
getFactory()
);

View File

@ -287,15 +287,6 @@ public class OneToManyPersister extends AbstractCollectionPersister {
return ( (Joinable) getElementPersister() ).getTableName();
}
@Override
public void applyWhereRestrictions(
Consumer<Predicate> predicateConsumer,
TableGroup tableGroup,
boolean useQualifier,
SqlAstCreationState creationState) {
super.applyWhereRestrictions( predicateConsumer, tableGroup, useQualifier, creationState );
}
protected void applyWhereFragments(
Consumer<Predicate> predicateConsumer,
String alias,

View File

@ -1680,6 +1680,7 @@ public abstract class AbstractEntityPersister
LockOptions.NONE,
this::fetchProcessor,
true,
LoadQueryInfluencers.NONE,
getFactory()
);

View File

@ -15,6 +15,7 @@ import java.util.function.Function;
import org.hibernate.Internal;
import org.hibernate.LockMode;
import org.hibernate.engine.FetchTiming;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.util.collections.Stack;
import org.hibernate.internal.util.collections.StandardStack;
@ -72,6 +73,7 @@ public class DomainResultCreationStateImpl
private final JdbcValuesMetadata jdbcResultsMetadata;
private final Consumer<SqlSelection> sqlSelectionConsumer;
private final LoadQueryInfluencers loadQueryInfluencers;
private final Map<ColumnReferenceKey, ResultSetMappingSqlSelection> sqlSelectionMap = new HashMap<>();
private boolean allowPositionalSelections = true;
@ -92,10 +94,12 @@ public class DomainResultCreationStateImpl
JdbcValuesMetadata jdbcResultsMetadata,
Map<String, Map<String, DynamicFetchBuilderLegacy>> legacyFetchBuilders,
Consumer<SqlSelection> sqlSelectionConsumer,
LoadQueryInfluencers loadQueryInfluencers,
SessionFactoryImplementor sessionFactory) {
this.stateIdentifier = stateIdentifier;
this.jdbcResultsMetadata = jdbcResultsMetadata;
this.sqlSelectionConsumer = sqlSelectionConsumer;
this.loadQueryInfluencers = loadQueryInfluencers;
this.fromClauseAccess = new FromClauseAccessImpl();
this.sqlAliasBaseManager = new SqlAliasBaseManager();
@ -245,6 +249,11 @@ public class DomainResultCreationStateImpl
return sqlAliasBaseManager;
}
@Override
public LoadQueryInfluencers getLoadQueryInfluencers() {
return loadQueryInfluencers;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// SqlAstProcessingState

View File

@ -19,6 +19,7 @@ import java.util.function.Consumer;
import org.hibernate.Incubating;
import org.hibernate.Internal;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.loader.NonUniqueDiscoveredSqlAliasException;
@ -185,6 +186,7 @@ public class ResultSetMappingImpl implements ResultSetMapping {
@Override
public JdbcValuesMapping resolve(
JdbcValuesMetadata jdbcResultsMetadata,
LoadQueryInfluencers loadQueryInfluencers,
SessionFactoryImplementor sessionFactory) {
final int numberOfResults;
@ -205,6 +207,7 @@ public class ResultSetMappingImpl implements ResultSetMapping {
jdbcResultsMetadata,
legacyFetchBuilders,
sqlSelections::add,
loadQueryInfluencers,
sessionFactory
);

View File

@ -77,6 +77,7 @@ import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.mapping.ModelPartContainer;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.metamodel.mapping.Restrictable;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.metamodel.mapping.SelectableMappings;
import org.hibernate.metamodel.mapping.SqlExpressible;
@ -394,7 +395,6 @@ import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
import org.hibernate.type.descriptor.java.EnumJavaType;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.JavaTypeHelper;
import org.hibernate.type.descriptor.java.TemporalJavaType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators;
import org.hibernate.type.spi.TypeConfiguration;
@ -3052,15 +3052,15 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
);
joinedTableGroup = joinedTableGroupJoin.getJoinedGroup();
pluralAttributeMapping.applyBaseRestrictions(
(predicate) -> addCollectionFilterPredicate( joinedTableGroup.getNavigablePath(), predicate ),
joinedTableGroup,
true,
getLoadQueryInfluencers().getEnabledFilters(),
null,
this
);
//
// pluralAttributeMapping.applyBaseRestrictions(
// (predicate) -> addCollectionFilterPredicate( joinedTableGroup.getNavigablePath(), predicate ),
// joinedTableGroup,
// true,
// getLoadQueryInfluencers().getEnabledFilters(),
// null,
// this
// );
}
else {
assert modelPart instanceof TableGroupJoinProducer;
@ -7145,13 +7145,14 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
FetchTiming fetchTiming = fetchable.getMappedFetchOptions().getTiming();
boolean joined = false;
EntityGraphTraversalState.TraversalResult traversalResult = null;
final FromClauseIndex fromClauseIndex = getFromClauseIndex();
final SqmAttributeJoin<?, ?> fetchedJoin = fromClauseIndex.findFetchedJoinByPath( resolvedNavigablePath );
boolean explicitFetch = false;
final NavigablePath fetchablePath;
final Integer maxDepth = getCreationContext().getMaximumFetchDepth();
final FromClauseIndex fromClauseIndex = getFromClauseIndex();
final SqmAttributeJoin<?, ?> fetchedJoin = fromClauseIndex.findFetchedJoinByPath( resolvedNavigablePath );
boolean explicitFetch = false;
EntityGraphTraversalState.TraversalResult traversalResult = null;
if ( fetchedJoin != null ) {
fetchablePath = fetchedJoin.getNavigablePath();
// there was an explicit fetch in the SQM
@ -7224,17 +7225,6 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
}
// final TableGroup existingJoinedGroup = fromClauseIndex.findTableGroup( fetchablePath );
// if ( existingJoinedGroup != null ) {
// we can use this to trigger the fetch from the joined group.
// todo (6.0) : do we want to do this though?
// On the positive side it would allow EntityGraph to use the existing TableGroup. But that ties in
// to the discussion above regarding how to handle eager and EntityGraph (JOIN versus SELECT).
// Can be problematic if the existing one is restricted
// fetchTiming = FetchTiming.IMMEDIATE;
// }
// lastly, account for any app-defined max-fetch-depth
if ( maxDepth != null ) {
if ( fetchDepth >= maxDepth ) {
@ -7259,6 +7249,29 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
BaseSqmToSqlAstConverter.this
);
lhs.addTableGroupJoin( tableGroupJoin );
// if ( fetchable instanceof PluralAttributeMapping ) {
// // apply restrictions
// ( (Restrictable) fetchable ).applyBaseRestrictions(
// tableGroupJoin::applyPredicate,
// tableGroupJoin.getJoinedGroup(),
// true,
// loadQueryInfluencers.getEnabledFilters(),
// null,
// getSqlAstCreationState()
// );
//
// ( (PluralAttributeMapping) fetchable ).applyBaseManyToManyRestrictions(
// tableGroupJoin::applyPredicate,
// tableGroupJoin.getJoinedGroup(),
// true,
// loadQueryInfluencers.getEnabledFilters(),
// null,
// getSqlAstCreationState()
// );
// }
// and return the joined group
return tableGroupJoin.getJoinedGroup();
}
);

View File

@ -10,6 +10,7 @@ import java.util.List;
import java.util.function.Supplier;
import org.hibernate.LockMode;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.internal.util.collections.Stack;
import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.query.sqm.spi.BaseSemanticQueryWalker;
@ -62,6 +63,11 @@ public class FakeSqmToSqlAstConverter extends BaseSemanticQueryWalker implements
return creationState.getSqlAliasBaseGenerator();
}
@Override
public LoadQueryInfluencers getLoadQueryInfluencers() {
return LoadQueryInfluencers.NONE;
}
@Override
public void registerLockMode(String identificationVariable, LockMode explicitLockMode) {
creationState.registerLockMode( identificationVariable, explicitLockMode );

View File

@ -177,7 +177,7 @@ public class OutputsImpl implements Outputs {
null,
null,
this.context.getQueryOptions(),
resultSetMapping.resolve( resultSetAccess, getSessionFactory() ),
resultSetMapping.resolve( resultSetAccess, context.getSession().getLoadQueryInfluencers(), getSessionFactory() ),
null,
executionContext
);

View File

@ -7,6 +7,7 @@
package org.hibernate.sql.ast.spi;
import org.hibernate.LockMode;
import org.hibernate.engine.spi.LoadQueryInfluencers;
/**
* Access to stuff used while creating a SQL AST
@ -24,5 +25,7 @@ public interface SqlAstCreationState {
SqlAliasBaseGenerator getSqlAliasBaseGenerator();
LoadQueryInfluencers getLoadQueryInfluencers();
void registerLockMode(String identificationVariable, LockMode explicitLockMode);
}

View File

@ -475,13 +475,13 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
final JdbcValuesMetadata metadataForCache;
final JdbcValuesMapping jdbcValuesMapping;
if ( queryResultsCacheKey == null ) {
jdbcValuesMapping = mappingProducer.resolve( resultSetAccess, factory );
jdbcValuesMapping = mappingProducer.resolve( resultSetAccess, session.getLoadQueryInfluencers(), factory );
metadataForCache = null;
}
else {
// If we need to put the values into the cache, we need to be able to capture the JdbcValuesMetadata
final CapturingJdbcValuesMetadata capturingMetadata = new CapturingJdbcValuesMetadata( resultSetAccess );
jdbcValuesMapping = mappingProducer.resolve( capturingMetadata, factory );
jdbcValuesMapping = mappingProducer.resolve( capturingMetadata, session.getLoadQueryInfluencers(), factory );
metadataForCache = capturingMetadata.resolveMetadataForCache();
}
@ -498,10 +498,10 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
else {
final JdbcValuesMapping jdbcValuesMapping;
if ( cachedResults.isEmpty() || !( cachedResults.get( 0 ) instanceof JdbcValuesMetadata ) ) {
jdbcValuesMapping = mappingProducer.resolve( resultSetAccess, factory );
jdbcValuesMapping = mappingProducer.resolve( resultSetAccess, session.getLoadQueryInfluencers(), factory );
}
else {
jdbcValuesMapping = mappingProducer.resolve( (JdbcValuesMetadata) cachedResults.get( 0 ), factory );
jdbcValuesMapping = mappingProducer.resolve( (JdbcValuesMetadata) cachedResults.get( 0 ), session.getLoadQueryInfluencers(), factory );
}
return new JdbcValuesCacheHit( cachedResults, jdbcValuesMapping );
}

View File

@ -10,6 +10,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.query.NativeQuery;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.sql.ast.spi.SqlSelection;
@ -43,6 +44,7 @@ public class JdbcValuesMappingProducerStandard implements JdbcValuesMappingProdu
@Override
public JdbcValuesMapping resolve(
JdbcValuesMetadata jdbcResultsMetadata,
LoadQueryInfluencers loadQueryInfluencers,
SessionFactoryImplementor sessionFactory) {
final List<SqlSelection> sqlSelections = resolvedMapping.getSqlSelections();
List<SqlSelection> resolvedSelections = null;

View File

@ -9,6 +9,7 @@ package org.hibernate.sql.results.jdbc.spi;
import java.util.Set;
import org.hibernate.Incubating;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.query.results.ResultSetMapping;
@ -32,6 +33,7 @@ public interface JdbcValuesMappingProducer {
*/
JdbcValuesMapping resolve(
JdbcValuesMetadata jdbcResultsMetadata,
LoadQueryInfluencers loadQueryInfluencers,
SessionFactoryImplementor sessionFactory);
void addAffectedTableNames(Set<String> affectedTableNames, SessionFactoryImplementor sessionFactory);

View File

@ -64,7 +64,7 @@ public class LoadPlanBuilderTest {
final List<DomainResult<?>> domainResults = loadPlan.getJdbcSelect()
.getJdbcValuesMappingProducer()
.resolve( null, sessionFactory )
.resolve( null, LoadQueryInfluencers.NONE, sessionFactory )
.getDomainResults();
assertThat( domainResults ).hasSize( 1 );
@ -105,7 +105,7 @@ public class LoadPlanBuilderTest {
);
final List<DomainResult<?>> domainResults = loadPlan.getJdbcSelect()
.getJdbcValuesMappingProducer()
.resolve( null, sessionFactory )
.resolve( null, LoadQueryInfluencers.NONE, sessionFactory )
.getDomainResults();
assertThat( domainResults ).hasSize( 1 );

View File

@ -0,0 +1,113 @@
/*
* 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.mapping.where;
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import static jakarta.persistence.FetchType.LAZY;
import static java.text.MessageFormat.format;
import static java.util.Objects.hash;
import static java.util.stream.Collectors.joining;
@NamedEntityGraph(
name = "user-entity-graph",
attributeNodes = {
@NamedAttributeNode(value = "detail"),
@NamedAttributeNode(value = "skills")
}
)
@Table(name = "t_users")
@Entity(name = "User")
public class User {
@Id
@Column(name = "user_id")
private Integer id;
@Column(name = "user_name")
private String name;
@OneToOne(mappedBy = "user", fetch = LAZY)
private UserDetail detail;
@OneToMany(mappedBy = "user", fetch = LAZY)
private Set<UserSkill> skills = new HashSet<>();
protected User() {
}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
public User(String name, UserDetail detail) {
this.name = name;
this.detail = detail;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public UserDetail getDetail() {
return detail;
}
public void setDetail(UserDetail detail) {
this.detail = detail;
}
public Set<UserSkill> getSkills() {
return skills;
}
public void setSkills(Set<UserSkill> skills) {
this.skills = skills;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
User that = (User) obj;
return id.equals(that.id);
}
@Override
public int hashCode() {
return hash(id);
}
// @Override
// public String toString() {
// return format("User(id={0}, name={1}, detail={2}, skills=[{3}])",
// id, name, detail, skills.stream().map(UserSkill::toString).collect(joining(", ")));
// }
}

View File

@ -0,0 +1,100 @@
/*
* 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.mapping.where;
import org.hibernate.annotations.Where;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import static jakarta.persistence.FetchType.LAZY;
import static java.text.MessageFormat.format;
import static java.util.Objects.hash;
@Where(clause = "is_active = true")
@Table(name = "t_user_details")
@Entity(name = "UserDetail")
public class UserDetail {
@Id
@Column(name = "detail_id")
private Integer id;
@Column(name = "city")
private String city;
@Column(name = "is_active")
private Boolean active;
@OneToOne(fetch = LAZY)
@JoinColumn(name = "user_fk")
private User user;
protected UserDetail() {
}
public UserDetail(Integer id, String city, Boolean active, User user) {
this.id = id;
this.city = city;
this.active = active;
this.user = user;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
UserDetail that = (UserDetail) obj;
return id.equals(that.id);
}
@Override
public int hashCode() {
return hash(id);
}
@Override
public String toString() {
return format("UserDetail(id={0}, city={1}, active={2})", id, city, active);
}
}

View File

@ -0,0 +1,100 @@
/*
* 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.mapping.where;
import org.hibernate.annotations.Where;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import static jakarta.persistence.FetchType.LAZY;
import static java.text.MessageFormat.format;
import static java.util.Objects.hash;
@Where(clause = "has_deleted = false")
@Table(name = "t_user_skills")
@Entity(name = "UserSkill")
public class UserSkill {
@Id
@Column(name = "skill_id")
private Integer id;
@Column(name = "skill_name")
private String skillName;
@Column(name = "has_deleted")
private Boolean deleted;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "user_fk")
private User user;
protected UserSkill() {
}
public UserSkill(Integer id, String skillName, Boolean deleted, User user) {
this.id = id;
this.skillName = skillName;
this.deleted = deleted;
this.user = user;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getSkillName() {
return skillName;
}
public void setSkillName(String skillName) {
this.skillName = skillName;
}
public Boolean getDeleted() {
return deleted;
}
public void setDeleted(Boolean deleted) {
this.deleted = deleted;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
UserSkill that = (UserSkill) obj;
return id.equals(that.id);
}
@Override
public int hashCode() {
return hash(id);
}
@Override
public String toString() {
return format("UserSkill(id={0}, skillName={1}, deleted={2})", id, skillName, deleted);
}
}

View File

@ -0,0 +1,582 @@
/*
* 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.mapping.where;
import java.util.Map;
import org.hibernate.Hibernate;
import org.hibernate.annotations.Where;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.graph.spi.RootGraphImplementor;
import org.hibernate.jpa.SpecHints;
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.RequiresDialect;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.internal.util.collections.CollectionHelper.toSettingsMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for {@link Where} handling.
*
* @implNote Requires H2 simply because we need hard-coded schema export. The schema is simple and would
* probably work on a larger number of databases; but there should really be nothing database specific in
* these tests.
*/
@ServiceRegistry(
settings = {
@Setting(
name = AvailableSettings.JAKARTA_HBM2DDL_CREATE_SCRIPT_SOURCE,
value = "org/hibernate/orm/test/mapping/schema.sql"
),
@Setting(
name = AvailableSettings.JAKARTA_HBM2DDL_CREATE_SOURCE,
value = "script"
),
@Setting(
name = AvailableSettings.HBM2DDL_IMPORT_FILES_SQL_EXTRACTOR,
value = "org.hibernate.tool.schema.internal.script.MultiLineSqlScriptExtractor"
),
}
)
@DomainModel( annotatedClasses = { User.class, UserDetail.class, UserSkill.class } )
@SessionFactory( useCollectingStatementInspector = true )
@Jira( "https://hibernate.atlassian.net/browse/HHH-16019" )
@RequiresDialect( H2Dialect.class )
public class WhereFragmentTests {
/**
* Loads a User, fetching their detail and skills using an entity-graph
*/
public User findUserByIdUsingEntityGraph(Integer id, SessionFactoryScope factoryScope) {
return factoryScope.fromTransaction( (session) -> {
final Map<String, Object> properties = toSettingsMap(
SpecHints.HINT_SPEC_FETCH_GRAPH,
session.getEntityGraph("user-entity-graph")
);
return session.find(User.class, id, properties);
} );
}
/**
* Loads a User (via HQL), fetching their detail and skills using an entity-graph
*/
public User findUserByNameUsingEntityGraph(String name, SessionFactoryScope factoryScope) {
return factoryScope.fromTransaction( (session) -> {
final RootGraphImplementor<?> entityGraph = session.getEntityGraph( "user-entity-graph" );
return session.createSelectionQuery( "from User where name = :name", User.class )
.setParameter( "name", name )
.setHint( SpecHints.HINT_SPEC_FETCH_GRAPH, entityGraph )
.uniqueResult();
} );
}
/**
* Loads a User (via HQL), fetching their detail and skills using HQL fetch joins
*/
public User findUserByNameUsingHqlFetches(String name, SessionFactoryScope factoryScope) {
final String queryString = "from User u left join fetch u.detail left join fetch u.skills where u.name=?1";
return factoryScope.fromTransaction( (session) ->session
.createSelectionQuery( queryString, User.class )
.setParameter( 1, name )
.uniqueResult()
);
}
@AfterEach
public void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.createNativeMutationQuery( "delete from t_user_skills" ).executeUpdate();
session.createNativeMutationQuery( "delete t_user_details" ).executeUpdate();
session.createNativeMutationQuery( "delete t_users" ).executeUpdate();
} );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Alice - no detail and no skills
/**
* Create the user Alice who has no details and no skills
*/
private void createAlice(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
User user = new User( 1, "Alice");
session.persist( user );
} );
}
private void verifyAlice(User alice) {
assertThat( alice ).isNotNull();
assertThat( alice.getName() ).isEqualTo( "Alice" );
assertThat( alice.getDetail() ).isNull();
assertThat( alice.getSkills() ).isEmpty();
}
@Test
public void testFindAliceById(SessionFactoryScope scope) {
createAlice( scope );
User alice = findUserByIdUsingEntityGraph( 1, scope );
verifyAlice( alice );
}
@Test
public void testFindAliceByHql(SessionFactoryScope scope) {
createAlice( scope );
User alice = findUserByNameUsingEntityGraph("Alice", scope);
verifyAlice( alice );
}
@Test
public void testFindAliceByHqlWithFetchJoin(SessionFactoryScope scope) {
createAlice( scope );
User alice = findUserByNameUsingHqlFetches("Alice", scope);
verifyAlice( alice );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Bob - an inactive detail and no skills
/**
* Create the user Bob who has an inactive detail and no skills
*/
private static void createBob(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
User user = new User( 1, "Bob" );
session.persist( user );
UserDetail detail = new UserDetail( 1, "New York", false, user );
session.persist( detail );
} );
}
private static void verifyBob(User bob) {
assertThat( bob ).isNotNull();
assertThat( bob.getName() ).isEqualTo( "Bob" );
assertThat( bob.getDetail() ).isNull();
assertThat( bob.getSkills() ).isEmpty();
}
@Test
public void testFindBobById(SessionFactoryScope scope) {
createBob( scope );
User bob = findUserByIdUsingEntityGraph( 1, scope );
verifyBob( bob );
}
@Test
public void testFindBobByHql(SessionFactoryScope scope) {
createBob( scope );
User bob = findUserByNameUsingEntityGraph( "Bob", scope );
verifyBob( bob );
}
@Test
public void testFindBobByHqlWithFetchJoin(SessionFactoryScope scope) {
createBob( scope );
User bob = findUserByNameUsingHqlFetches( "Bob", scope );
verifyBob( bob );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Charlie - an active detail and a deleted skill
/**
* Create the user Bob who has an active detail and a deleted skill
*/
private static void createCharlie(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
User user = new User( 1, "Charlie" );
session.persist(user);
UserDetail detail = new UserDetail( 1, "Paris", true, user );
session.persist(detail);
UserSkill skill = new UserSkill( 1, "Java", true, user );
session.persist(skill);
} );
}
private void verifyCharlie(User user) {
assertThat( user ).isNotNull();
assertThat( user.getName() ).isEqualTo( "Charlie" );
assertThat( user.getDetail() ).isNotNull();
assertThat( user.getDetail().getActive() ).isTrue();
assertThat( user.getDetail().getCity() ).isEqualTo( "Paris" );
assertThat( user.getSkills() ).isEmpty();
}
@Test
public void testFindCharlieById(SessionFactoryScope scope) {
createCharlie( scope );
verifyCharlie( findUserByIdUsingEntityGraph( 1, scope ) );
}
@Test
public void testFindCharlieByHqlWithGraph(SessionFactoryScope scope) {
createCharlie( scope );
verifyCharlie( findUserByNameUsingEntityGraph( "Charlie", scope ) );
}
@Test
public void testFindCharlieByHqlWithJoinFetch(SessionFactoryScope scope) {
createCharlie( scope );
verifyCharlie( findUserByNameUsingHqlFetches( "Charlie", scope ) );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// David - multiple details, single skill
/**
* Create the user David with multiple details, single skill
*/
private static void createDavid(SessionFactoryScope scope) {
scope.inTransaction( (entityManager) -> {
User user = new User( 1, "David" );
entityManager.persist( user );
UserDetail detail1 = new UserDetail( 1, "London", false, user );
entityManager.persist( detail1 );
UserDetail detail2 = new UserDetail( 2, "Rome", true, user );
entityManager.persist( detail2 );
UserSkill skill = new UserSkill( 1, "Kotlin", false, user );
entityManager.persist( skill );
} );
}
private static void verifyDavid(User david) {
assertNotNull( david );
assertEquals("David", david.getName());
assertNotNull( david.getDetail());
assertTrue( david.getDetail().getActive());
assertEquals("Rome", david.getDetail().getCity());
assertFalse( david.getSkills().isEmpty());
assertTrue( david.getSkills().stream().noneMatch(UserSkill::getDeleted));
assertEquals(1, david.getSkills().size());
assertEquals("Kotlin", david.getSkills().stream().findFirst().orElseThrow(IllegalStateException::new).getSkillName());
}
@Test
public void testFindDavidById(SessionFactoryScope scope) {
createDavid( scope );
User david = findUserByIdUsingEntityGraph( 1, scope );
verifyDavid( david );
}
@Test
public void testFindDavidByHqlWithGraph(SessionFactoryScope scope) {
createDavid( scope );
User david = findUserByNameUsingEntityGraph("David", scope );
verifyDavid( david );
}
@Test
public void testFindDavidByHqlWithJoinFetch(SessionFactoryScope scope) {
createDavid( scope );
User david = findUserByNameUsingHqlFetches("David", scope );
verifyDavid( david );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Eve
// - 1 active and 2 inactive details
// - 1 active and 1 inactive skills
private static void createEve(SessionFactoryScope scope) {
scope.inTransaction( (entityManager) -> {
User user = new User( 1, "Eve" );
entityManager.persist( user );
UserDetail detail1 = new UserDetail( 1, "Moscow", false, user );
entityManager.persist( detail1 );
UserDetail detail2 = new UserDetail( 2, "Istanbul", false, user );
entityManager.persist( detail2 );
UserDetail detail3 = new UserDetail( 3, "Berlin", true, user );
entityManager.persist( detail3 );
UserSkill skill1 = new UserSkill( 1, "Python", true, user );
entityManager.persist( skill1 );
UserSkill skill2 = new UserSkill( 2, "Ruby", false, user );
entityManager.persist( skill2 );
} );
}
private static void verifyEve(User eve) {
assertNotNull( eve );
assertEquals("Eve", eve.getName());
assertNotNull( eve.getDetail());
assertTrue( eve.getDetail().getActive());
assertEquals("Berlin", eve.getDetail().getCity());
assertFalse( eve.getSkills().isEmpty());
assertTrue( eve.getSkills().stream().noneMatch(UserSkill::getDeleted));
assertEquals(1, eve.getSkills().size());
assertEquals("Ruby", eve.getSkills().stream().findFirst().orElseThrow(IllegalStateException::new).getSkillName());
}
@Test
public void testFindEveById(SessionFactoryScope scope) {
createEve( scope );
User eve = findUserByIdUsingEntityGraph( 1, scope );
verifyEve( eve );
}
@Test
public void testFindEveByHqlWithGraph(SessionFactoryScope scope) {
createEve( scope );
User eve = findUserByNameUsingEntityGraph( "Eve", scope );
verifyEve( eve );
}
@Test
public void testFindEveByHqlWithJoinFetch(SessionFactoryScope scope) {
createEve( scope );
User eve = findUserByNameUsingHqlFetches( "Eve", scope );
verifyEve( eve );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Frank
// - 1 active detail
// - 2 deleted and 2 active skills
/**
* Create the user Frank with a single active detail and 2 deleted and 2 active skills
*/
private static void createFrank(SessionFactoryScope scope) {
scope.inTransaction( (entityManager) -> {
User user = new User( 1, "Frank" );
entityManager.persist( user );
UserDetail detail = new UserDetail( 1, "Madrid", true, user );
entityManager.persist( detail );
UserSkill skill1 = new UserSkill( 1, "Rust", true, user );
entityManager.persist( skill1 );
UserSkill skill2 = new UserSkill( 2, "Erlang", false, user );
entityManager.persist( skill2 );
UserSkill skill3 = new UserSkill( 3, "Go", false, user );
entityManager.persist( skill3 );
UserSkill skill4 = new UserSkill( 4, "C", true, user );
entityManager.persist( skill4 );
} );
}
private static void verifyFrank(User frank) {
assertNotNull( frank );
assertEquals("Frank", frank.getName());
assertNotNull( frank.getDetail());
assertTrue( frank.getDetail().getActive());
assertEquals("Madrid", frank.getDetail().getCity());
assertFalse( frank.getSkills().isEmpty());
assertTrue( frank.getSkills().stream().noneMatch(UserSkill::getDeleted));
assertEquals(2, frank.getSkills().size());
}
@Test
public void testFindFrankById(SessionFactoryScope scope) {
createFrank( scope );
User frank = findUserByIdUsingEntityGraph( 1, scope );
verifyFrank( frank );
}
@Test
public void testFindFrankByHqlWithGraph(SessionFactoryScope scope) {
createFrank( scope );
User frank = findUserByNameUsingEntityGraph( "Frank", scope );
verifyFrank( frank );
}
@Test
public void testFindFrankByHqlWithJoinFetch(SessionFactoryScope scope) {
createFrank( scope );
User frank = findUserByNameUsingHqlFetches( "Frank", scope );
verifyFrank( frank );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Grace
// - 1 active and 1 inactive details
// - 4 inactive skills
private static void createGrace(SessionFactoryScope scope) {
scope.inTransaction( (entityManager) -> {
User user = new User( 1, "Grace" );
entityManager.persist( user );
UserDetail detail1 = new UserDetail( 1, "Vienna", false, user );
entityManager.persist( detail1 );
UserDetail detail2 = new UserDetail( 2, "Barcelona", true, user );
entityManager.persist( detail2 );
UserSkill skill1 = new UserSkill( 1, "PHP", false, user );
entityManager.persist( skill1 );
UserSkill skill2 = new UserSkill( 2, "Swift", false, user );
entityManager.persist( skill2 );
UserSkill skill3 = new UserSkill( 3, "Dart", false, user );
entityManager.persist( skill3 );
UserSkill skill4 = new UserSkill( 4, "Scala", false, user );
entityManager.persist( skill4 );
} );
}
private static void verifyGrace(User grace) {
assertNotNull( grace );
assertEquals("Grace", grace.getName());
assertNotNull( grace.getDetail());
assertTrue( grace.getDetail().getActive());
assertEquals("Barcelona", grace.getDetail().getCity());
assertFalse( grace.getSkills().isEmpty());
assertTrue( grace.getSkills().stream().noneMatch(UserSkill::getDeleted));
assertEquals(4, grace.getSkills().size());
}
@Test
public void testFindGraceById(SessionFactoryScope scope) {
createGrace( scope );
User grace = findUserByIdUsingEntityGraph( 1, scope );
verifyGrace( grace );
}
@Test
public void testFindGraceByHqlWithGraph(SessionFactoryScope scope) {
createGrace( scope );
User grace = findUserByNameUsingEntityGraph( "Grace", scope );
verifyGrace( grace );
}
@Test
public void testFindGraceByHqlWithJoinFetch(SessionFactoryScope scope) {
createGrace( scope );
User grace = findUserByNameUsingHqlFetches( "Grace", scope );
verifyGrace( grace );
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Zurg
// - 1 active and 1 inactive detail
// - 1 active and 1 inactive skill
private void createZurg(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final User user = new User( 1, "Zurg" );
session.persist( user );
UserDetail detail1 = new UserDetail( 1, "Infinity", false, user );
session.persist( detail1 );
UserDetail detail2 = new UserDetail( 2, "Beyond", true, user );
session.persist( detail2 );
UserSkill skill1 = new UserSkill( 1, "Plundering", false, user );
session.persist( skill1 );
UserSkill skill2 = new UserSkill( 2, "Pillaging", true, user );
session.persist( skill2 );
} );
}
@Test
public void testLoadEntity(SessionFactoryScope scope) {
createZurg( scope );
scope.inTransaction( (session) -> {
final UserSkill skill1 = session.find( UserSkill.class, 1 );
assertThat( skill1 ).isNotNull();
final UserSkill skill2 = session.find( UserSkill.class, 2 );
assertThat( skill2 ).isNull();
} );
scope.inTransaction( (session) -> {
final UserDetail detail1 = session.find( UserDetail.class, 1 );
assertThat( detail1 ).isNull();
final UserDetail detail2 = session.find( UserDetail.class, 2 );
assertThat( detail2 ).isNotNull();
} );
}
@Test
public void testSubsequentInitialization(SessionFactoryScope scope) {
createZurg( scope );
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
User user = session.find( User.class, 1 );
// this should have initialized User & User#detail in 2 separate selects
assertThat( Hibernate.isInitialized( user.getDetail() ) ).isTrue();
assertThat( Hibernate.isInitialized( user.getSkills() ) ).isFalse();
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
// trigger load of User#skills
statementInspector.clear();
Hibernate.initialize( user.getSkills() );
assertThat( Hibernate.isInitialized( user.getSkills() ) ).isTrue();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( user.getSkills() ).hasSize( 1 );
} );
}
}

View File

@ -45,11 +45,6 @@ public class EagerToManyWhereUseClassWhereViaAnnotationTest extends BaseNonConfi
return new Class[] { Product.class, Category.class };
}
@Override
protected void addSettings(Map<String,Object> settings) {
settings.put( AvailableSettings.USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS, "false" );
}
@Test
@TestForIssue( jiraKey = "HHH-15936" )
public void testAssociatedWhereClause() {
@ -198,7 +193,7 @@ public class EagerToManyWhereUseClassWhereViaAnnotationTest extends BaseNonConfi
@Entity(name = "Category")
@Table(name = "CATEGORY")
@Where(clause = "inactive = 0", applyInToManyFetch = true)
@Where(clause = "inactive = 0")
public static class Category {
@Id
private int id;

View File

@ -0,0 +1,30 @@
/*
Needed because the test model maps a "non standard" schema.
The test models `User#detail` and `UserDetail#user` as a
bi-directional one-to-one, meaning that Hibernate would
normally (and correctly) create `t_user_details.user_fk` as
unique. The test model changes that cardinality by use of
`@Where`.
*/
drop table if exists t_user_details cascade;
drop table if exists t_user_skills cascade;
drop table if exists t_users cascade;
create table t_user_details (
detail_id integer not null,
is_active boolean,
city varchar(255),
user_fk integer,
primary key (detail_id)
);
create table t_user_skills (
skill_id integer not null,
has_deleted boolean,
skill_name varchar(255),
user_fk integer,
primary key (skill_id)
);
create table t_users (
user_id integer not null,
user_name varchar(255),
primary key (user_id)
);