关联缓存
所谓关联缓存,指把当前对象id映射为关联对象id或集合。
其中
BookStore.books-*
: 一对多关联缓存Book.store-*
: 多对一关联缓存Book.authors-*
: 多对多关联缓存Author.books-*
: 多对多关联缓存
和其他关联缓存不同,有一种场景不需要使用一对一或多对一关联缓存。
如果一对一或多对一关联基于真实外键,在数据库中存在对应的外键约束,那么外键本身就是关联对象id,无需使用关联缓存。
其他情况下,Jimmer都会使用一对一或多对一关联缓存,这些情况包括:
-
引用关联属性是反向映射
即
@OneToOne
的mappedBy
被配置 -
引用关联属性基于伪外键
所谓伪外键,即在开发人员意识中是外键,但是数据库中并没有对应的外键约束
伪外键字段可能是非法值,非null的值并不意味着关联对象的存在,所以,需要用关联缓存过滤得到合法的关联对象
-
引用关联属性基于中间表,而非基于外键。
-
在用Jimmer实现GraphQL时,本不应该在聚合根查询中使用对象抓取器 (GraphQL和对象抓取器是同质的功能), 但在聚合根查询中错误地使用了对象抓取器得到没有外键的对象。然而,GraphQL请求体中包含多对一关联。
在附带的官方示例中,多对一关联Book.store
基于真实外键,所以,其多对一缓存不会被使用。
因此,本文的例子基于一对多关联BookStore.books
和多对多关联Book.authors
。
启用关联缓存
- Java
- Kotlin
@Bean
public CacheFactory cacheFactory(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper
) {
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),
Duration.ofHours(10)
);
}
// 将当前对象id映射为关联对象id集合
// 适用于一对多和多对多关联属性
@Override
public Cache<?, ?> createAssociatedIdCache(@NotNull ImmutableProp prop) {
return createPropCache(
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
);
}
private <K, V> Cache<K, V> createPropCache(
ImmutableProp prop,
Duration caffeineDuration,
Duration redisDuration
) {
return new ChainCacheBuilder<>()
.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<*, *>? =
...省略代码...
// 将当前对象id映射为关联对象id
// 适用于一对一和多对一关联属性
override fun createAssociatedIdCache(prop: ImmutableProp): Cache<*, *>? =
createPropCache(
prop,
Duration.ofMinutes(10),
Duration.ofHours(10)
)
// 将当前对象id映射为关联对象id集合
// 适用于一对多和多对多关联属性
override fun createAssociatedIdListCache(prop: ImmutableProp): Cache<*, List<*>>? =
createPropCache(
prop,
Duration.ofMinutes(5),
Duration.ofHours(5)
)
private fun <K, V> createPropCache(
prop: ImmutableProp,
caffeineDuration: Duration,
redisDuration: Duration
): Cache<K, V> =
ChainCacheBuilder<Any, Any>()
.add(
CaffeineValueBinder
.forObject(type)
.maximumSize(512)
.duration(caffeineDuration)
.build()
)
.add(
RedisValueBinder
.forProp(prop)
.redis(connectionFactory)
.objectMapper(objectMapper)
.duration(redisDuration)
.build()
)
.build()
...省略其他代码...
}
}
集合关联排序
-
BookStore.books
- Java
- Kotlin
BookStore.java@Entity
public interface BookStore {
@OneToMany(
mappedBy = "store",
orderedProps = {
@OrderedProp("name"),
@OrderedProp(value = "edition", desc = true)
}
)
List<Book> books();
...省略其他代码...
}BookStore.kt@Entity
interface BookStore {
@OneToMany(
mappedBy = "store",
orderedProps = {
@OrderedProp("name"),
@OrderedProp(value = "edition", desc = true)
}
)
val books : List<Book>
...省略其他代码...
} -
Book.authors
- Java
- Kotlin
Book.java@Entity
public interface Book {
@ManyToMany(
orderedProps = {
@OrderedProp("firstName"),
@OrderedProp("lastName")
}
)
List<Author> authors();
...省略其他代码...
}Book.kt@Entity
interface Book {
@ManyToMany(
orderedProps = {
@OrderedProp("firstName"),
@OrderedProp("lastName")
}
)
val authors : List<Author>
...省略其他代码...
}
使用
如本文开头所言,本文的的示范基于一对多关联BookStore.books
和多对多关联Book.authors
。
一对多:BookStore.books
- 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()
)
)
)
.execute();
System.out.println(stores);
val stores = sqlClient
.createQuery(BookStore::class) {
select(
table.fetchBy {
allScalarFields()
books {
allScalarFields()
}
}
)
}
.execute()
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-1
和BookStore.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 descJimmer会把查询结果放入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
- Java
- Kotlin
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);
val books = sqlClient
.createQuery(Book::class) {
where(table.edition eq 1)
select(
table.fetchBy {
allScalarFields()
authors {
allScalarFields()
}
}
)
}
.execute()
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-1
、Book.authors-4
、Book.authors-7
和Book.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修改数据库
- Java
- Kotlin
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();sqlClient
.createUpdate(Book::class) {
// 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.authors
和Author.books
的关联缓存
从中间表中删除数据也可以达到相同的效果,这里我们使用插入来展示效果
-
假如启用了BinLog触发器,用任何手段修改数据库都可以导致Jimmer缓存一致性的介入。比如直接在SQL IDE中执行如下SQL
insert into
BOOK_AUTHOR_MAPPING(BOOK_ID, AUTHOR_ID)
values(10, 3); -
假如只启用了Transaction触发器,则必须用Jimmer的API修改数据库
- Java
- Kotlin
sqlClient
.getAssociations(BookProps.AUTHORS)
.save(10L, 3L);sqlClient
.getAssociations(Book::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
对应的关联缓存被删除
逻辑删除相关注意事项
如果关联对象支持逻辑删除,默认情况下,关联缓存仍然被支持。