Skip to main content

Custom Filters

Provide Abstract Mapped Superclass

First, provide MappedSuperclass for all entities that need multi-tenancy management to inherit:

TenantAware.java
@MappedSuperclass  
public interface TenantAware {

String tenant();
}

Any entity that needs multi-tenancy support can inherit TenantAware, such as Book:

Book.java
@Entity
public interface Book extends TenantAware {

...Other code omitted...
}
tip

Certainly, it's possible to apply filters directly to entity types without defining the abstract type, this works fine.

However, it is better to extract the abstract type from entities, so one filter can apply to multiple entity types.

More importantly, MappedSuperclass supports multiple inheritance, i.e. entities can inherit from multiple supertypes. Multi-inheritance combined with global filters brings amazing flexibility.

Define Filter

Assume there is an object of type TenantProvider in the Spring context. Its Java method get() and Kotlin property tenant are used to extract the tenant of the current operator from identity info. Define the filter as follows:

  • In Java, filter need to implement org.babyfish.jimmer.sql.filter.Filter.
  • In Kotlin, filter need to implement org.babyfish.jimmer.sql.kt.filter.KFilter.

If using Spring management, the code is:

@Component 
public class TenantFilter implements Filter<TenantAwareProps> {

private final TenantProvider tenantProvider;

public TenantFilter(TenantProvider tenantProvider) {
this.tenantProvider = tenantProvider;
}

@Override
public void filter(FilterArgs<TenantAwareProps> args) {
String tenant = tenantProvider.get();
if (tenant != null) {
args.where(args.getTable().tenant().eq(tenant));
}
}
}

There are slight differences in filter definition between Java and Kotlin:

  • In Java, the generic type of Filter is TenantAwareProps, which is part of the code auto-generated by the precompiler for the abstract type TenantAware.

  • In Kotlin, the generic type of KFilter is the abstract type TenantAware itself.

TenantFilter filters the abstract type TenantAware. For any entity that inherits the abstract interface TenantAware directly or indirectly, its queries will be handled by this filter, automatically adding a where condition.

Inside TenantFilter, it first extracts the tenant of the current operator from identity info. If the tenant is non-null, use it to filter data, querying only data that matches the specified tenant.

Configure Filter in Spring

In the above, the defined class TenantFilter is decorated with @Component, obviously a Spring-managed object.

info

If using Jimmer's Spring Boot Starter and ensuring the filter is Spring-managed, Jimmer will auto-register it without extra configuration.

Otherwise, must manually register.

Configure Filter Without Spring

In this case, the filter class does not need to be decorated with @Component, attach the filter to the SqlClient object for it to take effect:

JSqlClient sqlClient = JSqlBuilder
.newBuilder()
.addFilter(new CustomerFilter())
...Other config omitted...
.build();

Filter Aggregate Root Objects

Filtering aggregate roots is the simplest use of global filters.

Since Book entity inherits from TenantAware, its queries will be affected by this filter.

List<Book> books = sqlClient.getEntities.findAll(Book.class);

or

BookTable book = Tables.BOOK_TABLE;
List<Book> books = sqlClient
.createQuery(book)
.select(book)
.execute();

The generated SQL:

select
tb_1_.ID,
tb_1_.TENANT,
tb_1_.NAME,
tb_1_.EDITION,
tb_1_.PRICE,
tb_1_.STORE_ID
from BOOK as tb_1_
/* highlight-next-line */
where tb_1_.TENANT = ?

Obviously, the query here is very simple without any query parameters. But the final SQL still filters on tb_1_.TENANT.

Filter Associated Objects

Not only aggregate roots can be filtered, associated objects can be filtered too:

List<Author> authors = sqlClient.getEntities.findAll(
Fetchers.AUTHOR_FETCHER
.allScalarFields()
.books(
Fetchers.BOOK_FETCHER
.allScalarFields()
)
);

or

AuthorTable author = Tables.AUTHOR_TABLE;
List<Author> authors = sqlClient
.createQuery(author)
.select(
author.fetch(
Fetchers.AUTHOR_FETCHER
.allScalarFields()
.books(
Fetchers.BOOK_FETCHER
.allScalarFields()
)
)
)
.execute();

This generates two SQL statements:

  1. Query aggregate roots:

    select
    tb_1_.ID, tb_1_.FIRST_NAME, tb_1_.LAST_NAME, tb_1_.GENDER
    from AUTHOR as tb_1_
  2. Query associated objects:

    select
    tb_2_.AUTHOR_ID,
    tb_1_.ID,
    tb_1_.TENANT,
    tb_1_.NAME,
    tb_1_.EDITION,
    tb_1_.PRICE
    from BOOK as tb_1_
    inner join BOOK_AUTHOR_MAPPING as tb_2_
    on tb_1_.ID = tb_2_.BOOK_ID
    where
    tb_2_.AUTHOR_ID in (?, ?, ?, ?, ?)
    and
    /* highlight-next-line */
    tb_1_.TENANT = ?

Disable Filters

Calling sqlClient.filters creates a new temporary SqlClient without affecting the current sqlClient, which can be used to disable filters:

JSqlClient tmpSqlClient = 
sqlClient.filters(it -> {
it
.disableByTypes(TenantFilter.class);
});

Here we get a temporary tmpSqlClient. Queries created from it will ignore the filter we demonstrated above.

More filter interfaces

In addition to the basic Filter/KFilter interface, filter classes can also implement more interfaces, including:

  • CacheableFilter/KCacheableFilter

  • AssociationIntegrityAssuranceFilter/AssociationIntegrityAssuranceFilter

  • ShardingFilter/KShardingFilter

CacheableFilter

Global filters show different data to different users, and for any associated property that takes the filtered type as the target type, different users will naturally see different associations.

This will lead to:

  • These association properties cannot apply simple

  • Dependent calculated properties also cannot enable

To solve this problem, Jimmer supports

, but the cost is that related global filters must implement the CacheableFilter/KCacheableFilter interface. This part of the content is explained in detail in the Cache/Multi-view Cache/User Defined Cacheable Filters article later, and will not be repeated here.

AssociationIntegrityAssuranceFilter

The full name of this interface is:

  • Java: org.babyfish.jimmer.sql.filter.AssociationIntegrityAssuranceFilter<P>
  • Kotlin: org.babyfish.jimmer.sql.kt.filter.KAssociationIntegrityAssuranceFilter<E>

Compared to the basic Filter/KFilter interface, this interface does not add any new methods, it is only used as a type identifier.

For one-to-one/many-to-one association properties based on foreign keys, even if the field is set to a non-null type and has a real foreign key constraint, the associated object queried may still be null due to the filter.

Therefore, Jimmer stipulates that if the associated entity of a one-to-one/many-to-one association property is affected by the filter, the association property must be declared as nullable.

The AssociationIntegrityAssuranceFilter/KAssociationIntegrityAssuranceFilter allows the user to make a commitment to the characteristics of the database data to break this restriction.

  • First, let the filter class implement this interface

    @Component
    public class TenantFilter
    implements AssociationIntegrityAssuranceFilter<TenantAwareProps> {
    ...
    }
  • Then, let BookStore inherit TenantAware

    @Entity
    public interface Book extends TenantAware {
    ...
    }
  • Finally, let Book also inherit TenantAware, and define a non-null many-to-one association property Book.store

    @Entity
    public interface Book extends TenantAware {

    @ManyToOne // Not null
    BookStore store();
    ...
    }

The following code analysis:

  • Book and BookStore both inherit TenantAware, that is, both sides of the association are controlled by the filter TenantFilter

  • TenantFilter implements the AssociationIntegrityAssuranceFilter/KAssociationIntegrityAssuranceFilter interface.

    This interface is the user's commitment to the characteristics of the database data, committing that only objects that follow the same filtering rules will have associations. For this example, it means that only BookStore and Book objects belonging to the same tenant are associated, and BookStore and Book objects belonging to different tenants will never be associated.

Only under the user's commitment can the many-to-one association Book.store be set to non-null.

In summary, if the associated type of a one-to-one/many-to-one association property is affected by the filter, in order to set this property to non-null, the following two conditions must be met at the same time:

  • All global filters applied to the associated entity implement the AssociationIntegrityAssuranceFilter/KAssociationIntegrityAssuranceFilter interface.

  • All global filters applied to the associated entity are also applied to the current entity.

Sharding Filters

The full name of this interface is:

  • Java: org.babyfish.jimmer.sql.filter.ShardingFilter<P>
  • Kotlin: org.babyfish.jimmer.sql.kt.filter.KShardingFilter<E>

Compared to the basic Filter/KFilter interface, this interface does not add any new methods, it is only used as a type identifier.

Jimmer provides simple APIs to query entity/entities by id/ids.

By default, these APIs are special - they ignore global filters. Queries by id ignoring filters are correct since ids uniquely identify objects.

However, if sharding-jdbc is used at the JDBC level, and the field used as filter condition is the sharding field in sharding-jdbc, querying by id alone would cause sharding-jdbc to query multiple shards, which is disastrous.

To solve this, make the filter implement ShardingFilter (Java) or KShardingFilter (Kotlin). These interfaces have no behaviors, just for type marking.

Once a filter inherits ShardingFilter or KShardingFilter, these simple APIs will no longer ignore the filter. This ensures the final SQL contains the sharding field required by sharding-jdbc, querying only one shard instead of all shards.

Multi-view Cache

When user-defined filters and cache are used together, a problem called "multi-view cache" is involved.

Since we haven't introduced cache yet, we'll just make a brief note here. Please refer to Cache/Multi-view Cache for details.