Enable Cache
CacheFactory Interface
To enable cache, you first need to implement the CacheFactory
/KCacheFactory
interface, which is defined as follows:
- Java
- Kotlin
package org.babyfish.jimmer.sql.cache;
import org.babyfish.jimmer.meta.ImmutableProp;
import org.babyfish.jimmer.meta.ImmutableType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public interface CacheFactory {
@Nullable
default Cache<?, ?> createObjectCache(@NotNull ImmutableType type) {
return null;
}
@Nullable
default Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return null;
}
@Nullable
default Cache<?, List<?>> createAssociatedIdListCache(@NotNull ImmutableProp prop) {
return null;
}
@Nullable
default Cache<?, ?> createResolverCache(@NotNull ImmutableProp prop) {
return null;
}
}
package org.babyfish.jimmer.sql.kt.cache
import org.babyfish.jimmer.meta.ImmutableProp
import org.babyfish.jimmer.sql.cache.Cache
import org.babyfish.jimmer.sql.cache.CacheFactory
interface KCacheFactory : CacheFactory {
override fun createObjectCache(type: ImmutableType): Cache<*, *>? =
null
override fun createAssociatedIdCache(prop: ImmutableProp): Cache<*, *>? =
null
override fun createAssociatedIdListCache(prop: ImmutableProp): Cache<*, List<*>>? =
null
override fun createResolverCache(prop: ImmutableProp): Cache<*, *>? =
null
}
For Kotlin, in order to generate better code when overriding createAssociatedIdListCache
, please implement the org.babyfish.jimmer.sql.kt.cache.KCacheFactory
interface.
Otherwise, the return type of the override method generated by the IDE will be Cache<*, MutableList<*>>
instead of the expected Cache<*, List<*>>
.
Jimmer calls this interface to initialize caches for types and properties. Users implement this interface to answer:
-
createObjectCache
: Enable object cacheThe parameter specifies an entity type. If you want to enable object cache for it, create and return the cache; otherwise, return null.
refers to mapping id to entity object.infoThis entity object is orphaned, without associated properties.
One-to-one/many-to-one associations based on foreign keys are an exception, they can hold associated objects with only the
id
property, because theid
of the associated object is actually the foreign key field of the current table. -
createAssociatedIdCache
andcreateAssociatedIdListCache
: Enable association cacheBoth
createAssociatedIdCache
andcreateAssociatedIdListCache
are used to enable association cache. The only difference between them is:-
createAssociatedIdCache
: For one-to-one or many-to-one associations. -
createAssociatedIdListCache
: For one-to-many or many-to-many associations.
The parameter specifies an association property. If you want to enable association cache for it, create and return the cache; otherwise return null.
refers to mapping id to associated id or associated id collection. -
-
createResolverCache
: Enable calculated cacheThe parameter specifies a complex calculated property. If you want to enable calculated cache for it, create and return the cache; otherwise, return null.
refers to mapping id to calculated result.
Multi-level Cache Architecture
The return type of all methods of the CacheFactory
interface is org.babyfish.jimmer.sql.cache.Cache<K, V>
.
Users do not need to directly implement Cache<K, V>
, but need to use org.babyfish.jimmer.sql.cache.chain.ChainCacheBuilder
to build multi-level caches.
In theory, ChainCacheBuilder
supports caches of any number of levels. However, in most projects, two levels of cache are sufficient, for example:
- Java
- Kotlin
return new CacheFactory() {
@Override
@Nullable
public Cache<?, ?> createObjectCache(@NotNull ImmutableType type) {
return new ChainCacheBuilder<>()
.add( ❶
CaffeineValueBinder
.forObject(type)
.maximumSize(512)
.duration(Duration.ofSeconds(10))
.build()
)
.add( ❷
RedisValueBinder
.forObject(type)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(Duration.ofHours(10))
.build()
)
)
.build();
}
};
return object: KCacheFactory {
override fun createObjectCache(type: ImmutableType): Cache<*, *>? =
ChainCacheBuilder<Any, Any>()
.add( ❶
CaffeineValueBinder
.forObject(type)
.maximumSize(512)
.duration(Duration.ofSeconds(10))
.build()
)
.add( ❷
RedisValueBinder
.forObject(type)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(Duration.ofHours(10))
.build()
)
)
.build()
}
-
❶ Indicates first level cache based on Caffeine in-process JVM cache.
-
❷ Indicates second level cache based on Redis remote cache.
As we can see, ChainCacheBuilder
uses chain programming style. Calling the add
method multiple times can build multi-level caches.
The ChainCacheBuilder.add
method is defined as follows:
public class ChainCacheBuilder<K, V> {
public ChainCacheBuilder<K, V> add(LoadingBinder<K, V> binder) { ❶
...omit code...
return this;
}
public ChainCacheBuilder<K, V> add(LoadingBinder.Parameterized<K, V> binder) { ❷
...omit code...
return this;
}
public ChainCacheBuilder<K, V> add(SimpleBinder<K, V> binder) { ❸
...omit code...
return this;
}
...omit other code...
}
-
❶
org.babyfish.jimmer.sql.cache.chain.LoadingBinder
is an interface. Any cache technology that automatically loads values on first access to a key can be adapted through this interface.Almost all in-process JVM caches have automatic loading capabilities, such as Caffeine or Guava Cache used in the above code.
-
❷ The code is only used by multi-view caches. Readers can ignore it for now.
-
❸
org.babyfish.jimmer.sql.cache.chain.SimpleBinder
is an interface. Any cache technology without automatic value loading behavior can be adapted through this interface.Almost all remote caches do not have automatic loading capabilities, such as Redis used in the above code.
Any cache technology can be adapted to the abstract interfaces LoadingBinder
or SimpleBinder
. Therefore, in Jimmer's multi-level cache architecture, no assumptions or restrictions are made on the choice of cache technology at any level.
If using Jimmer's SpringBoot Starter, you can use the following three cache technology adapter classes, as in the code above:
Jimmer's built-in adapter classes | Implemented Interface | Supports Multi-view Cache |
---|---|---|
org.babyfish.jimmer.spring.cache.CaffeineBinder | org.babyfish.jimmer.sql.cache.chain.LoadingBinder | No |
org.babyfish.jimmer.spring.cache.RedisValueBinder | org.babyfish.jimmer.sql.cache.chain.SimpleBinder | No |
org.babyfish.jimmer.spring.cache.RedisHashBinder | org.babyfish.jimmer.sql.cache.chain.SimpleBinder.Parameterized | Yes |
Multi-view caches will be explained in a subsequent article. Please ignore them for now.
Configuring CacheFactory
Now we have introduced the CacheFactory
interface and multi-level cache architecture, but there is one last step to enable cache.
The last step is to register CacheFactory
for Jimmer.
SpringBoot Configuration
If using SpringBoot Starter, just let CacheFactory
be managed by Spring.
- Java
- Kotlin
@Bean
public CacheFactory cacheFactory() {
return new CacheFactory() {
...omit code...
};
}
@Bean
fun cacheFactory(): KCacheFactory =
object: KCacheFactory {
...omit code...
}
Underlying API Configuration
- Java
- Kotlin
JSqlClient sqlClient = JSqlClient
.newBuilder()
.setCacheFactory(
new CacheFactory() {
...omit code...
}
)
...omit other configuration...
.build();
val sqlClient = newKSqlClient {
setCacheFactory(
object: KCacheFactory {
...omit code...
}
)
...omit other configuration...
}
Redis Cache Helper APIs
Earlier we mentioned that if using Jimmer's SpringBoot Starter, you can use the ready-made org.babyfish.jimmer.spring.cache.RedisValueBinder
without having to adapt Redis yourself.
RedisHashBinder
is related to multi-view caches and will not be discussed in this article.
To build RedisValueBinder
, you need a RedisOperations<String, byte[]>.
Jimmer's SpringBoot Stater provides the org.babyfish.jimmer.spring.cache.RedisCaches
class whose static method RedisCaches.cacheRedisTemplate
can quickly build this RedisOperations<String, byte[]> object.
An example of the helper method RedisCaches.cacheRedisTemplate
is as follows:
- Java
- Kotlin
@Bean
public CacheFactory cacheFactory(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper
) {
return new CacheFactory() {
@Override
@Nullable
public Cache<?, ?> createObjectCache(@NotNull ImmutableType type) {
return new ChainCacheBuilder<Object, Object>()
.add(
CaffeineValueBinder
.forProp()
.maximumSize(1024)
.duration(RedisCaches.ofMinutes(24))
.bind()
)
.add(
RedisValueBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(Duration.ofHours(24))
.build()
)
.build();
}
@Override
@Nullable
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(
prop,
Duration.ofMinutes(10),
Duration.ofHours(10)
);
}
@Override
@Nullable
public Cache<?, List<?>> createAssociatedIdListCache(@NotNull ImmutableProp prop) {
return createPropCache(
prop,
Duration.ofMinutes(5),
Duration.ofHours(5),
);
}
@Override
@Nullable
public Cache<?, ?> createResolverCache(@NotNull ImmutableProp prop) {
return createPropCache(
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
);
}
private Cache<?, ?> createPropCache(
ImmutableProp prop,
Duration caffeineDuration
Duration redisDuration
) {
return new ChainCacheBuilder<K, V>()
.add(
CaffeineValueBinder
.forProp(prop)
.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<*, *>? =
ChainCacheBuilder<Any, Any>()
.add(
CaffeineValueBinder
.forObject(type)
.maximumSize(1024)
.duration(Duration.ofHours(1))
.build()
)
.add(
RedisValueBinder
.forObject(type)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(Duration.ofHours(24))
.build()
)
.build()
override fun createAssociatedIdCache(prop: ImmutableProp): Cache<*, *>? =
createPropCache(
prop,
Duration.ofMinutes(10),
Duration.ofHours(10)
)
override fun createAssociatedIdListCache(prop: ImmutableProp): Cache<*, List<*>>? =
createPropCache(
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
)
override fun createResolverCache(prop: ImmutableProp): Cache<*, *>? =
createPropCache(
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
)
private fun createPropCache(
type: ImmutableType,
caffeineDuration: Duration,
redisDuration: Duration
): Cache<*, *> =
ChainCacheBuilder<Any, Any>()
.add(
CaffeineValueBinder
.forProp(prop)
.maximumSize(512)
.duration(caffeineDuration)
.build()
)
.add(
RedisValueBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build()
}
}