Custom Filters
Provide Abstract Mapped Superclass
First, provide MappedSuperclass
for all entities that need multi-tenancy management to inherit:
- Java
- Kotlin
@MappedSuperclass
public interface TenantAware {
String tenant();
}
@MappedSuperclass
interface TenantAware {
val tenant: String
}
Any entity that needs multi-tenancy support can inherit TenantAware
, such as Book
:
- Java
- Kotlin
@Entity
public interface Book extends TenantAware {
...Other code omitted...
}
@Entity
interface Book : TenantAware {
...Other code omitted...
}
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:
- Java
- Kotlin
@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));
}
}
}
@Component
class TenantFilter(
private val tenantProvider: TenantProvider
) : KFilter<TenantAware> {
override fun filter(args: KFilterArgs<TenantAware>) {
tenantProvider.tenant?.let {
args.apply {
where(table.tenant.eq(it))
}
}
}
}
There are slight differences in filter definition between Java and Kotlin:
-
In Java, the generic type of
Filter
isTenantAwareProps
, which is part of the code auto-generated by the precompiler for the abstract typeTenantAware
. -
In Kotlin, the generic type of
KFilter
is the abstract typeTenantAware
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.
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:
- Java
- Kotlin
JSqlClient sqlClient = JSqlBuilder
.newBuilder()
.addFilter(new CustomerFilter())
...Other config omitted...
.build();
val sqlClient =
newKSqlClient {
addFilters(new CustomerFilter())
...Other config omitted...
}
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.
- Java
- Kotlin
List<Book> books = sqlClient.getEntities.findAll(Book.class);
val books = sqlClient.entities.findAll(Book::class);
or
- Java
- Kotlin
BookTable book = Tables.BOOK_TABLE;
List<Book> books = sqlClient
.createQuery(book)
.select(book)
.execute();
val books = SqlClient
.createQuery(Book::class) {
select(table)
}
.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:
- Java
- Kotlin
List<Author> authors = sqlClient.getEntities.findAll(
Fetchers.AUTHOR_FETCHER
.allScalarFields()
.books(
Fetchers.BOOK_FETCHER
.allScalarFields()
)
);
val books = sqlClient.entities.findAll(
newFetcher(Author::class).by {
allScalarFields()
books {
allScalarFields()
}
}
);
or
- Java
- Kotlin
AuthorTable author = Tables.AUTHOR_TABLE;
List<Author> authors = sqlClient
.createQuery(author)
.select(
author.fetch(
Fetchers.AUTHOR_FETCHER
.allScalarFields()
.books(
Fetchers.BOOK_FETCHER
.allScalarFields()
)
)
)
.execute();
val authors = SqlClient
.createQuery(Author::class) {
select(
table.fetchBy {
allScalarFields()
books {
allScalarFields()
}
}
)
}
.execute()
This generates two SQL statements:
-
Query aggregate roots:
select
tb_1_.ID, tb_1_.FIRST_NAME, tb_1_.LAST_NAME, tb_1_.GENDER
from AUTHOR as tb_1_ -
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:
- Java
- Kotlin
JSqlClient tmpSqlClient =
sqlClient.filters(it -> {
it
.disableByTypes(TenantFilter.class);
});
val tmpSqlClient =
sqlClient.filters {
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 theCacheableFilter
/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
- Java
- Kotlin
@Component
public class TenantFilter
implements AssociationIntegrityAssuranceFilter<TenantAwareProps> {
...
}@Component
class TenantFilter(
...
) : KAssociationIntegrityAssuranceFilter<TenantAware> {
...
} -
Then, let
BookStore
inheritTenantAware
- Java
- Kotlin
@Entity
public interface Book extends TenantAware {
...
}@Entity
interface Book : TenantAware {
...
} -
Finally, let
Book
also inheritTenantAware
, and define a non-null many-to-one association propertyBook.store
- Java
- Kotlin
@Entity
public interface Book extends TenantAware {
@ManyToOne // Not null
BookStore store();
...
}@Entity
interface Book : TenantAware {
val store: BookStore // NotNull
...
}
The following code analysis:
-
Book
andBookStore
both inheritTenantAware
, that is, both sides of the association are controlled by the filterTenantFilter
-
TenantFilter
implements theAssociationIntegrityAssuranceFilter
/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
andBook
objects belonging to the same tenant are associated, andBookStore
andBook
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.