查询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
下,按实体类型所处的包路径建立子目录com/yourcompany/yourproject/model
-
在上一步建立的目录下,建立文件
Book.dto
,文件必须和实体类同名,扩展名必须为dto
-
编辑此文件,利用DTO语言,定义Book实体的各种DTO形状
Book.dtoBookDetailView {
#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类型
查询DTO类型的方案有多种 :
-
使用从
JRepository/KRepository
中继承的方法 -
在自定义Repository中定义抽象方法
-
在自定义Repository中定义默认方法 (其实也是底层API查询DTO类型的方法)
使用从Repository继承的方法
- Java
- Kotlin
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(MyApp.class, args);
BookRepository bookRepository = ctx.getBean(BookRepository.class);
Book view = bookRepository
.viwer(BookDetailView.class)
.findNullable(1L);
System.out.println(view);
}
fun main(args: Array<String>) {
val ctx = runApplication<MyApp>(*args)
val bookRepository = ctx.getBean(BookRepository.class.java)
val view = bookRepository
.viwer(BookDetailView.class)
.findNullable(1L)
println(view)
}
其中,viewer(BookDetailView.class)
表示接下来的方法用于查询DTO对象,而非动态实体对象。
打印结果如下 (为方便阅读,人为进行了格式化)
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
)
]
)
不难发现,虽然现在查询不再返回动态实体对象,但功能和却和对象抓取器完全一样。这什么为什么呢?
其实在BookDetailView
类内部包含一个自动生成的对象抓取器,Jimmer靠它从数据库中查询了形状匹配的动态实体对象,并将之自动转化为DTO 对象。
相关细节在对象抓取器/DTO查询中已有详细论述,本文不再重复。
这就是本文开头所说的,DTO语言本质上是对象抓取器的另外一种表达方式。
在自定义Repository中定义抽象方法
有两种实施方案
-
直观但不推荐的方案
- Java
- Kotlin
BookRepository.java// 直观但不推荐的方案
public interface BookRepository extends JRepository<Book, Long> {
List<BookDetailView> findByNameLikeIgnoreCase(
@Nullable String name
);
}BookRepository.kt// 直观但不推荐的方案
interface BookRepository : KRepository<Book, Long> {
fun findByNameLikeIgnoreCase(
name: String? = null
) : List<BookDetailView>
}这种方案很简单,抽象方法不再返回代表任意数据的动态实体,直接返回固定形状的DTO类型即可。非常容易理解。
警告然而,这种方案存是有缺点的,并不推荐。
-
推荐做法
上面的代码虽然简单直观,但是违背了Jimmer一直努力追求重要价值观之一
提示不要在数据层固化被查询的数据结构的形状,而应该由上层业务来决定。
- Java
- Kotlin
BookRepository.javapublic interface BookRepository extends JRepository<Book, Long> {
<V extends View<Book>> List<V> findByNameLikeIgnoreCase(
@Nullable String name,
Class<V> viewType
);
}BookRepository.ktinterface BookRepository : KRepository<Book, Long> {
fun <V: View<Book>> findByNameLikeIgnoreCase(
name: String? = null
viewType: KClass<V>
) : List<V>
}可以看到
-
首先,定义一个方法级的范型参数
V
,该范型参数必须继承org.babyfish.jimmer.View<Book>
(这非常重要,否则Jimmer会通过异常提醒开发人员这样做)。 -
然后,利用
V
声明类型为Class<V>
或KClass<V>
的参数viewType
,将数据结构形状的决策权交给调用者。 -
最后,返回
V
、List<V>
或Page<V>
这类携带V
类型的结果。
这样,我们就可以用它来查询各种形状的数据结构,以Java为例
-
bookRepository.findByNameLikeIgnoreCase(null, BookDetailView.class)
-
bookRepository.findByNameLikeIgnoreCase(null, SimpleBookView.class)
-
bookRepository.findByNameLikeIgnoreCase(null, DefaultBookView.class)
让我们来回忆一下,在我们介绍DTO类型之前,我们是如何直接使用对象抓取器来实现相同功能的
- Java
- Kotlin
BookRepository.javapublic interface BookRepository extends JRepository<Book, Long> {
List<Book> findByNameLikeIgnoreCase(
@Nullable String name,
fetcher<Book> fetcher
);
}BookRepository.ktinterface BookRepository : KRepository<Book, Long> {
fun findByNameLikeIgnoreCase(
name: String? = null
viewType: Fetcher<Book>
) : List<Book>
}提示对比使用DTO的代码和直接使用对象抓取器的代码,不难发现,二者异曲同工。
这再次印证了本文开头所说DTO语言本质上是对象抓取器的另外一种表达方式。
在自定义Repository中定义默认方法
- Java
- Kotlin
public interface BookRepository extends JRepository<Book, Long> {
BookTable table = Tables.BOOK_TABLE;
default <V extends View<Book>> List<V> find(
@Nullable String name,
@Nullable String storeName,
@Nullable String authorName,
Class<V> viewType
) {
return sql()
.createQuery(table)
whereIf(
StringUtils.hasText(name),
table.name().ilike(name)
)
.whereIf(
StringUtils.hasText(storeName),
table.store().name().ilike(storeName)
)
.whereIf(
StringUtils.hasText(authorName),
table.id().in(
sql()
.createSubQuery(author)
.where(
Predicate.or(
author.firstName().ilike(authorName),
author.lastName().ilike(authorName)
)
)
.select(
author.books().id()
)
)
)
.orderBy(table.name())
.orderBy(table.edition().desc())
.select(table.fetch(viewType))
.execute();
}
}
interface BookRepository : KRepository<Book, Long> {
fun <V: View<Book>> findByNameLikeIgnoreCase(
name: String? = null
viewType: KClass<V>
) : List<V> =
sql
.createQuery(Book::class) {
name?.takeIf { it.isNotEmpty() }?.let {
where(table.name ilike it)
}
storeName?.takeIf { it.isNotEmpty() }?.let {
table.store.name ilike it
}
authorName?.takeIf { it.isNotEmpty() }?.let {
where(
table.id valueIn subQuery(Author::class) {
where(
or(
table.firstName ilike it,
table.lastName ilike it
)
)
select(table.books.id)
}
)
}
orderBy(table.name)
orderBy(table.edition.desc())
select(table.fetch(viewType))
}
.execute()
}
这里,我们看到了,以前代码中的table.fetch(fetcher)
被替换成了table.fetch(viewType)
。
其实,其他底层查询API也可以用viewType
替换fetcher
。比如sqlClient.findById(fetcher, 1L)
可以被替换为sqlClient.find(viewType, 1L)
。
所有底层查询API都可以用viewType
替换fetcher
,再次印证了本文开头所说DTO语言本质上是对象抓取器的另外一种表达方式。