基本用法
引入对象抓取器的原因
一句话查询任意形状的数据结构,就如同GraphQL所做的那样
在了解对象抓取器之前,有必要先了解常规的属性查询。
例如,我需要这样一个SQL:
select
b.id,
b.name,
b.edition
/* 不需要`b.price`和`b.store_id` */
from book b
where b.edition = 3;
在这个SQL中,我们只查询部分列,对应的Jimmer写法:
- Java
- Kotlin
BookTable table = Tables.BOOK_TABLE;
List<Tuple3<Long, String, Integer>> tuples = sqlClient
.createQuery(book)
.where(table.edition().eq(3))
.select(
table.id(),
table.name(),
table.edition()
)
.execute();
System.out.println(tuples);
val tuples = sqlClient
.createQuery(Book::class) {
where(table.edition.eq(3))
select(
table.id,
table.name,
table.edition
)
}
.execute()
println(tuples)
打印输出为 (为了方便阅读,这里认为进行了格式化):
[
{
"_1" : 3,
"_2" : "Learning GraphQL",
"_3" : 3
}
...省略其他对象
]
由于只查询对象的部分属性,所以无法简单地使用select(table)
,而是用了select(expr1, expr2, ..., expr3)
。
然而,这会导致返回的结果类型是元组,而非Book对象。
元组中属性的顺序对应select()
中的顺序,在上述例子中,每一个元组中的对应关系为:
_1 -> id
_2 -> name
_3 -> edition
如果我们需要返回对象,有一个选择是定义一个BookDTO类型 (假设Java版使用了lombok)
和原始的实体类型Book
不同,BookDTO
仅仅包含我们需要的属性,而非所有属性。
- Java
- Kotlin
@Data
public class BookDTO {
private long id;
private String name;
private int edition;
}
data class BookDTO(
val id: Long,
val name: String,
val edition: Int
)
再把元组转化为BookDTO
对象,有两种办法
-
不修改之前的代码,先查询出
Tuple3<Long, String, Integer>
集合,再用Stream API转换 -
利用Jimmer的快捷API,直接转换查询结果
这里,我们采用第二种方式,代码如下
- Java
- Kotlin
BookTable table = Tables.BOOK_TABLE;
List<BookDTO> bookDTOs = sqlClient
.createQuery(book)
.where(table.edition().eq(3))
.select(
table.id(),
table.name(),
table.edition()
)
.map(tuple -> {
BookDTO dto = new BookDTO();
dto.setId(tuple.get_1());
dto.setName(tuple.get_2());
dto.setEdition(tuple.get_3());
return dto;
});
System.out.println(bookDTOs);
val bookDTOs = sqlClient
.createQuery(Book::class) {
where(table.edition.eq(3))
select(
table.id,
table.name,
table.edition
)
}
.map {
BookDTO(it._1, it._2, it._3)
}
println(tuples)
转化后的结果才是我们需要的类型:
[
{
"id" : 3,
"name" : "Learning GraphQL",
"edition" : 3
}
...省略其他对象
]
JPA和MyBatis就是采用这样的选择:
-
JPA默认返回对象的所有属性,想要自定义返回属性的话,
-
要么使用HQL:
select new BookDTO(...) from ...
-
或者使用
query.unwrap(SQLQuery.class).setResultTransformer(Transformers.aliasToBean(DTO.class))
-
-
MyBatis需要编写和DTO对应的ResultMapper.
然而,不论是JPA还是MyBatis都避免不了需要额外再写一个DTO类并自己肩负起从查询结构到DTO对象的转化逻辑。
不幸的是
-
查询业务往往具有多样性,格式多样化必然DTO类型变多*(作为聚合根类型而言可能存在格式多样化问题,作为其他DTO的关联类型 会更容易导致格式多样化问题)*。 最终导致
-
对于JPA而言,这样的做法违背了了ORM的初衷:便捷性
接下来,我们使用Jimmer的对象抓取器来简化这一流程。
使用对象抓取器
Jimmer会根据每个实体接口,生成对应的Fetcher类,这些Fetcher就是对象抓取器。
对象抓取器会根据抓取的属性,自动封装成对象,免去了手动映射以及编写额外DTO类的烦恼。
Annotation processor会为每一 个实体接口自动生成一个Fetcher类,在这个例子中,就是BookFetcher
-
例1:
上方示例中的查询,以对象抓取器的形式可以写成:
- Java
- Kotlin
BookTable table = Tables.BOOK_TABLE;
List<Book> books = sqlClient.createQuery(table)
.where(table.edition().eq(3))
.select(
table.fetch(
Fetchers.BOOK_FETCHER
// id是隐含的、强制的
.name()
.edition()
)
)
.execute();
System.out.println(books);val books = sqlClient
.createQuery(Book::class) {
where(table.edition.eq(3))
select(
table.fetchBy {
// id是隐含的、强制的
name
edition
}
)
}
.execute()
println(books)可以看到得到的返回值就是
Book
类型,不需要再进行映射,这为日常编码节省了很多时间。输出结果如下:
[
{
"id" : 3,
"name" : "Learning GraphQL",
"edition" : 3
},
...省略其他对象...
] -
例2
我们可以修改一下代码,不抓取书籍版本,而抓取书籍价格:
- Java
- Kotlin
BookTable table = Tables.BOOK_TABLE;
List<Book> books = sqlClient.createQuery(table)
.where(table.edition().eq(3))
.select(
table.fetch(
Fetchers.BOOK_FETCHER
.name()
.price()
)
)
.execute();
System.out.println(books);val books = sqlClient
.createQuery(Book::class) {
where(table.edition.eq(3))
select(
table.fetchBy {
name
price
}
)
}
.execute()
println(books)此时输出的JSON为:
[
{
"id" : 3,
"name" : "Learning GraphQL",
"price" : 51.00
},
...省略其他对象...
] -
例3
你甚至可以查询关联对象
- Java
- Kotlin
BookTable table = Tables.BOOK_TABLE;
List<Book> books = sqlClient.createQuery(table)
.where(table.edition().eq(3))
.select(
table.fetch(
Fetchers.BOOK_FETCHER
.name()
.authors(
Fetchers.AUTHOR_FETCHER
.firstName()
)
)
)
.execute();
System.out.println(books);val books = sqlClient
.createQuery(Book::class) {
where(table.edition.eq(3))
select(
table.fetchBy {
name
authors {
firstName
}
}
)
}
.execute()
println(books)此时输出的JSON为:
[
{
"id" : 3,
"name" : "Learning GraphQL",
"authors": [
{ "id": 1, "Eve" },
{ "id": 2, "Alex" },
]
},
...省略其他对象...
]
借助Jimmer实体的动态性,不再需要编写额外的DTO,即可实现多样化的查询业务,只需要改变对象抓取器的形状即可。
对查询返回的输出数据而言,消灭
能极大地提高服务端的生产效率。和JPA EntityGraph的差别
当对象抓取器用于抓取关联属性时,和JPA的EntityGraph比较类似,但存在如下差异
-
对象抓取器使用
batch-load
策略加载关联对象,而非使用JPA中join-fetch
的概念,这很重要,因为-
抓取集合关联属性不会像JPA的EntityGraph那样破坏分页查询
-
-
JPA的EntityGraph仅仅控制关联属性,但对象抓取器可以控制任何属性,和GraphQL一样,精确控制到每个属性
-
对于因为自关联属性导致的无限递归结构,对象抓取器支持递归查询