跳到主要内容

关联缓存

所谓关联缓存,指把当前对象id映射为关联对象id或集合。

其中

  • BookStore.books-*: 一对多关联缓存
  • Book.store-*: 多对一关联缓存
  • Book.authors-*: 多对多关联缓存
  • Author.books-*: 多对多关联缓存
警告

和其他关联缓存不同,有一种场景不需要使用一对一或多对一关联缓存。

如果一对一或多对一关联基于真实外键,在数据库中存在对应的外键约束,那么外键本身就是关联对象id,无需使用关联缓存。

其他情况下,Jimmer都会使用一对一或多对一关联缓存,这些情况包括:

  • 引用关联属性是反向映射

    @OneToOnemappedBy被配置

  • 引用关联属性基于伪外键

    所谓伪外键,即在开发人员意识中是外键,但是数据库中并没有对应的外键约束

    伪外键字段可能是非法值,非null的值并不意味着关联对象的存在,所以,需要用关联缓存过滤得到合法的关联对象

  • 引用关联属性基于中间表,而非基于外键。

  • 用Jimmer实现GraphQL时,本不应该在聚合根查询中使用对象抓取器 (GraphQL和对象抓取器是同质的功能), 但在聚合根查询中错误地使用了对象抓取器得到没有外键的对象。然而,GraphQL请求体中包含多对一关联。

在附带的官方示例中,多对一关联Book.store基于真实外键,所以,其多对一缓存不会被使用。

因此,本文的例子基于一对多关联BookStore.books和多对多关联Book.authors

启用关联缓存

@Bean
public CacheFactory cacheFactory(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper
) {

RedisTemplate<String, byte[]> redisTemplate =
RedisCaches.cacheRedisTemplate(connectionFactory);

return new CacheFactory() {

@Override
public Cache<?, ?> createObjectCache(@NotNull ImmutableType type) {
...省略代码...
}

// 将当前对象id映射为关联对象id
// 适用于一对一和多对一关联属性
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(prop, Duration.ofMinutes(10));
}

// 将当前对象id映射为关联对象id集合
// 适用于一对多和多对多关联属性
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(prop, Duration.ofMinutes(5));
}

private <K, V> Cache<K, V> createPropCache(ImmutableProp prop, Duration duration) {
return new ChainCacheBuilder<>()
.add(
new CaffeineBinder<>(512, Duration.ofSeconds(1))
)
.add(
new RedisValueBinder<>(
redisTemplate,
objectMapper,
prop,
duration
)
)
.build();
}

...省略其他代码...
};
}

集合关联排序

警告

对于集合类型的关联,如果我们使用对象抓取器的字段级过滤器来实现关联级排序,将会导致Jimmer无视关联缓存。

如果既要充分利用关联缓存,又要让对象抓取器查询返回的关联对象集合的排序可控,可以在实体上使用静态配置制定默认排序。

  • BookStore.books

    BookStore.java
    @Entity
    public interface BookStore {

    @OneToMany(
    mappedBy = "store",
    orderedProps = {
    @OrderedProp("name"),
    @OrderedProp(value = "edition", desc = true)
    }
    )
    List<Book> books();

    ...省略其他代码...
    }
  • Book.authors

    Book.java
    @Entity
    public interface Book {

    @ManyToMany(
    orderedProps = {
    @OrderedProp("firstName"),
    @OrderedProp("lastName")
    }
    )
    List<Author> authors();

    ...省略其他代码...
    }

使用

如本文开头所言,本文的的示范基于一对多关联BookStore.books和多对多关联Book.authors

一对多:BookStore.books

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()
)
)
)
.execute();
System.out.println(stores);
  • 第一步:查询聚合根

    首先查询聚合根对象,执行如下SQL

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.WEBSITE
    from BOOK_STORE tb_1_

    这里实现了代码中的查询,得到了一些BookStore对象。这种被用户直接查询而得的对象叫做聚合根对象

    警告

    Jimmer不会对用户查询返回的聚合对象进行缓存,因为这种查询结果的一致性无法保证。 即便需要以牺牲一致性为代价对其缓存,也是用户的业务需要,不应由框架抽象并统一其行为。

  • 第二步:通过关联缓存把当前对象id转化为关联对象id

    上面的代码会得到一系列聚合根对象,如果数据库采用官方例子的数据,会得到两个聚合根对象。

    代码中的对象抓取器包含了一对多关联BookStore.books

    这2条BOOK_STORE的主键ID为1和2。

    Jimmer先从Redis查找数据,被查找的键为BookStore.books-1BookStore.books-2

    假设无法在Redis中找到这些键所对应的数据

    127.0.0.1:6379> keys BookStore.books-*
    (empty array)

    所以,执行如下SQL,从数据库加载数据

    select
    tb_1_.STORE_ID,
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION,
    tb_1_.PRICE
    from BOOK tb_1_
    where
    tb_1_.STORE_ID in (
    ? /* 1 */, ? /* 2 */
    )
    order by
    tb_1_.NAME asc,
    tb_1_.EDITION desc

    Jimmer会把查询结果放入Redis,因此,我们可以从redis中查看这些数据

    127.0.0.1:6379> keys BookStore.books-*
    1) "BookStore.books-2"
    2) "BookStore.books-1"
    127.0.0.1:6379> get BookStore.books-1
    "[6,5,4,3,2,1,9,8,7]"
    127.0.0.1:6379> get BookStore.books-2
    "[12,11,10]"
    127.0.0.1:6379>

    这样,两个BookStore对象可以通过的一对多关联BookStore.books得到各自的关联对象id集合。

    毫无疑问,在Redis中的数据因超时而被清除之前,再次执行上述Java/Kotlin代码,将直接从Redis中返回关联数据,第二条SQL不会被生成。

  • 第三步:通过对象缓存把关联对象id转化为关联对象

    这类操作在对象缓存中详细讨论过,本文聚焦于关联缓存,不再赘述。

    警告

    在启用缓存的配置中,如果对某个关联属性启用关联缓存但并未对其关联对象的类型启用对象缓存,将会导致异常。

信息

之前在对象抓取器中,我们看到Jimmer只需要一句SQL就可以根据一批当前对象查询所有关联对象,而这里却需要两句,请参见:

最终,Jimmer把两个步骤的结果拼接在一起,作为最终返回给用户的数据

[
{
"id":1,
"name":"O'REILLY",
"website":null,
"books":[
{
"id":6,
"name":"Effective TypeScript",
"edition":3,
"price":88
},
{
"id":5,
...略...
},
{
"id":4,
...略...
},
{
"id":3,
...略...
},
{
"id":2,
...略...
},
{
"id":1,
...略...
},
{
"id":9,
...略...
},
{
"id":8,
...略...
},
{
"id":7,
...略...
}
]
},
{
"id":2,
"name":"MANNING",
"website":null,
"books":[
{
"id":12,
"name":"GraphQL in Action",
"edition":3,
"price":80
},
{
"id":11,
...略...
},
{
"id":10,
...略...
}
]
}
]

多对多:Book.authors

BookTable table = Tables.BOOK_TABLE;
List<Book> books = sqlClient
.createQuery(table)
.where(table.edition().eq(1))
.select(
table.fetch(
Fetchers.BOOK_FETCHER
.allScalarFields()
.authors(
Fetchers.AUTHOR_FETCHER
.allScalarFields()
)
)
)
.execute();
System.out.println(books);
  • 第一步:查询聚合根

    首先查询聚合根对象,执行如下SQL

    select
    tb_1_.ID,
    tb_1_.NAME,
    tb_1_.EDITION,
    tb_1_.PRICE
    from BOOK tb_1_
    where
    tb_1_.EDITION = ? /* 1 */

    这里实现了代码中的查询,得到了一些Book对象。这种被用户直接查询而得的对象叫做聚合根对象

    警告

    Jimmer不会对用户查询返回的聚合对象进行缓存,因为这种查询结果的一致性无法保证。 即便需要以牺牲一致性为代价对其缓存,也是用户的业务问题,不应由框架抽象并统一其行为。

  • 第二步:通过关联缓存把当前对象id转化为关联对象id

    上面的代码会得到一系列聚合根对象,如果数据库采用官方例子的数据,会得到4个聚合根对象。

    代码中的对象抓取器包含了一对多关联Book.authors

    这4条BOOK的主键ID为1、4、7和10。

    Jimmer先从Redis查找数据,被查找的键为Book.authors-1Book.authors-4Book.authors-7Book.authors-10

    假设无法在Redis中找到这些键所对应的数据

    127.0.0.1:6379> keys Book.authors-*
    (empty array)

    所以,执行如下SQL,从数据库加载数据

    select
    tb_1_.BOOK_ID,
    tb_1_.AUTHOR_ID
    from BOOK_AUTHOR_MAPPING tb_1_
    inner join AUTHOR tb_3_
    on tb_1_.AUTHOR_ID = tb_3_.ID
    where
    tb_1_.BOOK_ID in (
    ? /* 1 */, ? /* 4 */, ? /* 7 */, ? /* 10 */
    )
    order by
    tb_3_.FIRST_NAME asc,
    tb_3_.LAST_NAME asc
    备注

    如果不通过@ManyToMany.orderedProps为关联属性Book.authors指定默认排序,这里就的join不会出现

    Jimmer会把查询结果放入Redis,因此,我们可以从redis中查看这些数据

    127.0.0.1:6379> keys Book.authors-*
    1) "Book.authors-4"
    2) "Book.authors-1"
    3) "Book.authors-10"
    4) "Book.authors-7"
    127.0.0.1:6379> get Book.authors-1
    "[2,1]"
    127.0.0.1:6379> get Book.authors-4
    "[3]"
    127.0.0.1:6379> get Book.authors-7
    "[4]"
    127.0.0.1:6379> get Book.authors-10
    "[5]"
    127.0.0.1:6379>

    这样,我们就得到4个Book对象各自通过多对多关联Book.authors可以得到的关联对象id集合。

    毫无疑问,在Redis中的数据因超时而被清除之前,再次执行上述Java/Kotlin代码,将直接从Redis中返回关联数据,第二条SQL不会被生成。

  • 第三步:通过对象缓存把关联对象id转化为关联对象

    这类操作在对象缓存中详细讨论过,本文聚焦于关联缓存,不再赘述。

    警告

    在启用缓存的配置中,如果对某个关联属性启用关联缓存但并未对其关联对象的类型启用对象缓存,将会导致异常。

信息

之前在对象抓取器中,我们看到Jimmer只需要一句SQL就可以根据一批当前对象查询所有关联对象,而这里却需要两句,请参见:

最终,Jimmer把3个步骤的结果拼接在一起,作为最终返回给用户的数据

[
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":51,
"authors":[
{
"id":2,
"firstName":"Alex",
"lastName":"Banks",
"gender":"MALE"
},
{
"id":1,
"firstName":"Eve",
"lastName":"Procello",
"gender":"FEMALE"
}
]
},
{
"id":4,
"name":"Effective TypeScript",
"edition":1,
"price":73,
"authors":[...略...]
},
{
"id":7,
"name":"Programming TypeScript",
"edition":1,
"price":47.5,
"authors":[...略...]
},
{
"id":10,
"name":"GraphQL in Action",
"edition":1,
"price":80,
"authors":[...略...]
}
]

一致性

警告

要使用Jimmer缓存的自动一致性,需要先启用触发器

一对多:BookStore.books

修改BOOK表的外键STORE_ID,Jimmer自动删除多对一关联Book.store和一对多BookStore.books的关联缓存

  • 假如启用了BinLog触发器,用任何手段修改数据库都可以导致Jimmer缓存一致性的介入。比如直接在SQL IDE中执行如下SQL

    update BOOK 
    /* Old value: 1, New value: 2 */
    set STORE_ID = 2
    where ID = 7;
  • 假如只启用了Transaction触发器,则必须用Jimmer的API修改数据库

    BookTable table = Tables.BOOK_TABLE;
    sqlClient
    .createUpdate(table)
    // Old value: 1L, New value: 2L
    .set(table.store().id, 2L)
    .where(table.id().eq(7L))
    .execute();

无论通过上述何种方式修改数据,你都会在看到如下日志输出

Delete data from redis: [Book-7]
Delete data from redis: [Book.store-7] ❶
Delete data from redis: [BookStore.books-1] ❷
Delete data from redis: [BookStore.books-2] ❸
  • ❶ 对于id为7的Book对象而言,其多对一关联Book.store对应的关联缓存被删除
  • ❷ 对于id为1 (修改前的旧值) 的BookStore对象而言,其一对多关联BookStore.books对应的关联缓存被删除
  • ❸ 对于id为2 (修改前的新值) 的BookStore对象而言,其一对多关联BookStore.books对应的关联缓存被删除

多对多:Book.authors

为中间表BOOK_AUTHOR_MAPPING插入数据,Jimmer自动删除多对多关联Book.authorsAuthor.books的关联缓存

备注

从中间表中删除数据也可以达到相同的效果,这里我们使用插入来展示效果

  • 假如启用了BinLog触发器,用任何手段修改数据库都可以导致Jimmer缓存一致性的介入。比如直接在SQL IDE中执行如下SQL

    insert into 
    BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
    values(10, 3);
  • 假如只启用了Transaction触发器,则必须用Jimmer的API修改数据库

    sqlClient
    .getAssociations(BookProps.AUTHORS)
    .save(10L, 3L);

无论通过上述何种方式修改数据,你都会在看到如下日志输出

Delete data from redis: [Book.authors-10] ❶
Delete data from redis: [Author.books-3] ❷
  • ❶ 对于id为10的Book对象而言,其一对多关联Book.authors对应的关联缓存被删除
  • ❷ 对于id为3的Author对象而言,其一对多关联Author.books对应的关联缓存被删除

逻辑删除相关注意事项

如果关联对象支持逻辑删除,默认情况下,关联缓存仍然被支持。

注意

然而,一旦逻辑删除过滤器被忽略反转,关联缓存会被忽略。