Skip to main content

Enable Cache

CacheFactory Interface

To enable cache, you first need to implement the CacheFactory/KCacheFactory interface, which is defined as follows:

CacheFactory.java
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;
}
}
info

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 cache

    The 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.

    info

    This 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 the id of the associated object is actually the foreign key field of the current table.

  • createAssociatedIdCache and createAssociatedIdListCache: Enable association cache

    Both createAssociatedIdCache and createAssociatedIdListCache 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 cache

    The 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:

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();
}
};
  • ❶ 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.

tip

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 classesImplemented InterfaceSupports Multi-view Cache
org.babyfish.jimmer.spring.cache.CaffeineBinderorg.babyfish.jimmer.sql.cache.chain.LoadingBinderNo
org.babyfish.jimmer.spring.cache.RedisValueBinderorg.babyfish.jimmer.sql.cache.chain.SimpleBinderNo
org.babyfish.jimmer.spring.cache.RedisHashBinderorg.babyfish.jimmer.sql.cache.chain.SimpleBinder.ParameterizedYes
note

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.

@Bean
public CacheFactory cacheFactory() {
return new CacheFactory() {
...omit code...
};
}

Underlying API Configuration

JSqlClient sqlClient = JSqlClient
.newBuilder()
.setCacheFactory(
new CacheFactory() {
...omit code...
}
)
...omit other configuration...
.build();

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.

note

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:

@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();
}
};
}