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]
====
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]]
.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[]
----
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`,

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
*/
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.
*/
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 ),
Collections.emptyMap(),
false
true,
true
)
);
}

View File

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

View File

@ -7,6 +7,7 @@
package org.hibernate.boot.model.source.internal.hbm;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
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, ManagedBean<? extends Supplier<?>>> parameterResolverMap = new HashMap<>();
private final boolean autoEnabled;
private final boolean applyToLoadByKey;
/**
* 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.
*/
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(
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.defaultFilterCondition = defaultCondition;
if ( explicitParamJaMappings != null ) {
this.explicitParamJaMappings.putAll( explicitParamJaMappings );
}
this.applyToLoadByKey = applyToLoadByKey;
if ( parameterResolverMap != null ) {
this.parameterResolverMap.putAll( parameterResolverMap );
}
@ -101,6 +103,16 @@ public class FilterDefinition implements Serializable {
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
*

View File

@ -13,6 +13,7 @@ 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;
@ -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.
* @return an unmodifiable Set of enabled filter names.

View File

@ -32,6 +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 applyToLoadByKey;
void afterDeserialize(SessionFactoryImplementor factory) {
definition = factory.getFilterDefinition( filterName );
@ -47,6 +48,7 @@ public class FilterImpl implements Filter, Serializable {
this.definition = configuration;
filterName = definition.getFilterName();
this.autoEnabled = definition.isAutoEnabled();
this.applyToLoadByKey = definition.isApplyToLoadByKey();
}
public FilterDefinition getFilterDefinition() {
@ -71,6 +73,17 @@ public class FilterImpl implements Filter, Serializable {
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() {
return parameters;
}

View File

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

View File

@ -2281,6 +2281,7 @@
</xsd:complexType>
</xsd:element>
<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: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[]
});
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 -> {
//tag::pc-no-filter-collection-query-example[]
Client client = entityManager.find(Client.class, 1L);
@ -283,6 +327,19 @@ public class FilterTest extends BaseEntityManagerFunctionalTestCase {
name="activeAccount",
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 {
@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.internal.util.collections.CollectionHelper.toMap;
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.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -110,7 +111,8 @@ public class TenantIdTest implements SessionFactoryProducer {
currentTenant = "yours";
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() );
session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.find(Account.class, acc.id) );
@ -247,9 +249,9 @@ public class TenantIdTest implements SessionFactoryProducer {
Record r = em.find( Record.class, record.id );
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 );
assertEquals( "yours", yours.state.tenantId );
assertNull(yours);
em.createQuery( "from Record where id = :id", Record.class )

View File

@ -79,7 +79,8 @@ public class TenantLongIdTest implements SessionFactoryProducer {
currentTenant = yours;
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() );
session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.find(Account.class, acc.id) );

View File

@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
import java.util.UUID;
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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -86,7 +87,8 @@ public class TenantUuidTest implements SessionFactoryProducer {
currentTenant = yours;
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() );
session.disableFilter(TenantIdBinder.FILTER_NAME);
assertNotNull( session.find(Account.class, acc.id) );