HHH-16830 Ensure filters applied for by key lookups don't mess with to-one associations

This commit is contained in:
Christian Beikov 2024-05-27 19:31:23 +02:00
parent 4125902eea
commit be8705f317
31 changed files with 426 additions and 143 deletions

View File

@ -102,5 +102,5 @@ public interface Filter {
*
* @return The flag value
*/
boolean isApplyToLoadById();
boolean isApplyToLoadByKey();
}

View File

@ -10,6 +10,8 @@ import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.hibernate.Incubating;
import static java.lang.annotation.ElementType.PACKAGE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@ -101,7 +103,8 @@ public @interface FilterDef {
* be applied on direct fetches or not.
* <p>
* If the flag is true, the filter will be
* applied on direct fetches, such as findById().
* applied on direct fetches, such as {@link org.hibernate.Session#find(Class, Object)}.
*/
boolean applyToLoadById() default false;
@Incubating
boolean applyToLoadByKey() default false;
}

View File

@ -59,7 +59,7 @@ public class TenantIdBinder implements AttributeBinder<TenantId> {
"",
singletonMap( PARAMETER_NAME, tenantIdType ),
Collections.emptyMap(),
true,
false,
true
)
);

View File

@ -93,7 +93,7 @@ class FilterDefBinder {
explicitParamJaMappings,
parameterResolvers,
filterDef.autoEnabled(),
filterDef.applyToLoadById()
filterDef.applyToLoadByKey()
);
LOG.debugf( "Binding filter definition: %s", filterDefinition.getFilterName() );
context.getMetadataCollector().addFilterDefinition( filterDefinition );

View File

@ -37,30 +37,37 @@ public class FilterDefinition implements Serializable {
private final Map<String, JdbcMapping> explicitParamJaMappings = new HashMap<>();
private final Map<String, ManagedBean<? extends Supplier<?>>> parameterResolverMap = new HashMap<>();
private final boolean autoEnabled;
private final boolean applyToLoadById;
private final boolean applyToLoadByKey;
/**
* Construct a new FilterDefinition instance.
*
* @param name The name of the filter for which this configuration is in effect.
*/
public FilterDefinition(String name, String defaultCondition, @Nullable Map<String, JdbcMapping> explicitParamJaMappings) {
public FilterDefinition(
String name,
String defaultCondition,
@Nullable Map<String, JdbcMapping> explicitParamJaMappings) {
this( name, defaultCondition, explicitParamJaMappings, Collections.emptyMap(), false, false);
}
public FilterDefinition(
String name, String defaultCondition, @Nullable Map<String, JdbcMapping> explicitParamJaMappings,
Map<String, ManagedBean<? extends Supplier<?>>> parameterResolverMap, boolean autoEnabled, boolean applyToLoadById) {
String name,
String defaultCondition,
@Nullable Map<String, JdbcMapping> explicitParamJaMappings,
Map<String, ManagedBean<? extends Supplier<?>>> parameterResolverMap,
boolean autoEnabled,
boolean applyToLoadByKey) {
this.filterName = name;
this.defaultFilterCondition = defaultCondition;
if ( explicitParamJaMappings != null ) {
this.explicitParamJaMappings.putAll( explicitParamJaMappings );
}
this.applyToLoadById = applyToLoadById;
if ( parameterResolverMap != null ) {
this.parameterResolverMap.putAll( parameterResolverMap );
}
this.autoEnabled = autoEnabled;
this.applyToLoadByKey = applyToLoadByKey;
}
/**
@ -109,8 +116,8 @@ public class FilterDefinition implements Serializable {
*
* @return The flag value.
*/
public boolean isApplyToLoadById() {
return applyToLoadById;
public boolean isApplyToLoadByKey() {
return applyToLoadByKey;
}
/**

View File

@ -13,7 +13,6 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.hibernate.Filter;
import org.hibernate.Internal;
@ -50,10 +49,10 @@ public class LoadQueryInfluencers implements Serializable {
private CascadingFetchProfile enabledCascadingFetchProfile;
//Lazily initialized!
private HashSet<String> enabledFetchProfileNames;
private @Nullable HashSet<String> enabledFetchProfileNames;
//Lazily initialized!
private HashMap<String,Filter> enabledFilters;
private @Nullable HashMap<String,Filter> enabledFilters;
private boolean subselectFetchEnabled;
@ -76,12 +75,37 @@ public class LoadQueryInfluencers implements Serializable {
for (FilterDefinition filterDefinition : sessionFactory.getAutoEnabledFilters()) {
FilterImpl filter = new FilterImpl( filterDefinition );
if ( enabledFilters == null ) {
this.enabledFilters = new HashMap<>();
enabledFilters = new HashMap<>();
}
enabledFilters.put( filterDefinition.getFilterName(), filter );
}
}
/**
* Special constructor for {@link #copyForLoadByKey()}.
*/
private LoadQueryInfluencers(LoadQueryInfluencers original) {
this.sessionFactory = original.sessionFactory;
this.enabledCascadingFetchProfile = original.enabledCascadingFetchProfile;
this.enabledFetchProfileNames = original.enabledFetchProfileNames == null ? null : new HashSet<>( original.enabledFetchProfileNames );
this.subselectFetchEnabled = original.subselectFetchEnabled;
this.batchSize = original.batchSize;
this.readOnly = original.readOnly;
HashMap<String,Filter> enabledFilters;
if ( original.enabledFilters == null ) {
enabledFilters = null;
}
else {
enabledFilters = new HashMap<>( original.enabledFilters.size() );
for ( Map.Entry<String, Filter> entry : original.enabledFilters.entrySet() ) {
if ( entry.getValue().isApplyToLoadByKey() ) {
enabledFilters.put( entry.getKey(), entry.getValue() );
}
}
}
this.enabledFilters = enabledFilters;
}
public EffectiveEntityGraph applyEntityGraph(@Nullable RootGraphImplementor<?> rootGraph, @Nullable GraphSemantic graphSemantic) {
final EffectiveEntityGraph effectiveEntityGraph = getEffectiveEntityGraph();
if ( graphSemantic != null ) {
@ -156,6 +180,7 @@ public class LoadQueryInfluencers implements Serializable {
}
public Map<String,Filter> getEnabledFilters() {
final HashMap<String, Filter> enabledFilters = this.enabledFilters;
if ( enabledFilters == null ) {
return Collections.emptyMap();
}
@ -169,20 +194,6 @@ public class LoadQueryInfluencers implements Serializable {
}
}
/**
* Returns a Map of enabled filters that have the applyToLoadById
* flag set to true
* @return a Map of enabled filters that have the applyToLoadById
* flag set to true
*/
public Map<String, Filter> getEnabledFiltersForFind() {
return getEnabledFilters()
.entrySet()
.stream()
.filter(f -> f.getValue().isApplyToLoadById())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/**
* Returns an unmodifiable Set of enabled filter names.
* @return an unmodifiable Set of enabled filter names.
@ -283,8 +294,14 @@ public class LoadQueryInfluencers implements Serializable {
@Internal
public @Nullable HashSet<String> adjustFetchProfiles(@Nullable Set<String> disabledFetchProfiles, @Nullable Set<String> enabledFetchProfiles) {
final HashSet<String> oldFetchProfiles =
hasEnabledFetchProfiles() ? new HashSet<>( enabledFetchProfileNames ) : null;
final HashSet<String> currentEnabledFetchProfileNames = this.enabledFetchProfileNames;
final HashSet<String> oldFetchProfiles;
if ( currentEnabledFetchProfileNames == null || currentEnabledFetchProfileNames.isEmpty() ) {
oldFetchProfiles = null;
}
else {
oldFetchProfiles = new HashSet<>( currentEnabledFetchProfileNames );
}
if ( disabledFetchProfiles != null && enabledFetchProfileNames != null ) {
enabledFetchProfileNames.removeAll( disabledFetchProfiles );
}
@ -391,4 +408,7 @@ public class LoadQueryInfluencers implements Serializable {
return false;
}
public LoadQueryInfluencers copyForLoadByKey() {
return new LoadQueryInfluencers( this );
}
}

View File

@ -123,6 +123,10 @@ public class FilterHelper {
return aliasTableMap.size() == 1 && aliasTableMap.containsKey( null );
}
public String[] getFilterNames() {
return filterNames;
}
public boolean isAffectedBy(Map<String, Filter> enabledFilters) {
for ( String filterName : filterNames ) {
if ( enabledFilters.containsKey( filterName ) ) {
@ -132,6 +136,16 @@ public class FilterHelper {
return false;
}
public boolean isAffectedByApplyToLoadByKey(Map<String, Filter> enabledFilters) {
for ( String filterName : filterNames ) {
Filter filter = enabledFilters.get( filterName );
if ( filter != null && filter.isApplyToLoadByKey() ) {
return true;
}
}
return false;
}
public static void applyBaseRestrictions(
Consumer<Predicate> predicateConsumer,
Restrictable restrictable,

View File

@ -32,7 +32,7 @@ public class FilterImpl implements Filter, Serializable {
private final String filterName;
private final Map<String,Object> parameters = new HashMap<>();
private final boolean autoEnabled;
private final boolean applyToLoadById;
private final boolean applyToLoadByKey;
void afterDeserialize(SessionFactoryImplementor factory) {
definition = factory.getFilterDefinition( filterName );
@ -48,7 +48,7 @@ public class FilterImpl implements Filter, Serializable {
this.definition = configuration;
filterName = definition.getFilterName();
this.autoEnabled = definition.isAutoEnabled();
this.applyToLoadById = definition.isApplyToLoadById();
this.applyToLoadByKey = definition.isApplyToLoadByKey();
}
public FilterDefinition getFilterDefinition() {
@ -80,8 +80,8 @@ public class FilterImpl implements Filter, Serializable {
*
* @return The flag value.
*/
public boolean isApplyToLoadById() {
return applyToLoadById;
public boolean isApplyToLoadByKey() {
return applyToLoadByKey;
}
public Map<String,?> getParameters() {

View File

@ -10,7 +10,7 @@ import org.hibernate.Hibernate;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.loader.ast.spi.EntityBatchLoader;
import org.hibernate.metamodel.mapping.EntityMappingType;
@ -24,9 +24,9 @@ public abstract class AbstractEntityBatchLoader<T>
private final SingleIdEntityLoaderStandardImpl<T> singleIdLoader;
public AbstractEntityBatchLoader(EntityMappingType entityDescriptor, SessionFactoryImplementor sessionFactory) {
super( entityDescriptor, sessionFactory );
singleIdLoader = new SingleIdEntityLoaderStandardImpl<>( entityDescriptor, sessionFactory );
public AbstractEntityBatchLoader(EntityMappingType entityDescriptor, LoadQueryInfluencers loadQueryInfluencers) {
super( entityDescriptor, loadQueryInfluencers.getSessionFactory() );
this.singleIdLoader = new SingleIdEntityLoaderStandardImpl<>( entityDescriptor, loadQueryInfluencers );
}
protected abstract void initializeEntities(

View File

@ -42,6 +42,7 @@ public class EntityBatchLoaderArrayParam<T>
implements SqlArrayMultiKeyLoader {
private final int domainBatchSize;
private final LoadQueryInfluencers loadQueryInfluencers;
private final BasicEntityIdentifierMapping identifierMapping;
private final JdbcMapping arrayJdbcMapping;
private final JdbcParameter jdbcParameter;
@ -63,8 +64,9 @@ public class EntityBatchLoaderArrayParam<T>
public EntityBatchLoaderArrayParam(
int domainBatchSize,
EntityMappingType entityDescriptor,
SessionFactoryImplementor sessionFactory) {
super( entityDescriptor, sessionFactory );
LoadQueryInfluencers loadQueryInfluencers) {
super( entityDescriptor, loadQueryInfluencers );
this.loadQueryInfluencers = loadQueryInfluencers;
this.domainBatchSize = domainBatchSize;
if ( MULTI_KEY_LOAD_LOGGER.isDebugEnabled() ) {
@ -89,7 +91,7 @@ public class EntityBatchLoaderArrayParam<T>
sqlAst = LoaderSelectBuilder.createSelectBySingleArrayParameter(
getLoadable(),
identifierMapping,
new LoadQueryInfluencers( sessionFactory ),
loadQueryInfluencers,
LockOptions.NONE,
jdbcParameter,
sessionFactory

View File

@ -46,6 +46,7 @@ public class EntityBatchLoaderInPredicate<T>
private final int domainBatchSize;
private final int sqlBatchSize;
private final LoadQueryInfluencers loadQueryInfluencers;
private final JdbcParametersList jdbcParameters;
private final SelectStatement sqlAst;
private final JdbcOperationQuerySelect jdbcSelectOperation;
@ -56,8 +57,9 @@ public class EntityBatchLoaderInPredicate<T>
public EntityBatchLoaderInPredicate(
int domainBatchSize,
EntityMappingType entityDescriptor,
SessionFactoryImplementor sessionFactory) {
super( entityDescriptor, sessionFactory );
LoadQueryInfluencers loadQueryInfluencers) {
super( entityDescriptor, loadQueryInfluencers );
this.loadQueryInfluencers = loadQueryInfluencers;
this.domainBatchSize = domainBatchSize;
int idColumnCount = entityDescriptor.getEntityPersister().getIdentifierType().getColumnSpan( sessionFactory );
this.sqlBatchSize = sessionFactory.getJdbcServices()
@ -85,7 +87,7 @@ public class EntityBatchLoaderInPredicate<T>
identifierMapping,
null,
sqlBatchSize,
new LoadQueryInfluencers( sessionFactory ),
loadQueryInfluencers,
LockOptions.NONE,
jdbcParametersBuilder::add,
sessionFactory

View File

@ -30,6 +30,7 @@ import org.hibernate.metamodel.CollectionClassification;
import org.hibernate.metamodel.mapping.AttributeMapping;
import org.hibernate.metamodel.mapping.CollectionPart;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.EntityValuedModelPart;
import org.hibernate.metamodel.mapping.ForeignKeyDescriptor;
import org.hibernate.metamodel.mapping.ModelPart;
@ -67,6 +68,7 @@ import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate;
import org.hibernate.sql.ast.tree.predicate.InArrayPredicate;
import org.hibernate.sql.ast.tree.predicate.InListPredicate;
import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate;
import org.hibernate.sql.ast.tree.predicate.PredicateContainer;
import org.hibernate.sql.ast.tree.select.QueryPart;
import org.hibernate.sql.ast.tree.select.QuerySpec;
import org.hibernate.sql.ast.tree.select.SelectStatement;
@ -708,16 +710,15 @@ public class LoaderSelectBuilder {
}
private void applyFiltering(
QuerySpec querySpec,
PredicateContainer predicateContainer,
TableGroup tableGroup,
Restrictable restrictable,
SqlAstCreationState astCreationState) {
restrictable.applyBaseRestrictions(
querySpec::applyPredicate,
predicateContainer::applyPredicate,
tableGroup,
true,
// HHH-16830 Session.find should apply filters only if specified on the filter definition
loadQueryInfluencers.getEnabledFiltersForFind(),
loadQueryInfluencers.getEnabledFilters(),
null,
astCreationState
);
@ -962,9 +963,9 @@ public class LoaderSelectBuilder {
creationState
);
if ( fetch.getTiming() == FetchTiming.IMMEDIATE && isFetchablePluralAttributeMapping ) {
final PluralAttributeMapping pluralAttributeMapping = (PluralAttributeMapping) fetchable;
if ( joined ) {
if ( fetch.getTiming() == FetchTiming.IMMEDIATE && joined ) {
if ( isFetchablePluralAttributeMapping ) {
final PluralAttributeMapping pluralAttributeMapping = (PluralAttributeMapping) fetchable;
final TableGroup joinTableGroup = creationState.getFromClauseAccess()
.getTableGroup( fetchablePath );
final QuerySpec querySpec = creationState.getInflightQueryPart().getFirstQuerySpec();
@ -981,6 +982,19 @@ public class LoaderSelectBuilder {
creationState
);
}
else if ( fetchable instanceof ToOneAttributeMapping ) {
final EntityMappingType entityType = ( (ToOneAttributeMapping) fetchable ).getEntityMappingType();
final FromClauseAccess fromClauseAccess = creationState.getFromClauseAccess();
final TableGroup joinTableGroup = fromClauseAccess.getTableGroup( fetchablePath );
final TableGroupJoin join = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath() )
.findTableGroupJoin( joinTableGroup );
applyFiltering(
join,
joinTableGroup,
entityType,
creationState
);
}
}
fetches.add( fetch );

View File

@ -30,7 +30,6 @@ import org.hibernate.loader.ast.spi.MultiIdLoadOptions;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
import org.hibernate.sql.ast.tree.expression.JdbcParameter;
import org.hibernate.sql.ast.tree.select.SelectStatement;
import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl;
import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect;
@ -220,7 +219,7 @@ public class MultiIdEntityLoaderStandard<T> extends AbstractMultiIdEntityLoader<
getLoadable().getIdentifierMapping(),
null,
numberOfIdsInBatch,
session.getLoadQueryInfluencers(),
session.getLoadQueryInfluencers().copyForLoadByKey(),
lockOptions,
jdbcParametersBuilder::add,
getSessionFactory()

View File

@ -34,11 +34,11 @@ public class SingleIdEntityLoaderStandardImpl<T> extends SingleIdEntityLoaderSup
public SingleIdEntityLoaderStandardImpl(
EntityMappingType entityDescriptor,
SessionFactoryImplementor sessionFactory) {
LoadQueryInfluencers loadQueryInfluencers) {
this(
entityDescriptor,
sessionFactory,
(lockOptions, influencers) -> createLoadPlan( entityDescriptor, lockOptions, influencers, sessionFactory )
loadQueryInfluencers,
(lockOptions, influencers) -> createLoadPlan( entityDescriptor, lockOptions, influencers, influencers.getSessionFactory() )
);
}
@ -50,15 +50,14 @@ public class SingleIdEntityLoaderStandardImpl<T> extends SingleIdEntityLoaderSup
*/
protected SingleIdEntityLoaderStandardImpl(
EntityMappingType entityDescriptor,
SessionFactoryImplementor sessionFactory,
LoadQueryInfluencers influencers,
BiFunction<LockOptions, LoadQueryInfluencers, SingleIdLoadPlan<T>> loadPlanCreator) {
// todo (6.0) : consider creating a base AST and "cloning" it
super( entityDescriptor, sessionFactory );
super( entityDescriptor, influencers.getSessionFactory() );
this.loadPlanCreator = loadPlanCreator;
// see org.hibernate.persister.entity.AbstractEntityPersister#createLoaders
// we should preload a few - maybe LockMode.NONE and LockMode.READ
final LockOptions lockOptions = LockOptions.NONE;
final LoadQueryInfluencers influencers = new LoadQueryInfluencers( sessionFactory );
final SingleIdLoadPlan<T> plan = loadPlanCreator.apply( LockOptions.NONE, influencers );
if ( isLoadPlanReusable( lockOptions, influencers ) ) {
selectByLockMode.put( lockOptions.getLockMode(), plan );

View File

@ -25,7 +25,6 @@ import org.hibernate.metamodel.mapping.ManagedMappingType;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.mapping.SingularAttributeMapping;
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
import org.hibernate.metamodel.model.domain.NavigableRole;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
import org.hibernate.sql.ast.tree.select.SelectStatement;
@ -52,7 +51,8 @@ public class SingleUniqueKeyEntityLoaderStandard<T> implements SingleUniqueKeyEn
public SingleUniqueKeyEntityLoaderStandard(
EntityMappingType entityDescriptor,
SingularAttributeMapping uniqueKeyAttribute) {
SingularAttributeMapping uniqueKeyAttribute,
LoadQueryInfluencers loadQueryInfluencers) {
this.entityDescriptor = entityDescriptor;
this.uniqueKeyAttributePath = getAttributePath( uniqueKeyAttribute );
if ( uniqueKeyAttribute instanceof ToOneAttributeMapping ) {
@ -69,7 +69,7 @@ public class SingleUniqueKeyEntityLoaderStandard<T> implements SingleUniqueKeyEn
Collections.emptyList(),
uniqueKeyAttribute,
null,
new LoadQueryInfluencers( sessionFactory ),
loadQueryInfluencers,
LockOptions.NONE,
builder::add,
sessionFactory

View File

@ -33,19 +33,20 @@ public class StandardBatchLoaderFactory implements BatchLoaderFactory {
@Override
public <T> EntityBatchLoader<T> createEntityBatchLoader(
int domainBatchSize, EntityMappingType entityDescriptor,
SessionFactoryImplementor factory) {
int domainBatchSize,
EntityMappingType entityDescriptor,
LoadQueryInfluencers loadQueryInfluencers) {
final SessionFactoryImplementor factory = loadQueryInfluencers.getSessionFactory();
// NOTE : don't use the EntityIdentifierMapping here because it will not be known until later
final Type identifierType = entityDescriptor.getEntityPersister().getIdentifierType();
if ( identifierType.getColumnSpan( factory ) == 1
&& supportsSqlArrayType( factory.getJdbcServices().getDialect() )
&& identifierType instanceof BasicType ) {
// we can use a single ARRAY parameter to send all the ids
return new EntityBatchLoaderArrayParam<>( domainBatchSize, entityDescriptor, factory );
return new EntityBatchLoaderArrayParam<>( domainBatchSize, entityDescriptor, loadQueryInfluencers );
}
else {
return new EntityBatchLoaderInPredicate<>( domainBatchSize, entityDescriptor, factory );
return new EntityBatchLoaderInPredicate<>( domainBatchSize, entityDescriptor, loadQueryInfluencers );
}
}

View File

@ -18,6 +18,21 @@ import org.hibernate.service.Service;
* @author Steve Ebersole
*/
public interface BatchLoaderFactory extends Service {
/**
* Create a BatchLoader for batch-loadable entities.
*
* @param domainBatchSize The total number of entities (max) that will be need to be initialized
* @param entityDescriptor The entity mapping metadata
* @deprecated Use {@link #createEntityBatchLoader(int, EntityMappingType, LoadQueryInfluencers)} instead
*/
@Deprecated(forRemoval = true)
default <T> EntityBatchLoader<T> createEntityBatchLoader(
int domainBatchSize,
EntityMappingType entityDescriptor,
SessionFactoryImplementor factory) {
return createEntityBatchLoader( domainBatchSize, entityDescriptor, new LoadQueryInfluencers( factory ) );
}
/**
* Create a BatchLoader for batch-loadable entities.
*
@ -27,7 +42,7 @@ public interface BatchLoaderFactory extends Service {
<T> EntityBatchLoader<T> createEntityBatchLoader(
int domainBatchSize,
EntityMappingType entityDescriptor,
SessionFactoryImplementor factory);
LoadQueryInfluencers loadQueryInfluencers);
/**
* Create a BatchLoader for batch-loadable collections.

View File

@ -35,6 +35,13 @@ public interface Loadable extends ModelPart, RootTableGroupProducer {
|| isAffectedByBatchSize( influencers );
}
default boolean isAffectedByInfluencersForLoadByKey(LoadQueryInfluencers influencers) {
return isAffectedByEntityGraph( influencers )
|| isAffectedByEnabledFetchProfiles( influencers )
|| isAffectedByEnabledFiltersForLoadByKey( influencers )
|| isAffectedByBatchSize( influencers );
}
default boolean isNotAffectedByInfluencers(LoadQueryInfluencers influencers) {
return !isAffectedByEntityGraph( influencers )
&& !isAffectedByEnabledFetchProfiles( influencers )
@ -55,6 +62,13 @@ public interface Loadable extends ModelPart, RootTableGroupProducer {
*/
boolean isAffectedByEnabledFilters(LoadQueryInfluencers influencers);
/**
* Whether any of the "influencers" affect this loadable.
*/
default boolean isAffectedByEnabledFiltersForLoadByKey(LoadQueryInfluencers influencers) {
return isAffectedByInfluencers( influencers.copyForLoadByKey() );
}
/**
* Whether the {@linkplain LoadQueryInfluencers#getEffectiveEntityGraph() effective entity-graph}
* applies to this loadable

View File

@ -547,6 +547,11 @@ public interface EntityMappingType
return getEntityPersister().isAffectedByEnabledFilters( influencers );
}
@Override
default boolean isAffectedByEnabledFiltersForLoadByKey(LoadQueryInfluencers influencers) {
return getEntityPersister().isAffectedByEnabledFiltersForLoadByKey( influencers );
}
@Override
default boolean isAffectedByEntityGraph(LoadQueryInfluencers influencers) {
return getEntityPersister().isAffectedByEntityGraph( influencers );

View File

@ -1077,6 +1077,11 @@ public class PluralAttributeMappingImpl
return getCollectionDescriptor().isAffectedByEnabledFilters( influencers );
}
@Override
public boolean isAffectedByEnabledFiltersForLoadByKey(LoadQueryInfluencers influencers) {
return getCollectionDescriptor().isAffectedByEnabledFiltersForLoadByKey( influencers );
}
@Override
public boolean isAffectedByEntityGraph(LoadQueryInfluencers influencers) {
return getCollectionDescriptor().isAffectedByEntityGraph( influencers );

View File

@ -248,7 +248,7 @@ public class ToOneAttributeMapping
stateArrayPosition,
fetchableIndex,
attributeMetadata,
adjustFetchTiming( mappedFetchTiming, bootValue ),
adjustFetchTiming( mappedFetchTiming, bootValue, entityMappingType ),
mappedFetchStyle,
declaringType,
propertyAccess
@ -270,7 +270,7 @@ public class ToOneAttributeMapping
);
if ( bootValue instanceof ManyToOne ) {
final ManyToOne manyToOne = (ManyToOne) bootValue;
this.notFoundAction = ( (ManyToOne) bootValue ).getNotFoundAction();
this.notFoundAction = determineNotFoundAction( ( (ManyToOne) bootValue ).getNotFoundAction(), entityMappingType );
if ( manyToOne.isLogicalOneToOne() ) {
cardinality = Cardinality.LOGICAL_ONE_TO_ONE;
}
@ -424,7 +424,7 @@ public class ToOneAttributeMapping
else {
this.bidirectionalAttributePath = SelectablePath.parse( oneToOne.getMappedByProperty() );
}
notFoundAction = null;
notFoundAction = determineNotFoundAction( null, entityMappingType );
isKeyTableNullable = isNullable();
isOptional = !bootValue.isConstrained();
isInternalLoadNullable = isNullable();
@ -569,6 +569,16 @@ public class ToOneAttributeMapping
}
}
private NotFoundAction determineNotFoundAction(NotFoundAction notFoundAction, EntityMappingType entityMappingType) {
// When a filter exists that affects a singular association, we have to enable NotFound handling
// to force an exception if the filter would result in the entity not being found.
// If we silently just read null, this could lead to data loss on flush
if ( entityMappingType.getEntityPersister().hasFilterForLoadByKey() && notFoundAction == null ) {
return NotFoundAction.EXCEPTION;
}
return notFoundAction;
}
private static SelectablePath findBidirectionalOneToManyAttributeName(
String propertyPath,
ManagedMappingType declaringType,
@ -638,12 +648,18 @@ public class ToOneAttributeMapping
return null;
}
private static FetchTiming adjustFetchTiming(FetchTiming mappedFetchTiming, ToOne bootValue) {
private static FetchTiming adjustFetchTiming(
FetchTiming mappedFetchTiming,
ToOne bootValue,
EntityMappingType entityMappingType) {
if ( bootValue instanceof ManyToOne ) {
if ( ( (ManyToOne) bootValue ).getNotFoundAction() != null ) {
return FetchTiming.IMMEDIATE;
}
}
if ( entityMappingType.getEntityPersister().hasFilterForLoadByKey() ) {
return FetchTiming.IMMEDIATE;
}
return mappedFetchTiming;
}

View File

@ -1677,6 +1677,18 @@ public abstract class AbstractCollectionPersister
}
}
@Override
public boolean isAffectedByEnabledFiltersForLoadByKey(LoadQueryInfluencers influencers) {
if ( influencers.hasEnabledFilters() ) {
final Map<String, Filter> enabledFilters = influencers.getEnabledFilters();
return filterHelper != null && filterHelper.isAffectedByApplyToLoadByKey( enabledFilters )
|| manyToManyFilterHelper != null && manyToManyFilterHelper.isAffectedByApplyToLoadByKey( enabledFilters );
}
else {
return false;
}
}
@Override
public boolean isAffectedByEntityGraph(LoadQueryInfluencers influencers) {
// todo (6.0) : anything to do here?

View File

@ -296,6 +296,10 @@ public interface CollectionPersister extends Restrictable {
throw new UnsupportedOperationException( "CollectionPersister used for [" + getRole() + "] does not support SQL AST" );
}
default boolean isAffectedByEnabledFiltersForLoadByKey(LoadQueryInfluencers influencers) {
throw new UnsupportedOperationException( "CollectionPersister used for [" + getRole() + "] does not support SQL AST" );
}
default boolean isAffectedByEntityGraph(LoadQueryInfluencers influencers) {
throw new UnsupportedOperationException( "CollectionPersister used for [" + getRole() + "] does not support SQL AST" );
}

View File

@ -17,7 +17,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@ -26,6 +25,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
@ -536,6 +536,16 @@ public abstract class AbstractEntityPersister
? MutableEntityEntryFactory.INSTANCE
: ImmutableEntityEntryFactory.INSTANCE;
// Handle any filters applied to the class level
filterHelper = isNotEmpty( persistentClass.getFilters() ) ? new FilterHelper(
persistentClass.getFilters(),
getEntityNameByTableNameMap(
persistentClass,
factory.getSqlStringGenerationContext()
),
factory
) : null;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
representationStrategy = creationContext.getBootstrapContext().getRepresentationStrategySelector()
@ -804,16 +814,6 @@ public abstract class AbstractEntityPersister
propertyDefinedOnSubclass = toBooleanArray( definedBySubclass );
// Handle any filters applied to the class level
filterHelper = isNotEmpty( persistentClass.getFilters() ) ? new FilterHelper(
persistentClass.getFilters(),
getEntityNameByTableNameMap(
persistentClass,
factory.getSqlStringGenerationContext()
),
factory
) : null;
useReferenceCacheEntries = shouldUseReferenceCacheEntries( creationContext.getSessionFactoryOptions() );
useShallowQueryCacheLayout = shouldUseShallowCacheLayout(
persistentClass.getQueryCacheLayout(),
@ -884,10 +884,10 @@ public abstract class AbstractEntityPersister
final int batchSize = loadQueryInfluencers.effectiveBatchSize( this );
return factory.getServiceRegistry()
.requireService( BatchLoaderFactory.class )
.createEntityBatchLoader( batchSize, this, factory );
.createEntityBatchLoader( batchSize, this, loadQueryInfluencers );
}
else {
return new SingleIdEntityLoaderStandardImpl<>( this, factory );
return new SingleIdEntityLoaderStandardImpl<>( this, loadQueryInfluencers );
}
}
@ -1255,6 +1255,18 @@ public abstract class AbstractEntityPersister
return storeDiscriminatorInShallowQueryCacheLayout;
}
@Override
public boolean hasFilterForLoadByKey() {
if ( filterHelper != null ) {
for ( String filterName : filterHelper.getFilterNames() ) {
if ( factory.getFilterDefinition( filterName ).isApplyToLoadByKey() ) {
return true;
}
}
}
return false;
}
@Override
public Iterable<UniqueKeyEntry> uniqueKeyEntries() {
if ( this.uniqueKeyEntries == null ) {
@ -2039,7 +2051,7 @@ public abstract class AbstractEntityPersister
);
}
return getUniqueKeyLoader( uniquePropertyName ).resolveId( key, session );
return getUniqueKeyLoader( uniquePropertyName, session ).resolveId( key, session );
}
@ -2652,16 +2664,25 @@ public abstract class AbstractEntityPersister
Object uniqueKey,
Boolean readOnly,
SharedSessionContractImplementor session) throws HibernateException {
return getUniqueKeyLoader( propertyName ).load( uniqueKey, LockOptions.NONE, readOnly, session );
return getUniqueKeyLoader( propertyName, session ).load( uniqueKey, LockOptions.NONE, readOnly, session );
}
private Map<SingularAttributeMapping, SingleUniqueKeyEntityLoader<?>> uniqueKeyLoadersNew;
protected SingleUniqueKeyEntityLoader<?> getUniqueKeyLoader(String attributeName) {
protected SingleUniqueKeyEntityLoader<?> getUniqueKeyLoader(String attributeName, SharedSessionContractImplementor session) {
final SingularAttributeMapping attribute = (SingularAttributeMapping) findByPath( attributeName );
final LoadQueryInfluencers influencers = session.getLoadQueryInfluencers();
// no subselect fetching for entities for now
if ( isAffectedByInfluencersForLoadByKey( influencers ) ) {
return new SingleUniqueKeyEntityLoaderStandard<>(
this,
attribute,
influencers.copyForLoadByKey()
);
}
final SingleUniqueKeyEntityLoader<?> existing;
if ( uniqueKeyLoadersNew == null ) {
uniqueKeyLoadersNew = new IdentityHashMap<>();
uniqueKeyLoadersNew = new ConcurrentHashMap<>();
existing = null;
}
else {
@ -2673,7 +2694,7 @@ public abstract class AbstractEntityPersister
}
else {
final SingleUniqueKeyEntityLoader<?> loader =
new SingleUniqueKeyEntityLoaderStandard<>( this, attribute );
new SingleUniqueKeyEntityLoaderStandard<>( this, attribute, new LoadQueryInfluencers( factory ) );
uniqueKeyLoadersNew.put( attribute, loader );
return loader;
}
@ -3744,8 +3765,8 @@ public abstract class AbstractEntityPersister
else {
final LoadQueryInfluencers influencers = session.getLoadQueryInfluencers();
// no subselect fetching for entities for now
return isAffectedByInfluencers( influencers )
? buildSingleIdEntityLoader( influencers )
return isAffectedByInfluencersForLoadByKey( influencers )
? buildSingleIdEntityLoader( influencers.copyForLoadByKey() )
: getSingleIdLoader();
}
}
@ -3863,6 +3884,30 @@ public abstract class AbstractEntityPersister
return false;
}
@Override
public boolean isAffectedByEnabledFiltersForLoadByKey(LoadQueryInfluencers loadQueryInfluencers) {
if ( filterHelper != null && loadQueryInfluencers.hasEnabledFilters() ) {
if ( filterHelper.isAffectedByApplyToLoadByKey( loadQueryInfluencers.getEnabledFilters() ) ) {
return true;
}
// we still need to verify collection fields to be eagerly loaded by join
final AttributeMappingsList attributeMappings = getAttributeMappings();
for ( int i = 0; i < attributeMappings.size(); i++ ) {
final AttributeMapping attributeMapping = attributeMappings.get( i );
if ( attributeMapping instanceof PluralAttributeMapping ) {
final PluralAttributeMapping pluralAttributeMapping = (PluralAttributeMapping) attributeMapping;
if ( pluralAttributeMapping.getMappedFetchOptions().getTiming() == FetchTiming.IMMEDIATE
&& pluralAttributeMapping.getMappedFetchOptions().getStyle() == FetchStyle.JOIN
&& pluralAttributeMapping.getCollectionDescriptor().isAffectedByEnabledFiltersForLoadByKey( loadQueryInfluencers ) ) {
return true;
}
}
}
}
return false;
}
@Override
public boolean isSubclassPropertyNullable(int i) {
return subclassPropertyNullabilityClosure[i];

View File

@ -1258,6 +1258,8 @@ public interface EntityPersister extends EntityMappingType, EntityMutationTarget
@Incubating
boolean storeDiscriminatorInShallowQueryCacheLayout();
boolean hasFilterForLoadByKey();
/**
* The property name of the "special" identifier property in HQL
*

View File

@ -14,13 +14,14 @@ import org.hibernate.sql.ast.SqlTreeCreationLogger;
import org.hibernate.sql.ast.spi.SqlAstTreeHelper;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.sql.ast.tree.predicate.PredicateContainer;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
/**
* @author Steve Ebersole
*/
public class TableGroupJoin implements TableJoin, DomainResultProducer {
public class TableGroupJoin implements TableJoin, PredicateContainer, DomainResultProducer {
private final NavigablePath navigablePath;
private final TableGroup joinedGroup;
@ -78,6 +79,7 @@ public class TableGroupJoin implements TableJoin, DomainResultProducer {
return predicate;
}
@Override
public void applyPredicate(Predicate predicate) {
this.predicate = SqlAstTreeHelper.combinePredicates( this.predicate, predicate );
}

View File

@ -760,6 +760,11 @@ public class GoofyPersisterClassProvider implements PersisterClassResolver {
return false;
}
@Override
public boolean hasFilterForLoadByKey() {
return false;
}
@Override
public Iterable<UniqueKeyEntry> uniqueKeyEntries() {
return Collections.emptyList();

View File

@ -54,7 +54,7 @@ public class LoadPlanBuilderTest {
.getMappingMetamodel()
.getEntityDescriptor( Message.class );
final SingleIdEntityLoaderStandardImpl<?> loader = new SingleIdEntityLoaderStandardImpl<>( entityDescriptor, sessionFactory );
final SingleIdEntityLoaderStandardImpl<?> loader = new SingleIdEntityLoaderStandardImpl<>( entityDescriptor, new LoadQueryInfluencers( sessionFactory ) );
final SingleIdLoadPlan<?> loadPlan = loader.resolveLoadPlan(
LockOptions.READ,
@ -89,7 +89,7 @@ public class LoadPlanBuilderTest {
final SessionFactoryImplementor sessionFactory = scope.getSessionFactory();
final EntityPersister entityDescriptor = (EntityPersister) sessionFactory.getRuntimeMetamodels().getEntityMappingType( Message.class );
final SingleIdEntityLoaderStandardImpl<?> loader = new SingleIdEntityLoaderStandardImpl<>( entityDescriptor, sessionFactory );
final SingleIdEntityLoaderStandardImpl<?> loader = new SingleIdEntityLoaderStandardImpl<>( entityDescriptor, new LoadQueryInfluencers( sessionFactory ) );
final LoadQueryInfluencers influencers = new LoadQueryInfluencers( sessionFactory ) {
@Override

View File

@ -739,6 +739,11 @@ public class PersisterClassProviderTest {
return false;
}
@Override
public boolean hasFilterForLoadByKey() {
return false;
}
@Override
public Iterable<UniqueKeyEntry> uniqueKeyEntries() {
return Collections.emptyList();

View File

@ -896,6 +896,11 @@ public class CustomPersister implements EntityPersister {
return false;
}
@Override
public boolean hasFilterForLoadByKey() {
return false;
}
@Override
public Iterable<UniqueKeyEntry> uniqueKeyEntries() {
return Collections.emptyList();

View File

@ -14,6 +14,7 @@ import java.util.function.Supplier;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityGraph;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
@ -23,13 +24,18 @@ import jakarta.persistence.NoResultException;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import org.hibernate.FetchNotFoundException;
import org.hibernate.Session;
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;
import org.hibernate.jpa.AvailableHints;
import org.hibernate.metamodel.CollectionClassification;
import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase;
import org.hibernate.testing.orm.junit.JiraKey;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.hibernate.cfg.AvailableSettings.DEFAULT_LIST_SEMANTICS;
@ -38,6 +44,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
/**
* @author Vlad Mihalcea
@ -58,47 +66,59 @@ public class FilterTest extends BaseEntityManagerFunctionalTestCase {
options.put( DEFAULT_LIST_SEMANTICS, CollectionClassification.BAG.name() );
}
@Test
public void testLifecycle() {
@Before
public void setup() {
doInJPA(this::entityManagerFactory, entityManager -> {
//tag::pc-filter-persistence-example[]
Client client = new Client()
.setId(1L)
.setName("John Doe")
.setType(AccountType.DEBIT);
.setId(1L)
.setName("John Doe")
.setType(AccountType.DEBIT);
Account account1;
client.addAccount(
account1 = new Account()
.setId(1L)
.setType(AccountType.CREDIT)
.setAmount(5000d)
.setRate(1.25 / 100)
.setActive(true)
);
client.addAccount(
new Account()
.setId(1L)
.setType(AccountType.CREDIT)
.setAmount(5000d)
.setRate(1.25 / 100)
.setActive(true)
);
new Account()
.setId(2L)
.setType(AccountType.DEBIT)
.setAmount(0d)
.setRate(1.05 / 100)
.setActive(false)
.setParentAccount( account1 )
);
client.addAccount(
new Account()
.setId(2L)
.setType(AccountType.DEBIT)
.setAmount(0d)
.setRate(1.05 / 100)
.setActive(false)
);
client.addAccount(
new Account()
.setType(AccountType.DEBIT)
.setId(3L)
.setAmount(250d)
.setRate(1.05 / 100)
.setActive(true)
);
new Account()
.setType(AccountType.DEBIT)
.setId(3L)
.setAmount(250d)
.setRate(1.05 / 100)
.setActive(true)
);
entityManager.persist(client);
//end::pc-filter-persistence-example[]
});
}
@After
public void tearDown() {
doInJPA(this::entityManagerFactory, entityManager -> {
entityManager.createQuery( "delete from Account" ).executeUpdate();
entityManager.createQuery( "delete from Client" ).executeUpdate();
});
}
@Test
public void testLifecycle() {
doInJPA(this::entityManagerFactory, entityManager -> {
log.infof("Activate filter [%s]", "activeAccount");
@ -177,6 +197,33 @@ public class FilterTest extends BaseEntityManagerFunctionalTestCase {
//end::pc-filter-entity-query-example[]
});
doInJPA(this::entityManagerFactory, entityManager -> {
//tag::pc-no-filter-collection-query-example[]
Client client = entityManager.find(Client.class, 1L);
assertEquals(3, client.getAccounts().size());
//end::pc-no-filter-collection-query-example[]
});
doInJPA(this::entityManagerFactory, entityManager -> {
log.infof("Activate filter [%s]", "activeAccount");
//tag::pc-filter-collection-query-example[]
entityManager
.unwrap(Session.class)
.enableFilter("activeAccount")
.setParameter("active", true);
Client client = entityManager.find(Client.class, 1L);
assertEquals(2, client.getAccounts().size());
//end::pc-filter-collection-query-example[]
});
}
@Test
@JiraKey("HHH-16830")
public void testApplyToLoadByKey() {
doInJPA(this::entityManagerFactory, entityManager -> {
log.infof("Activate filter [%s]", "minimumAmount");
//tag::pc-filter-entity-example[]
@ -220,28 +267,44 @@ public class FilterTest extends BaseEntityManagerFunctionalTestCase {
assertEquals(1, accounts.size());
//end::pc-filter-entity-query-example[]
});
}
@Test
@JiraKey("HHH-16830")
public void testApplyToLoadByKeyAssociationFiltering() {
doInJPA(this::entityManagerFactory, entityManager -> {
//tag::pc-no-filter-collection-query-example[]
Client client = entityManager.find(Client.class, 1L);
assertEquals(3, client.getAccounts().size());
//end::pc-no-filter-collection-query-example[]
Account account = entityManager.find(Account.class, 2L);
assertNotNull( account.getParentAccount() );
});
doInJPA(this::entityManagerFactory, entityManager -> {
log.infof("Activate filter [%s]", "activeAccount");
entityManager.unwrap(Session.class)
.enableFilter("accountType")
.setParameter("type", "DEBIT");
//tag::pc-filter-collection-query-example[]
entityManager
.unwrap(Session.class)
.enableFilter("activeAccount")
.setParameter("active", true);
FetchNotFoundException exception = assertThrows(
FetchNotFoundException.class,
() -> entityManager.find( Account.class, 2L )
);
// Account with id 1 does not exist
assertTrue( exception.getMessage().contains( "`1`" ) );
});
doInJPA(this::entityManagerFactory, entityManager -> {
entityManager.unwrap(Session.class)
.enableFilter("accountType")
.setParameter("type", "DEBIT");
EntityGraph<Account> entityGraph = entityManager.createEntityGraph( Account.class );
entityGraph.addAttributeNodes( "parentAccount" );
Client client = entityManager.find(Client.class, 1L);
assertEquals(2, client.getAccounts().size());
//end::pc-filter-collection-query-example[]
FetchNotFoundException exception = assertThrows(
FetchNotFoundException.class,
() -> entityManager.find(
Account.class,
2L,
Map.of( AvailableHints.HINT_SPEC_LOAD_GRAPH, entityGraph )
)
);
// Account with id 1 does not exist
assertTrue( exception.getMessage().contains( "`1`" ) );
});
}
@ -333,12 +396,24 @@ public class FilterTest extends BaseEntityManagerFunctionalTestCase {
name="amount",
type=Double.class
),
applyToLoadById = true
applyToLoadByKey = true
)
@Filter(
name="minimumAmount",
condition="amount > :amount"
)
@FilterDef(
name="accountType",
parameters = @ParamDef(
name="type",
type=String.class
),
applyToLoadByKey = true
)
@Filter(
name="accountType",
condition="account_type = :type"
)
public static class Account {
@ -361,6 +436,10 @@ public class FilterTest extends BaseEntityManagerFunctionalTestCase {
//Getters and setters omitted for brevity
//end::pc-filter-Account-example[]
@ManyToOne(fetch = FetchType.LAZY)
private Account parentAccount;
public Long getId() {
return id;
}
@ -415,6 +494,14 @@ public class FilterTest extends BaseEntityManagerFunctionalTestCase {
return this;
}
public Account getParentAccount() {
return parentAccount;
}
public Account setParentAccount(Account parentAccount) {
this.parentAccount = parentAccount;
return this;
}
//tag::pc-filter-Account-example[]
}
//end::pc-filter-Account-example[]