HHH-16830: apply filters to find() method

This commit is contained in:
Dmitrii Karmanov 2024-05-24 17:20:09 +02:00 committed by Christian Beikov
parent 84f2f3535f
commit e721a37691
17 changed files with 168 additions and 24 deletions

View File

@ -558,9 +558,11 @@ include::{example-dir-pc}/FilterTest.java[tags=pc-filter-resolver-Account-exampl
[IMPORTANT] [IMPORTANT]
==== ====
Filters apply to entity queries, but not to direct fetching. Filters apply to entity queries, but not to direct fetching, unless otherwise configured using the `applyToLoadByKey` flag
on the `@FilterDef`, that should be set to `true` in order to activate the filter with direct fetching.
Therefore, in the following example, the filter is not taken into consideration when fetching an entity from the Persistence Context. Therefore, in the following example, the `activeAccount` filter is not taken into consideration when fetching an entity from the Persistence Context.
On the other hand, the `minimumAmount` filter is taken into consideration, because its `applyToLoadByKey` flag is set to `true`.
[[pc-filter-entity-example]] [[pc-filter-entity-example]]
.Fetching entities mapped with `@Filter` .Fetching entities mapped with `@Filter`
@ -574,7 +576,13 @@ include::{example-dir-pc}/FilterTest.java[tags=pc-filter-entity-example]
include::{extrasdir}/pc-filter-entity-example.sql[] include::{extrasdir}/pc-filter-entity-example.sql[]
---- ----
As you can see from the example above, contrary to an entity query, the filter does not prevent the entity from being loaded. [source, SQL, indent=0]
----
include::{extrasdir}/pc-filter-entity-find-example.sql[]
----
As you can see from the example above, contrary to an entity query, the `activeAccount` filter does not prevent the entity from being loaded,
but the `minimumAmount` filter limits the results to the ones with an amount that is greater than the specified one.
==== ====
Just like with entity queries, collections can be filtered as well, but only if the filter is enabled on the currently running Hibernate `Session`, Just like with entity queries, collections can be filtered as well, but only if the filter is enabled on the currently running Hibernate `Session`,

View File

@ -0,0 +1,14 @@
SELECT
a.id as id1_0_0_,
a.active_status as active2_0_0_,
a.amount as amount3_0_0_,
a.client_id as client_i6_0_0_,
a.rate as rate4_0_0_,
a.account_type as account_5_0_0_,
c.id as id1_1_1_,
c.name as name2_1_1_
FROM
Account a
WHERE
a.id = 1
AND a.amount > 9000

View File

@ -95,4 +95,12 @@ public interface Filter {
* @return The flag value * @return The flag value
*/ */
boolean isAutoEnabled(); boolean isAutoEnabled();
/**
* Get the associated {@link FilterDefinition applyToLoadByKey} of this
* named filter.
*
* @return The flag value
*/
boolean isApplyToLoadByKey();
} }

View File

@ -95,4 +95,13 @@ public @interface FilterDef {
* The flag used to auto-enable the filter on the session. * The flag used to auto-enable the filter on the session.
*/ */
boolean autoEnabled() default false; boolean autoEnabled() default false;
/**
* The flag used to decide if the filter will
* be applied on direct fetches or not.
* <p>
* If the flag is true, the filter will be
* applied on direct fetches, such as findById().
*/
boolean applyToLoadByKey() default false;
} }

View File

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

View File

@ -416,11 +416,11 @@ public final class AnnotationBinder {
//@Entity and @MappedSuperclass on the same class leads to a NPE down the road //@Entity and @MappedSuperclass on the same class leads to a NPE down the road
if ( annotatedClass.isAnnotationPresent( Entity.class ) ) { if ( annotatedClass.isAnnotationPresent( Entity.class ) ) {
throw new AnnotationException( "Type '" + annotatedClass.getName() throw new AnnotationException( "Type '" + annotatedClass.getName()
+ "' is annotated both '@Entity' and '@MappedSuperclass'" ); + "' is annotated both '@Entity' and '@MappedSuperclass'" );
} }
if ( annotatedClass.isAnnotationPresent( Table.class ) ) { if ( annotatedClass.isAnnotationPresent( Table.class ) ) {
throw new AnnotationException( "Mapped superclass '" + annotatedClass.getName() throw new AnnotationException( "Mapped superclass '" + annotatedClass.getName()
+ "' may not specify a '@Table'" ); + "' may not specify a '@Table'" );
} }
if ( annotatedClass.isAnnotationPresent( Inheritance.class ) ) { if ( annotatedClass.isAnnotationPresent( Inheritance.class ) ) {
throw new AnnotationException( "Mapped superclass '" + annotatedClass.getName() throw new AnnotationException( "Mapped superclass '" + annotatedClass.getName()
@ -651,8 +651,8 @@ public final class AnnotationBinder {
for ( FetchOverride fetch : fetchProfile.fetchOverrides() ) { for ( FetchOverride fetch : fetchProfile.fetchOverrides() ) {
if ( fetch.fetch() == FetchType.LAZY && fetch.mode() == FetchMode.JOIN ) { if ( fetch.fetch() == FetchType.LAZY && fetch.mode() == FetchMode.JOIN ) {
throw new AnnotationException( "Fetch profile '" + name throw new AnnotationException( "Fetch profile '" + name
+ "' has a '@FetchOverride' with 'fetch=LAZY' and 'mode=JOIN'" + "' has a '@FetchOverride' with 'fetch=LAZY' and 'mode=JOIN'"
+ " (join fetching is eager by nature)"); + " (join fetching is eager by nature)");
} }
context.getMetadataCollector() context.getMetadataCollector()
.addSecondPass( new FetchOverrideSecondPass( name, fetch, context ) ); .addSecondPass( new FetchOverrideSecondPass( name, fetch, context ) );

View File

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

View File

@ -7,6 +7,7 @@
package org.hibernate.boot.model.source.internal.hbm; package org.hibernate.boot.model.source.internal.hbm;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;

View File

@ -37,6 +37,7 @@ public class FilterDefinition implements Serializable {
private final Map<String, JdbcMapping> explicitParamJaMappings = new HashMap<>(); private final Map<String, JdbcMapping> explicitParamJaMappings = new HashMap<>();
private final Map<String, ManagedBean<? extends Supplier<?>>> parameterResolverMap = new HashMap<>(); private final Map<String, ManagedBean<? extends Supplier<?>>> parameterResolverMap = new HashMap<>();
private final boolean autoEnabled; private final boolean autoEnabled;
private final boolean applyToLoadByKey;
/** /**
* Construct a new FilterDefinition instance. * Construct a new FilterDefinition instance.
@ -44,17 +45,18 @@ public class FilterDefinition implements Serializable {
* @param name The name of the filter for which this configuration is in effect. * @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); this( name, defaultCondition, explicitParamJaMappings, Collections.emptyMap(), false, false);
} }
public FilterDefinition( public FilterDefinition(
String name, String defaultCondition, @Nullable Map<String, JdbcMapping> explicitParamJaMappings, String name, String defaultCondition, @Nullable Map<String, JdbcMapping> explicitParamJaMappings,
Map<String, ManagedBean<? extends Supplier<?>>> parameterResolverMap, boolean autoEnabled) { Map<String, ManagedBean<? extends Supplier<?>>> parameterResolverMap, boolean autoEnabled, boolean applyToLoadByKey) {
this.filterName = name; this.filterName = name;
this.defaultFilterCondition = defaultCondition; this.defaultFilterCondition = defaultCondition;
if ( explicitParamJaMappings != null ) { if ( explicitParamJaMappings != null ) {
this.explicitParamJaMappings.putAll( explicitParamJaMappings ); this.explicitParamJaMappings.putAll( explicitParamJaMappings );
} }
this.applyToLoadByKey = applyToLoadByKey;
if ( parameterResolverMap != null ) { if ( parameterResolverMap != null ) {
this.parameterResolverMap.putAll( parameterResolverMap ); this.parameterResolverMap.putAll( parameterResolverMap );
} }
@ -101,6 +103,16 @@ public class FilterDefinition implements Serializable {
return defaultFilterCondition; return defaultFilterCondition;
} }
/**
* Get a flag that defines if the filter should be applied
* on direct fetches or not.
*
* @return The flag value.
*/
public boolean isApplyToLoadByKey() {
return applyToLoadByKey;
}
/** /**
* Called before binding a JDBC parameter * Called before binding a JDBC parameter
* *

View File

@ -13,6 +13,7 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.hibernate.Filter; import org.hibernate.Filter;
import org.hibernate.Internal; import org.hibernate.Internal;
@ -168,6 +169,20 @@ public class LoadQueryInfluencers implements Serializable {
} }
} }
/**
* Returns a Map of enabled filters that have the applyToLoadByKey
* flag set to true
* @return a Map of enabled filters that have the applyToLoadByKey
* flag set to true
*/
public Map<String, Filter> getEnabledFiltersForFind() {
return getEnabledFilters()
.entrySet()
.stream()
.filter(f -> f.getValue().isApplyToLoadByKey())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
/** /**
* Returns an unmodifiable Set of enabled filter names. * Returns an unmodifiable Set of enabled filter names.
* @return an unmodifiable Set of enabled filter names. * @return an unmodifiable Set of enabled filter names.

View File

@ -32,7 +32,8 @@ public class FilterImpl implements Filter, Serializable {
private final String filterName; private final String filterName;
private final Map<String,Object> parameters = new HashMap<>(); private final Map<String,Object> parameters = new HashMap<>();
private final boolean autoEnabled; private final boolean autoEnabled;
private final boolean applyToLoadByKey;
void afterDeserialize(SessionFactoryImplementor factory) { void afterDeserialize(SessionFactoryImplementor factory) {
definition = factory.getFilterDefinition( filterName ); definition = factory.getFilterDefinition( filterName );
validate(); validate();
@ -47,6 +48,7 @@ public class FilterImpl implements Filter, Serializable {
this.definition = configuration; this.definition = configuration;
filterName = definition.getFilterName(); filterName = definition.getFilterName();
this.autoEnabled = definition.isAutoEnabled(); this.autoEnabled = definition.isAutoEnabled();
this.applyToLoadByKey = definition.isApplyToLoadByKey();
} }
public FilterDefinition getFilterDefinition() { public FilterDefinition getFilterDefinition() {
@ -70,7 +72,18 @@ public class FilterImpl implements Filter, Serializable {
public boolean isAutoEnabled() { public boolean isAutoEnabled() {
return autoEnabled; return autoEnabled;
} }
/**
* Get a flag that defines if the filter should be applied
* on direct fetches or not.
*
* @return The flag value.
*/
public boolean isApplyToLoadByKey() {
return applyToLoadByKey;
}
public Map<String,?> getParameters() { public Map<String,?> getParameters() {
return parameters; return parameters;
} }

View File

@ -7,7 +7,6 @@
package org.hibernate.loader.ast.internal; package org.hibernate.loader.ast.internal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -717,8 +716,8 @@ public class LoaderSelectBuilder {
querySpec::applyPredicate, querySpec::applyPredicate,
tableGroup, tableGroup,
true, true,
// HHH-16179 Session.find should not apply filters // HHH-16830 Session.find should apply filters only if specified on the filter definition
Collections.emptyMap(),//loadQueryInfluencers.getEnabledFilters(), loadQueryInfluencers.getEnabledFiltersForFind(),
null, null,
astCreationState astCreationState
); );

View File

@ -2281,6 +2281,7 @@
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="condition" type="xsd:string" minOccurs="0"/> <xsd:element name="condition" type="xsd:string" minOccurs="0"/>
<xsd:element name="apply-to-load-by-key" type="xsd:boolean" default="false"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/> <xsd:attribute name="name" use="required" type="xsd:string"/>

View File

@ -177,6 +177,50 @@ public class FilterTest extends BaseEntityManagerFunctionalTestCase {
//end::pc-filter-entity-query-example[] //end::pc-filter-entity-query-example[]
}); });
doInJPA(this::entityManagerFactory, entityManager -> {
log.infof("Activate filter [%s]", "minimumAmount");
//tag::pc-filter-entity-example[]
entityManager
.unwrap(Session.class)
.enableFilter("minimumAmount")
.setParameter("amount", 9000d);
Account account = entityManager.find(Account.class, 1L);
assertNull( account );
//end::pc-filter-entity-example[]
});
doInJPA(this::entityManagerFactory, entityManager -> {
log.infof("Activate filter [%s]", "minimumAmount");
//tag::pc-filter-entity-example[]
entityManager
.unwrap(Session.class)
.enableFilter("minimumAmount")
.setParameter("amount", 100d);
Account account = entityManager.find(Account.class, 1L);
assertNotNull( account );
//end::pc-filter-entity-example[]
});
doInJPA(this::entityManagerFactory, entityManager -> {
log.infof("Activate filter [%s]", "minimumAmount");
//tag::pc-filter-entity-query-example[]
entityManager
.unwrap(Session.class)
.enableFilter("minimumAmount")
.setParameter("amount", 500d);
List<Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class)
.getResultList();
assertEquals(1, accounts.size());
//end::pc-filter-entity-query-example[]
});
doInJPA(this::entityManagerFactory, entityManager -> { doInJPA(this::entityManagerFactory, entityManager -> {
//tag::pc-no-filter-collection-query-example[] //tag::pc-no-filter-collection-query-example[]
Client client = entityManager.find(Client.class, 1L); Client client = entityManager.find(Client.class, 1L);
@ -280,9 +324,22 @@ public class FilterTest extends BaseEntityManagerFunctionalTestCase {
) )
) )
@Filter( @Filter(
name="activeAccount", name="activeAccount",
condition="active_status = :active" condition="active_status = :active"
) )
@FilterDef(
name="minimumAmount",
parameters = @ParamDef(
name="amount",
type=Double.class
),
applyToLoadByKey = true
)
@Filter(
name="minimumAmount",
condition="amount > :amount"
)
public static class Account { public static class Account {
@Id @Id

View File

@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.cfg.AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION; import static org.hibernate.cfg.AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION;
import static org.hibernate.internal.util.collections.CollectionHelper.toMap; import static org.hibernate.internal.util.collections.CollectionHelper.toMap;
import static org.hibernate.jpa.HibernateHints.HINT_TENANT_ID; import static org.hibernate.jpa.HibernateHints.HINT_TENANT_ID;
import static org.junit.Assert.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -110,7 +111,8 @@ public class TenantIdTest implements SessionFactoryProducer {
currentTenant = "yours"; currentTenant = "yours";
scope.inTransaction( session -> { scope.inTransaction( session -> {
assertNotNull( session.find(Account.class, acc.id) ); //HHH-16830 Sessions applies tenantId filter on find()
assertNull( session.find(Account.class, acc.id) );
assertEquals( 0, session.createQuery("from Account", Account.class).getResultList().size() ); assertEquals( 0, session.createQuery("from Account", Account.class).getResultList().size() );
session.disableFilter(TenantIdBinder.FILTER_NAME); session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.find(Account.class, acc.id) ); assertNotNull( session.find(Account.class, acc.id) );
@ -247,9 +249,9 @@ public class TenantIdTest implements SessionFactoryProducer {
Record r = em.find( Record.class, record.id ); Record r = em.find( Record.class, record.id );
assertEquals( "mine", r.state.tenantId ); assertEquals( "mine", r.state.tenantId );
// Session seems to not apply tenant-id on #find // HHH-16830 Session applies tenant-id on #find
Record yours = em.find( Record.class, record2.id ); Record yours = em.find( Record.class, record2.id );
assertEquals( "yours", yours.state.tenantId ); assertNull(yours);
em.createQuery( "from Record where id = :id", Record.class ) em.createQuery( "from Record where id = :id", Record.class )

View File

@ -79,7 +79,8 @@ public class TenantLongIdTest implements SessionFactoryProducer {
currentTenant = yours; currentTenant = yours;
scope.inTransaction( session -> { scope.inTransaction( session -> {
assertNotNull( session.find(Account.class, acc.id) ); //HHH-16830 Sessions applies tenantId filter on find()
assertNull( session.find(Account.class, acc.id) );
assertEquals( 0, session.createQuery("from Account").getResultList().size() ); assertEquals( 0, session.createQuery("from Account").getResultList().size() );
session.disableFilter(TenantIdBinder.FILTER_NAME); session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.find(Account.class, acc.id) ); assertNotNull( session.find(Account.class, acc.id) );

View File

@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
import java.util.UUID; import java.util.UUID;
import static org.hibernate.cfg.AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION; import static org.hibernate.cfg.AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION;
import static org.junit.Assert.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -86,7 +87,8 @@ public class TenantUuidTest implements SessionFactoryProducer {
currentTenant = yours; currentTenant = yours;
scope.inTransaction( session -> { scope.inTransaction( session -> {
assertNotNull( session.find(Account.class, acc.id) ); //HHH-16830 Sessions applies tenantId filter on find()
assertNull( session.find(Account.class, acc.id) );
assertEquals( 0, session.createQuery("from Account").getResultList().size() ); assertEquals( 0, session.createQuery("from Account").getResultList().size() );
session.disableFilter(TenantIdBinder.FILTER_NAME); session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.find(Account.class, acc.id) ); assertNotNull( session.find(Account.class, acc.id) );