User Defined Cacheable Filters
Cache-friendly Filters
Basic Concepts
In the User defined Filters documentation, we introduced that custom global filters need to implement the Filter/KFilter
interface.
However, ordinary filters defined using this interface are not cache-friendly.
Taking the Book
entity as an example, if a cache-unfriendly global filter is set for it, it will cause all the following filter-sensitive properties
-
Association properties targeting
Book
, such asBookStore.books
,Author.books
-
Calculated properties relying on the above association properties, such as
BookStore.avgPrice
,BookStore.newestBooks
to become uncacheable.
Jimmer uses the CacheableFilter/KCacheableFilter
interface to define cache-friendly filters:
- Java
- Kotlin
package org.babyfish.jimmer.sql.filter;
import org.babyfish.jimmer.sql.ast.table.Props;
import org.babyfish.jimmer.sql.event.EntityEvent;
import java.util.SortedMap;
public interface CacheableFilter<P extends Props> extends Filter<P> {
SortedMap<String, Object> getParameters();
boolean isAffectedBy(EntityEvent<?> e);
}
package org.babyfish.jimmer.sql.kt.filter
import org.babyfish.jimmer.sql.event.EntityEvent
import java.util.*
interface KCacheableFilter<E: Any> : KFilter<E> {
fun getParameters(): SortedMap<String, Any>?
fun isAffectedBy(e: EntityEvent<*>): Boolean
}
This interface inherits from Filter/KFilter
and adds two new methods:
-
getParameters
: The sub key fragment contributed by this filter for multi-view cache. -
isAffectedBy
: Accepts an event that the filtered entity is modified, and judges whether the filtering fields depended on by the current filter are changed.
An entity type allows being processed by multiple global filters:
-
If any one of them is cache-unfriendly, it will cause all filter-sensitive properties to become uncacheable.
Therefore, these global filters must either all be cache-unfriendly
Filter/KFilter
, or all be cache-friendlyCacheableFilter/KCacheableFilter
. Mixing them together makes no sense.If such meaningless mixing occurs accidentally, Jimmer will tell why cache is abandoned.
-
When all global filters are cache-friendly, the data returned by the
getParameters()
method of allCacheableFilter/KCacheableFilter
objects is merged together as theSubKey
of the multi-view cache.For example, if an entity is processed by two global filters at the same time. One is the filter implied by logical delete, denoted as
a
; the other one is a user-defined filter, denoted asb
.Assume
-
a
'sgetParameters()
returns{"logicalDeleted":false}
-
b
'sgetParameters()
returns{"tenant":"a"}
Then the final
SubKey
in multi-view cache will be{"logicalDeleted":false,"tenant":"a"}
-
Define Cache-friendly Filters
In the User-defined Filters documentation, we defined a super type TenantAware
for entities. Let's review its code again:
- Java
- Kotlin
@MappedSuperclass
public interface TenantAware {
String tenant();
}
@MappedSuperclass
interface TenantAware {
val tenant: String
}
Any entity type that needs to support multiple tenants can inherit TenantAware
, such as Book
:
- Java
- Kotlin
@Entity
public interface Book extends TenantAware {
...code omitted...
}
@Entity
interface Book : TenantAware {
...code omitted...
}
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 from the identity information of the current operator. Define the following filter:
- Java
- Kotlin
@Component
public class TenantFilter implements CacheableFilter<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));
}
}
@Override
public SortedMap<String, Object> getParameters() {
String tenant = tenantProvider.get();
if (tenant == null) {
return null;
}
SortedMap<String, Object> map = new TreeMap<>();
map.put("tenant", tenant);
return map;
}
@Override
public boolean isAffectedBy(EntityEvent<?> e) {
return e.isChanged(TenantAwareProps.TENANT)
}
}
@Component
class TenantFilter(
private val tenantProvider: TenantProvider
) : KCacheableFilter<TenantAware> {
override fun filter(args: KFilterArgs<TenantAware>) {
tenantProvider.tenant?.let {
args.apply {
where(table.tenant.eq(it))
}
}
}
override fun getParameters(): SortedMap<String, Any>? =
tenantProvider.tenant?.let {
sortedMapOf("tenant" to it)
}
override fun isAffectedBy(e: EntityEvent<*>): Boolean =
e.isChanged(TenantAware::tenant)
}
Enable Multi-view Cache
Simple Approach
- Java
- Kotlin
@Bean
public CacheFactory cacheFactory(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper
) {
return new CacheFactory() {
@Override
public Cache<?, ?> createObjectCache(@NotNull ImmutableType type) {
...code omitted...
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
...code omitted...
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(
prop == BookStoreProps.BOOKS.unwrap() ||
prop == AuthorProps.BOOKS.unwrap()
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
);
}
@Override
public Cache<?, ?> createResolverCache(ImmutableProp prop) {
return createPropCache(
prop == BookStoreProps.AVG_PRICE.unwrap() ||
prop == BookStoreProps.NEWEST_BOOKS.unwrap()
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
);
}
private <K, V> Cache<K, V> createPropCache(
boolean isMultiviewCache,
ImmutableProp prop,
Duration caffeineDuration,
Duration redisDuration
) {
if (isMultiView) {
return new ChainCacheBuilder<K, V>()
.add(
CaffeineHashBinder
.forProp(prop)
.maximumSize(128)
.duration(caffeineDuration)
.build()
)
.add(
RedisHashBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build();
}
return new ChainCacheBuilder<>()
.add(
CaffeineValueBinder
.forObject(type)
.maximumSize(512)
.duration(caffeineDuration)
.build()
)
.add(
RedisValueBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build();
}
};
}
@Bean
fun cacheFactory(
connectionFactory: RedisConnectionFactory,
objectMapper: ObjectMapper
): KCacheFactory {
return object: KCacheFactory {
override fun createObjectCache(type: ImmutableType): Cache<*, *>? =
...code omitted...
override fun createAssociatedIdCache(prop: ImmutableProp): Cache<*, *>? =
...code omitted...
override fun createAssociatedIdListCache(prop: ImmutableProp): Cache<*, List<*>>? =
createPropCache(
prop === BookStore::books.toImmutableProp() ||
prop === Author::books.toImmutableProp(),
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
)
override fun createResolverCache(prop: ImmutableProp): Cache<*, *> =
createPropCache(
prop === BookStore::avgPrice.toImmutableProp() ||
prop === BookStore::newestBooks.toImmutableProp(),
prop,
Duration.ofMinutes(1),
Duration.ofHours(1)
)
private fun <K, V> createPropCache(
isMultiView: Boolean,
prop: ImmutableProp,
caffeineDuration: Duration,
redisDuration: Duration
): Cache<K, V> {
if (isMultiView) {
return ChainCacheBuilder<K, V>()
.add(
CaffeineHashBinder
.forProp(prop)
.maximumSize(128)
.duration(caffeineDuration)
.build()
)
.add(
RedisHashBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build();
}
ChainCacheBuilder<Any, Any>()
.add(
CaffeineValueBinder
.forProp(prop)
.maximumSize(512)
.duration(caffeineDuration)
.build()
)
.add(
RedisValueBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build()
}
}
}
The RedisHashBinder
class in the above code is a very important implementation that utilizes Redis' support for multi-view cache. The underlying storage structure corresponds to Redis Hashes, i.e. nested Hash structures.
Cache Style | Is Multi-view | Abstract API | Built-in Impl |
---|---|---|---|
Cache with self-loading (usually first-level cache technologies like Guava, Caffeine) | Single-view | LoadingBinder<K, V> | CaffeineValueBinder<K, V> |
Multi-view | LoadingBinder<K, V>.Parameterized<K, V> | None | |
Cache without self-loading (usually second-level cache technologies like Redis) | Single-view | SimpleBinder<K, V> | RedisValueBinder<K, V> |
Multi-view | SimpleBinder.Parameterized<K, V> | RedisHashBinder<K, V> | |
RedisHashBinder<K, V> |
Better Approach
In the above code, the createAssociatedIdListCache
method judges the prop
parameter to decide whether to build multi-view cache or single-view cache. However,
For association properties, whether multi-view cache needs to be built can be determined solely by whether the target entity is filtered. Jimmer provides better support for this.
Developers only need to replace the super interface CacheFactory
/KCacheFactory
with the super class AbstractCacheFactory
/AbstractKCacheFactory
to inherit a member called getFilterState
/filterState
which can help us determine whether to build multi-view cache.
- Java
- Kotlin
@Bean
public CacheFactory cacheFactory(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper
) {
return new AbstractCacheFactory() {
@Override
public Cache<?, ?> createObjectCache(@NotNull ImmutableType type) {
...code omitted...
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(
getFilterState().isAffectedBy(prop.getTargetType()),
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
);
}
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(
getFilterState().isAffectedBy(prop.getTargetType()),
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
);
}
@Override
public Cache<?, ?> createResolverCache(ImmutableProp prop) {
return createPropCache(
prop == BookStoreProps.AVG_PRICE.unwrap() ||
prop == BookStoreProps.NEWEST_BOOKS.unwrap()
prop,
Duration.ofSeconds(1),
Duration.ofHours(24)
);
}
private <K, V> Cache<K, V> createPropCache(
boolean isMultiviewCache,
ImmutableProp prop,
Duration duration
) {
...code omitted...
}
};
}
@Bean
fun cacheFactory(
connectionFactory: RedisConnectionFactory,
objectMapper: ObjectMapper
): KCacheFactory {
return object: AbstractKCacheFactory() {
override fun createObjectCache(type: ImmutableType): Cache<*, *>? =
...code omitted...
override fun createAssociatedIdCache(prop: ImmutableProp): Cache<*, *>? =
createPropCache(
filterState.isAffectedBy(prop.targetType),
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
)
override fun createAssociatedIdListCache(prop: ImmutableProp): Cache<*, List<*>>? =
createPropCache(
filterState.isAffectedBy(prop.targetType),
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
)
override fun createResolverCache(prop: ImmutableProp): Cache<*, *> =
createPropCache(
prop === BookStore::avgPrice.toImmutableProp() ||
prop === BookStore::newestBooks.toImmutableProp(),
prop,
Duration.ofHours(1),
Duration.ofHours(24)
)
private fun <K, V> createPropCache(
isMultiView: Boolean,
prop: ImmutableProp,
caffeineDuration: Duration,
redisDuration: Duration
): Cache<K, V> {
...code omitted...
}
}
}
Unfortunately, this method can only simplify the construction of association caches, i.e. simplify the createAssociatedIdCache
and createAssociatedIdListCache
methods.
For calculated properties, since the framework is unaware of the internal logic used by user-defined calculated properties, it cannot simplify them. Users need to decide whether to build multi-view caches based on their own business characteristics.
SubKey of Calculated Properties
We have defined the getParameters
method in TenantFilter
. All affected association properties will automatically specify SubKey
for their association caches.
However, unfortunately, due to the introduction of user-defined calculation rules that the framework cannot understand, developers must manually specify SubKey
for the TransientResolver
implementation of calculated properties.
- Java
- Kotlin
@Component
public class BookStoreAvgPriceResolver implements TransientResolver<Long, BigDecimal> {
private final JSqlClient sqlClient;
@Override
public Ref<SortedMap<String, Object>> getParameterMapRef() {
return sqlClient
.getFilters()
.getTargetParameterMapRef(BookStoreProps.BOOKS);
}
...code omitted...
}
@Component
class BookStoreAvgPriceResolver(
private val sqlClient: KSqlClient
) : KTransientResolver<Long, BigDecimal> {
override fun getParameterMapRef(): Ref<SortedMap<String, Any>?>? {
return sqlClient
.filters
.getTargetParameterMapRef(BookStore::books)
}
...code omitted...
}
Obviously, the calculated property BookStore.avgPrice
is actually determined by the association property BookStore.books
and changes with it.
Therefore, whatever SubKey
the association property BookStore.books
specifies for the multi-view cache system in the current invocation context, the calculated property BookStore.avgPrice
should specify the same one.
BookStore.avgPrice
is also affected by Book.price
. It changes when Book.price
changes.
However, Book.price
is a non-associative property of the object, so it must be irrelevant to the multi-view cache system. The getParameterMapRef
method does not need to consider it here.
Usage
Now that we have made the association property BookStore.books
and the calculated property BookStore.avgPrice
support multi-view caching, let's use object fetchers to query them:
- Java
- Kotlin
BookStoreTable table = Tables.BOOK_STORE_TABLE;
List<BookStore> stores = sqlClient
.createQuery(table)
.select(
table.fetch(
Fetchers.BOOK_STORE_FETCHER
.allScalarFields()
.books( ❶
Fetchers.BOOK_FETCHER
.allScalarFields()
)
.avgPrice() ❷
)
)
.execute();
System.out.println(stores);
val stores = sqlClient
.createQuery(BookStore::class) {
select(
table.fetchBy {
allScalarFields()
books { ❶
allScalarFields()
}
avgPrice() ❷
}
)
}
.execute()
println(stores)
Execute with one tenant identity
Assume the current tenant name is a
, the execution process is as follows:
-
First step: Query aggregate root
First query the aggregate root object by executing the following SQL:
select
tb_1_.ID,
tb_1_.NAME,
tb_1_.WEBSITE
from BOOK_STORE tb_1_Here the query in the code is implemented to get some BookStore objects. Such objects directly queried by the user are called aggregate root objects.
-
Second step: Query
BookStore.books
in ❶ through association cacheThe above code will get a series of aggregate root objects. If using the official example data, it will get two aggregate root objects with
ID
of 1 and 2 forBOOK_STORE
.Jimmer first looks up the data from Redis. The keys looked up are
BookStore.books-1
andBookStore.books-2
.Assume the data for these keys cannot be found in Redis:
127.0.0.1:6379> keys BookStore.books-*
(empty array)So the following SQL is executed to complete the associated property
BookStore.books
:SQL: select,
tb_1_.STORE_ID,
tb_1_.ID
from BOOK tb_1_
where
tb_1_.STORE_ID in (
? /* 1 */, ? /* 2 */
)
and
// highlight-next-line
tb_1_.TENANT = ? /* a */
order by
tb_1_.NAME asc,
tb_1_.EDITION descinfoThe filter condition
tb_1_.TENANT = 'a'
comes from the user filterTenantFilter
.Jimmer will put the query result into Redis. So we can view the data from Redis:
127.0.0.1:6379> keys BookStore.books-*
1) "BookStore.books-2"
2) "BookStore.books-1"
127.0.0.1:6379> hgetall BookStore.books-1
1) "{\"tenant\":\"a\"}"
2) "[5,3,1,9,7]"
127.0.0.1:6379> hgetall BookStore.books-2
1) "{\"tenant\":\"a\"}"
2) "[11]"infoJimmer uses Redis Hash for multi-perspective cache. So
hgetall
instead ofget
is needed.Redis Hash is a nested KV structure:
-
The outer Redis Key, e.g.
BookStore.books-1
andBookStore.books-2
, is no different from single-perspective cache. -
The inner Hash Key, also called SubKey in Jimmer, is provided by global filters.
Here,
{"tenant":"a"}
is provided byTenantProvider
, indicating the cached value is not the id set of all associated objects, but the id set of associated objects visible to tenanta
.
tipUndoubtedly, executing the above Java/Kotlin code again with the same tenant identity before data expiration in Redis, it will directly return associated data from Redis without generating related SQL.
-
-
Third step: Convert id set to associated objects
In the previous step we got the id set of associated objects corresponding to associated property
BookStore.books
, representing the associated objects visible to tenanta
.Now we can use the object cache of
Book
to convert theBook
id set intoBook
object set.This step is very simple without further discussion.
-
Fourth step: Query
BookStore.avgPrice
in ❷ through computation cacheThe above code will get a series of aggregate root objects. If using the official example data, it will get two aggregate root objects with
ID
of 1 and 2 forBOOK_STORE
.Jimmer first looks up the data from Redis. The keys looked up are
BookStore.avgPrice-1
andBookStore.avgPrice-2
.Assume the data for these keys cannot be found in Redis:
127.0.0.1:6379> keys BookStore.avgPrice-*
(empty array)So the following SQL is executed to compute the calculation property:
select
tb_1_.ID,
avg(tb_2_.PRICE)
from BOOK_STORE tb_1_
left join BOOK tb_2_
on tb_1_.ID = tb_2_.STORE_ID
where
tb_1_.ID in (
? /* 1 */, ? /* 2 */
)
and
// highlight-next-line
tb_1_.TENANT = ? /* a */
group by
tb_1_.IDinfoThe filter condition
tb_1_.TENANT = 'a'
comes from the user filterTenantFilter
.Jimmer will put the query result into Redis. So we can view the data from Redis:
127.0.0.1:6379> keys BookStore.avgPrice-*
1) "BookStore.avgPrice-2"
2) "BookStore.avgPrice-1"
127.0.0.1:6379> hgetall BookStore.avgPrice-1
1) "{\"tenant\":\"a\"}"
2) "53.1"
127.0.0.1:6379> hgetall BookStore.avgPrice-2
1) "{\"tenant\":\"a\"}"
2) "81"infoJimmer uses Redis Hash for multi-perspective cache. So
hgetall
instead ofget
is needed.Redis Hash is a nested KV structure:
-
The outer Redis Key, e.g.
BookStore.avgPrice-1
andBookStore.avgPrice-2
, is no different from single-perspective cache. -
The inner Hash Key, also called SubKey in Jimmer, is provided by global filters.
Here,
{"tenant": "a"}
is provided byTenantProvider
, indicating the cached value is not the average price of all associated objects, but the average price of associated objects visible to tenanta
.
tipUndoubtedly, executing the above Java/Kotlin code again with the same tenant identity before data expiration in Redis, it will directly return associated data from Redis without generating related SQL.
-
Finally, Jimmer concatenates the results of the 4 steps and returns them to the user.
[
{
"id":2,
"name":"MANNING",
"website":null,
"books":[
{
"id":11,
"name":"GraphQL in Action",
"edition":2,
"price":81
}
],
"avgPrice":81
},
{
"id":1,
"name":"O'REILLY",
"website":null,
"books":[
{
"id":5,
"name":"Effective TypeScript",
"edition":2,
"price":69
},
{
"id":3,
...omitted...
},
{
"id":1,
...omitted...
},
{
"id":9,
...omitted...
},
{
"id":7,
...omitted...
}
],
"avgPrice":53.1
}
]
Execute repeatedly with multiple tenant identities
The query execution process with tenant a
has been discussed. Similarly, we can execute multiple times using different tenant identities to leave cache data in Redis from the following perspectives:
tenant
= nulltenant
= "a"tenant
= "b"
For the official example, TenantProvider
is implemented based on HTTP request header and has
Among them, tenant = null
corresponds to the unauthorized/logout state in swagger UI.
Open redis-cli, we can verify the data in Redis:
127.0.0.1:6379> keys BookStore.books-*
1) "BookStore.books-2"
2) "BookStore.books-1"
127.0.0.1:6379> hgetall BookStore.books-1
1) "{\"tenant\":\"b\"}"
2) "[6,4,2,8]"
3) "{\"tenant\":\"a\"}"
4) "[5,3,1,9,7]"
5) "{}"
6) "[6,5,4,3,2,1,9,8,7]"
127.0.0.1:6379> hgetall BookStore.books-2
1) "{\"tenant\":\"b\"}"
2) "[12,10]"
3) "{\"tenant\":\"a\"}"
4) "[11]"
5) "{}"
6) "[12,11,10]"
127.0.0.1:6379> keys BookStore.avgPrice-*
1) "BookStore.avgPrice-2"
2) "BookStore.avgPrice-1"
127.0.0.1:6379> hgetall BookStore.avgPrice-1
1) "{\"tenant\":\"b\"}"
2) "65.25"
3) "{\"tenant\":\"a\"}"
4) "53.1"
5) "{}"
6) "58.500000"
127.0.0.1:6379> hgetall BookStore.avgPrice-2
1) "{\"tenant\":\"b\"}"
2) "80"
3) "{\"tenant\":\"a\"}"
4) "81"
5) "{}"
6) "80.333333"
Readers can take a close look at these redis-cli
commands and easily find that the data of sub key {"tenant":"a"}
merged with the data of sub key {"tenant":"b"}
is exactly the data of SubKey {}
.
The data returned to the user in the 3 calls is:
- tenant=null
- tenant=a
- tenant=b
[
{
"id":2,
"name":"MANNING",
"website":null,
"books":[
{
"id":12,
"name":"GraphQL in Action",
"edition":3,
"price":80,
},
{
"id":11,
...omitted...
},
{
"id":10,
...omitted...
}
],
"avgPrice":80.333333
},
{
"id":1,
"name":"O'REILLY",
"website":null,
"books":[
{
"id":6,
"name":"Effective TypeScript",
"edition":3,
"price":88
},
{
"id":5,
...omitted...
},
{
"id":4,
...omitted...
},
{
"id":3,
"name":"Learning GraphQL",
"edition":3,
"price":51
},
{
"id":2,
...omitted...
},
{
"id":1,
...omitted...
},
{
"id":9,
"name":"Programming TypeScript",
"edition":3,
"price":48
},
{
"id":8,
...omitted...
},
{
"id":7,
...omitted...
}
],
"avgPrice":58.5
}
]
[
{
"id":2,
"name":"MANNING",
"website":null,
"books":[
{
"id":11,
"name":"GraphQL in Action",
"edition":2,
"price":81
}
],
"avgPrice":81
},
{
"id":1,
"name":"O'REILLY",
"website":null,
"books":[
{
"id":5,
"name":"Effective TypeScript",
"edition":2,
"price":69
},
{
"id":3,
...omitted...
},
{
"id":1,
...omitted...
},
{
"id":9,
...omitted...
},
{
"id":7,
...omitted...
}
],
"avgPrice":53.1
}
]
[
{
"id":2,
"name":"MANNING",
"website":null,
"books":[
{
"id":12,
"name":"GraphQL in Action",
"edition":3,
"price":80
},
{
"id":10,
...omitted...
}
],
"avgPrice":80
},
{
"id":1,
"name":"O'REILLY",
"website":null,
"books":[
{
"id":6,
"name":"Effective TypeScript",
"edition":3,
"price":88
},
{
"id":4,
...omitted...
},
{
"id":2,
...omitted...
},
{
"id":8,
...omitted...
}
],
"avgPrice":65.25
}
]
Cache Invalidation
Now let's modify the property Book.tenant
of the Book
object with id 6
from "b" to "a".
Since Book-6
belongs to BookStore-1
, it is foreseeable that the multi-view caches corresponding to the properties BookStore.books-1
and BookStore.avgPrice-1
will definitely be invalidated.
-
If BinLog trigger is enabled, modifying the database in any way can lead to Jimmer's cache consistency involvement. For example, directly executing the following SQL in SQL IDE:
update BOOK
set TENANT = 'a'
where ID = 6; -
If only Transaction trigger is enabled, the database must be modified using Jimmer's API:
- Java
- Kotlin
sqlClient.save(
Immutables.createBook(draft -> {
draft.setId(6L);
draft.setTenant("a");
})
);sqlClient.save(
Book {
id = 6L
tenant = "a"
}
)
No matter which way above is used to modify the data, you will see the following log output:
Delete data from redis: [Book-6] ❶
Delete data from redis: [Author.books-3] ❷
Delete data from redis: [BookStore.books-1] ❸
Delete data from redis: [BookStore.avgPrice-1] ❹
-
❶ Update object cache of modified entity
-
❷ Any association property targeting
Book
must be affected, of course includingAuthor.books
According to existing database data, the affected
Author
object id is 3 -
❸ Any association property targeting
Book
must be affected, of course includingBookStore.books
According to existing database data, the affected
BookStore
object id is 1 -
❹ The calculated cache
BookStore.avgPrice
ofBookStore
object with id 1 is also affected. This is the most amazing characteristic.Although the framework is unaware of the calculation rule used by users in calculated properties, in the Calculated Cache documentation, we discussed the following code in the
BookStoreAvgPriceResolver
class:- Java
- Kotlin
@EventListener
public void onAssociationChange(AssociationEvent e) {
if (sqlClient.getCaches().isAffectedBy(e) &&
e.isChanged(BookStoreProps.BOOKS)
) {
...code omitted...
}
}@EventListener
fun onAssociationChange(e: AssociationEvent) {
if (sqlClient.caches.isAffectedBy(e) &&
e.isChanged(BookStore::books)
) {
...code omitted...
}
}If you have forgotten the specific logic of this code, you can review the Calculated Cache documentation. Just focus on the highlighted line. Here, this calculated property cares about changes to the association property
BookStore.books
.tipModifying the association field between tables is not the only way to trigger association change events. Modifying the filtered field in associated objects that affects global filters, like
TENANT
here, can also trigger association change events.This is a very important characteristic of Jimmer's trigger mechanism!
It is obvious that ❸ has already sensed the change of association property
BookStore.books
, so it will further lead to the invalidation of the calculated cache here.