DTO查询
之前我们介绍了,利用对象抓取器灵活控制被查询数据结构的形状。
现在,我们介绍一个与之等价的功能:查询DTO对象。
Jimmer提供了DTO语言。
该语言本质上是对象抓取器的另外一种表达方式
利用该语言,开发人员可以快速以某个实体类型为聚合根定义多种数据结构的形状,编译器会为每种形状定义生成相应的Java/Kotlin DTO类。每个DTO类型都包含和原动态类型之间的彼此转化逻辑,以及一个和自身形状匹配的对象抓取器。
某些情况下,服务端查询出某种形状的数据后,并不是为了作为HTTP请求的返回,而是自己用,用来驱动后续的复杂的业务逻辑,这是采用这种方式的理想场合。
注意,如果服务端查询某种形状的数据不是为了自己用,而是为了直接作为HTTP请求的返回值,则更推荐直接返回动态实体对象,并利用客户端篇中的方案自动生成开发体验很高的客户端代码。
定义DTO的形状
本文侧重于讲解如何查询静态DTO类型,并非系统性介绍DTO语言,请参考对象篇/DTO转换/DTO语言以了解完整的DTO语言。
假如Book
类的全名为com.yourcompany.yourproject.model.Book
,你可以
-
在实体定义所在项目中,建立目录
src/main/dto
-
在
src/main/dto
下,建立文件Book.dto
-
编辑此文件,利用DTO语言,定义Book实体的各种DTO形状
Book.dtoexport com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
store {
#allScalars
}
authors {
#allScalars
}
}
SimpleBookView { ...略... }
...省略其他DTO形状定义...
自动生成DTO类型
Jimmer负责编译dto文件,自动生成符合这些形状的DTO类型。
如果除了dto文件外还有其他Java/Kotlin原代码文件被修改了,直接点击IDE中运行按钮可以导致dto文件的重新编译
但是,如果除了dto文件外没有其他Java/Kotlin文件被修改,简单地点击IDE中运行按钮并不会导致dto文件被重新编译,除非显式地rebuild!
如果你使用的构建工具是Gradle,也可以使用社区提供的第三方Gradle插件来解决这个问题: jimmer-gradle
以上面代码中的BookDetailView
为例,此dto文件被Jimmer成功编译后,会自动生成如下DTO类型
- Java
- Kotlin
package com.yourcompany.yourproject.model.dto;
import com.yourcompany.yourproject.model.Book;
import org.babyfish.jimmer.View;
@GeneratedBy(file = "<your_project>/src/main/dto/Book.dto")
public class BookDetailView implements Input<Book> {
private long id;
private String name;
private int edition;
private BigDecimal price;
private TargetOf_store store;
private List<TargetOf_authors> authors;
public static class TargetOf_store implements Input<BookStore> {
private long id;
private String name;
@Nullable
private String website;
...省略其他成员...
}
public static class TargetOf_authors implements Input<Author> {
private long id;
private String firstName;
private String lastName;
private Gender gender;
...省略其他成员...
}
...省略其他成员...
}
package com.yourcompany.yourproject.model.dto
import com.yourcompany.yourproject.model.Book
import org.babyfish.jimmer.View
@GeneratedBy(file = "<your_project>/src/main/dto/Book.dto")
data class BookDetailView(
val id: Long = 0,
val name: String = "",
val edition: Int = 0,
val price: BigDecimal,
val store: TargetOf_store? = null,
val authors: List<TargetOf_authors> = emptyList(),
) : Input<Book> {
data class TargetOf_store(
val id: Long = 0,
val name: String = "",
val website: String? = null,
) : Input<BookStore> {
...省略其他成员...
}
data class TargetOf_authors(
val id: Long = 0,
public val firstName: String = "",
public val lastName: String = "",
public val gender: Gender,
) : Input<Author> {
...省略其他成员...
}
...省略其他成员...
}
-
生成的DTO类所在的包并非实体所处的包,而是其
dto
子包 -
对于Java而言,假设用户已经使用了lombok
查询DTO对象
使用findById
- Java
- Kotlin
BookDetailView view = sqlClient.findNullable(
BookDetailView.class,
1L
);
System.out.println(view);
val view = sqlClient.findNullable(
BookDetailView::class,
1L
);
println(view)
打印结果如下 (为方便阅读,人为进行了格式化)
BookDetailView(
id=1,
name=Learning GraphQL,
edition=1,
price=50.00,
store=BookDetailView.TargetOf_store(
id=1,
name=O'REILLY,
website=null,
version=0
),
authors=[
BookDetailView.TargetOf_authors(
id=2,
firstName=Alex,
lastName=Banks,
gender=MALE
),
BookDetailView.TargetOf_authors(
id=1,
firstName=Eve,
lastName=Procello,
gender=FEMALE
)
]
)
不难发现,虽然现在查询不再返回动态实体对象,但功能和却和对象抓取器完全一样。这什么为什么呢?
由DTO语言编译而来的DTO类型,都会包含与其形状匹配的对象抓取器,如下
- Java
- Kotlin
@Data
public class BookDetailView implements View<Book> {
public static final ViewMetadata<Book, BookDetailView> METADATA =
new ViewMetadata<Book, BookDetailView>(
Fetchers.BOOK_FETCHER
.name()
.edition()
.price()
.store(TargetOf_store.METADATA.getFetcher())
.authors(TargetOf_authors.METADATA.getFetcher()),
BookDetailView::new
);
@Data
public static class TargetOf_store implements View<BookStore> {
public static final ViewMetadata<BookStore, TargetOf_store> METADATA =
new ViewMetadata<BookStore, TargetOf_store>(
Fetchers.BOOK_STORE_FETCHER
.name()
.website()
.version(),
TargetOf_store::new
);
...省略其他成员...
}
@Data
public static class TargetOf_authors implements View<Author> {
public static final ViewMetadata<Author, TargetOf_authors> METADATA =
new ViewMetadata<Author, TargetOf_authors>(
Fetchers.AUTHOR_FETCHER
.firstName()
.lastName()
.gender(),
TargetOf_authors::new
);
...省略其他成员...
}
...省略其他成员...
}
@Data
data class BookDetailView(
...略...
) : View<Book> {
...省略其他成员...
companion object {
@JvmStatic
public val METADATA: ViewMetadata<Book, BookDetailView> =
ViewMetadata<Book, CompositeBookInput>(
newFetcher(Book::class).by {
name()
edition()
price()
store(TargetOf_store.METADATA.fetcher)
authors(TargetOf_authors.METADATA.fetcher)
}
) {
CompositeBookInput(it)
}
}
@Data
public static class TargetOf_store implements View<BookStore> {
...省略其他成员...
companion object {
@JvmStatic
val METADATA: ViewMetadata<BookStore, TargetOf_store> =
ViewMetadata<BookStore, TargetOf_store>(
newFetcher(BookStore::class).by {
name()
website()
}
) {
TargetOf_store(it)
}
}
}
@Data
public static class TargetOf_authors implements View<Author> {
...省略其他成员...
companion object {
@JvmStatic
val METADATA: ViewMetadata<Author, TargetOf_authors> =
ViewMetadata<Author, TargetOf_authors>(
newFetcher(Author::class).by {
firstName()
lastName()
gender()
}
) {
TargetOf_authors(it)
}
}
}
}
这就是本文开头说DTO语言本质上是对象抓取器的另外一种表达方式的原因
使用自定义查询
- Java
- Kotlin
BookTable table = Tables.BOOK_TABLE;
List<Book> books = sqlClient
.createQuery(table)
.where(table.name().eq("GraphQL in Action"))
.orderBy(table.name())
.orderBy(table.edition().desc())
.select(
table.fetch(BookDetailView.class)
)
.execute()
val books = sqlClient
.createQuery(Book::class) {
where(table.name eq "GraphQL in Action")
orderBy(table.name)
orderBy(table.edition.desc())
select(
table.fetch(BookDetailView::class)
)
}
.execute()
这里,我们看到了,以前代码中的table.fetch(fetcher)
被替换成了table.fetch(BookDetailView.class)
。
所有底层查询API都可以用viewType
替换fetcher
,再次印证了本文开头所说DTO语言本质上是对象抓取器的另外一种表达方式。
关联属性特有配置
Jimmer DTO分为三种
- Output DTO
- Input DTO
- Specification DTO
本文所讨论的DTO,用于定义对象抓取器并将其查询结果类型映射为静态对象,属于Output DTO。
因此,本节所介绍的内容,是Output DTO特有的配置。
在对象抓取器/关联属性中,我们介绍了关联属性的一些特有配置。
- BatchSize
- 关联级分页
- 属性过滤器
- 引用抓取方式
在对象抓取器/递归查询中,我们介绍了
- 限制递归深度
- 控制每个节点是否递归
同理,作为类型抓取器更加类型化的体现,Output DTO也同样具备这些配置
BatchSize
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
!batch(2)
authors {
#allScalars
}
}
关联级分页
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
!limit(10, 90) // limit: 10, offset: 90
authors {
#allScalars
}
}
或
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
!limit(10) // limit: 10, offset: 0
authors {
#allScalars
}
}
属性过滤器
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
!where(firstName ilike '%a%' or lastName ilike '%a%')
!orderBy(firstName asc, lastName asc)
authors {
#allScalars
}
}
为了限制DTO语言的复杂度,!where
和!orderBy
故意限制了语法复杂度,以!where
中的条件表达式为例
- 支持
and
和or
,以及通过()
改变运算符优先级。 - 变量必须是关联实体 (本例中为
Author
) 中能映射到数据库列的属性,即,标量属性和关联id。如果属性是复合字段,可以实现用
.
链路引用内部属性,例如fullName.firstName
。 - 支持二元运算符
=
、<>
、!=
、<
、<=
、>
、>=
、like
、和ilike
,其中<>
和!=
等价。 - 支持一元云运算
is null
和is not null
。
显然,设计目的在于控制DTO语言的复杂性。如果要编写任意复杂的筛选和排序,请使用!filter
,如下:
-
首先,新建一个类。
- 对于Java而言,请实现
org.babyfish.jimmer.sql.fetcher.FieldFilter<T>
接口 - 对于Kotlin而言,请实现
org.babyfish.jimmer.sql.kt.fetcher.KFieldFilter<E>
接口
- Java
- Kotlin
AuthorFilter.javapackage com.yourcompany.yourpackage.strategy;
...省略import语句...
public class AuthorsPropFilter implements FieldFilter<AuthorTable> {
@Override
public void apply(FieldFilterArgs<AuthorTable> args) {
AuthorTable table = args.getTable();
args
.where(
Predicate.or(
table.firstName().ilike("a"),
table.lastName().ilke("b")
)
)
.orderBy(
table.firstName(),
table.lastName()
);
}
}AuthorFilterpackage com.yourcompany.yourpackage.strategy
...省略import语句...
class AuthorsPropFilter : KFieldFilter<Author> {
override fun KFieldFilterDsl<Author>.applyTo() {
where(
or(
table.firstName ilike "a",
table.lastName ilike "a",
)
)
orderBy(
table.firstName,
table.lastName
)
}
}可见,此类型允许我们使用完整的Jimmer DSL的能力,实现任意复杂的筛选和排序逻辑。
- 对于Java而言,请实现
-
在DTO声明中使用上述Java/Kotlin类
Book.dtoexport com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
import com.yourcompany.yourpackage.strategy.AuthorsPropFilter
BookDetailView {
#allScalars
!filter(AuthorsPropFilter)
authors {
#allScalars
}
}
引用抓取方式
BookDetailView {
#allScalars
!fetchType(JOIN_ALWASY)
store {
#allScalars
}
}
注意:和对象抓取器一样,只能为引用关联 (非集合关联,@ManyToOne
或@OneToOne
) 属性指定抓取方式
限制递归深度
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookDetailView {
#allScalars
!depth(2)
childNodes*
}
控制每个节点是否递归
-
首先,新建一个类,实现
org.babyfish.jimmer.sql.fetcher.RecursionStrategy<E>
接口- Java
- Kotlin
ChildNodesRecursionStrategy.javapackage com.yourcompany.yourpackage.strategy;
...省略import语句...
public class ChildNodesRecursionStrategy implements RecursionStrategy<TreeNode> {
@Override
public boolean isRecursive(Args<TreeNode> args) {
return !args.getEntity().name().equals("Clothing");
}
}ChildNodesRecursionStrategy.ktpackage com.yourcompany.yourpackage.strategy
...省略import语句...
class ChildNodesRecursionStrategy : RecursionStrategy<TreeNode> {
override fun isRecursive(args: RecursionStrategy.Args<TreeNode>): Boolean {
return args.entity.name != "Clothing"
}
} -
在DTO声明中使用上述Java/Kotlin类
Book.dtoexport com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
import com.yourcompany.yourpackage.strategy.ChildNodesRecursionStrategy
BookDetailView {
#allScalars
!recursion(ChildNodesRecursionStrategy)
childNodes*
}