DTO语言
1. 概念
1.1. 痛点
Jimmer提供动态实体,可以很好地解决很大一部分DTO爆炸问题。所以,一般情况下不需要定义输出型的DTO类型来表达查询返回结果。
然而,并非所有DTO类型都可以被消灭,其中,输入型的DTO对象很难去除。
以GraphQL为例,虽然从output的角度讲,为客户端返回动态的
GraphQLObject
数据;但是,从input的角度讲,接受客户端提交的静态的GraphQLInput
数据。GraphQL协议为什么将
GraphQLInput
定义为静态类型呢?是因为API的明确性和系统的安全性是非常重要需求,请参考动态对象作为输入参数的问题。GraphQL协议面对的问题,Jimmer也同样需要面对,必须给出完整的解决方案。
DTO语言是为了无法被消灭的那部分DTO类型而设计,目的是为了它们变得极其廉价。
1.2. 方案
作为一个综合性解决方案,Jimmer不局限于ORM本身,而是为整个项目的考虑,为解决此问题,提供了DTO语言。
DTO语言是Jimmer提供的一个非常强大的功能,是一个和对象抓取器高度类似的概念,但在编译过程中位于更早的阶段。
DTO语言用于快速定义数据结构的形状,根据这些形状,DTO可以在编译时
-
生成特定数据结构所对应的静态DTO类型
-
生成DTO静态对象和Jimmer动态对象之间的相互转换逻辑
-
生成与DTO形状定义完全契合的对象抓取器
使用DTO语言快速构建DTO类型,是为Jimmer量身设计的方案,开发效率极高,远快于使用mapstruct,是优先推荐的方式。
1.3. 应用场景
DTO语言的应用场景
2. 创建文件
DTO语言的代码体现为扩展名为dto
的文件,一旦编译完成,这些文件就没有价值了。所以,这些文件不能放到src/main/resources
中打包,而是放到src/main/dto
目录中。
因此,第一件事,是在src/main
下建立dto
子目录。
Jimmer不要求src/main/dto
目录必须在实体类型所在的项目中。事实上,你可以在任何能合法访问实体的项目中定义创建此目录。
Jimmer只要求在包含dto文件的项目中使用jimmer-apt
或jimmer-ksp
,它们负责DTO文件的编译和相关代码的生成。
对于Java项目而言,除了
jimmer-apt
外,有可能还还需要多一个额外的配置,请参见注意事项
2.1. 两种创建文件的方法
src/main/dto
目录下可以定义若干个dto文件,每一个文件和一个原始实体相互对应。
假设存在一个Jimmer实体类型,其完整的类型名为com.yourcompany.yourproject.Book
,该类被@org.babyfish.jimmer.sql.Entity
修饰 (DTO语言只支持Entity类型),
有两种方法建立dto文件。
-
不使用
export
语句这种情况下,dto文件的目录需要和原始实体的包对应,名称需要和原始实体的名称对应:
-
在
src/main/dto
目录下建立目录com/yourcompany/yourproject
,即,建立和包名一致的目录 -
在上一步建立的目录中新文件
Book.dto
,该文件必须和类同名,且扩展名为dto
-
-
使用
export
语句语句很重要,我们单独用一个小结来讲解。
2.2. export语句
这种情况下,dto文件目录和名称是随意的,因为我们会在文件内部使用export
语句定义dto文件和哪个原始实体对应。
-
由于对dto文件的目录没有要求,建议直接在
src/main/dto
下建立dto文件 -
虽然对dto文件的名称也没有要求,但是为了项目的可维护性,仍然建议文件名采用原始实体的名称,这里,就是
Book.dto
-
dto文件的第一行代码为
export
语句export com.yourcompany.yourproject.Book
...后续代码...
DTO文件被编译后,将自动生成更多的Java/Kotlin类型,它们默认的包名为:实体包名
+ .dto
。
如果你使用了export
语句,你可以进一步定义生成的代码所在的包,例如:
export com.yourcompany.yourproject.Book
-> package com.yourcompany.yourproject.mydto
用户可以编辑Book.dto
文件,定义任意个以Book
类型为聚合根的所有DTO类型。这里,我们先定一个DTO类型:
...省略export(如果有的话)...
BookView {
...略...
}
DTO文件的第一行代码可能是export
语句,为简化本文,后文不再写出。
编译后会生成Java/Kotlin类型BookView
,假设生成代码所在包的默认值没有别修改,生成代码如下
- Java
- Kotlin
package com.yourcompany.yourproject.dto;
import com.yourcompany.yourproject.Book;
import org.babyfish.jimmer.View;
public class BookView implements View<Book> {
...略...
}
package com.yourcompany.yourproject.dto
import com.yourcompany.yourproject.Book
import org.babyfish.jimmer.View
open class BookView(
...略...
) : View<Book> {
...略...
}
2.3. 注意事项
-
对于Java项目而言 (kotlin开发者请忽略):
如果当前项目并非定义实体的项目,则需要在当前项目随意找一个类,用
@org.babyfish.jimmer.sql.EnableDtoGeneration
修饰。否则,DTO文件不会被编译。
-
dto文件由Jimmer的Annotation Processor (Java) 或 Ksp (Kotlin) 编译。
因此,如果正在使用诸如Intellij这样的IDE开发项目,那么
-
如果除了dto文件外还有其他Java/Kotlin文件被修改了,直接点击IDE中运行按钮可以导致dto文件的重新编译
-
但是,如果除了dto文件外没有其他Java/Kotlin文件被修改,简单地点击IDE中运行按钮并不会导致dto文件被重新编译,除非显式地rebuild!
-
如果你使用的构建工具是Gradle,也可以使用社区提供的第三方Gradle插件来解决这个问题: jimmer-gradle
-
3. view、input和specification
前文提到,DTO语言有三种使用场景
所以,DTO语言可以定义三种DTO
-
view: 既不使用
input
关键字也不使用specification
关键字,可以被理解为Output DTO。 -
input: 使用
input
关键字声明,可以被理解为Input DTO。 -
specification: 使用
specification
关键字声明,本身和DTO关系不够大,但可以用于做查询参数,支持超级QBE查询。
BookView {
...略...
}
AnotherBookView {
...略...
}
input BookInput {
...略...
}
input AnotherBookInput {
...略...
}
specification BookSpecification {
...略...
}
specification AnotherBookSpecification {
...略...
}
这表示
-
BookView
和AnotherBookView
用作查询输出,生成的Java/Kotlin类型会实现org.babyfish.jimmer.View<E>
接口备注建议输出DTO以
View
结尾 -
BookInput
和AnotherBookInput
用作保存指令输入,生成的Java/Kotlin类型会实现org.babyfish.jimmer.Input<E>
接口备注建议输入DTO以
Input
结尾 -
BookSpecification
和AnotherBookSpecification
用作查询参数,生成的Java/Kotlin类型会实现org.babyfish.jimmer.Specification<E>
接口备注建议查询参数DTO以
Specification
结尾
3.1 view和input共有的功能
对于view和input而言,其生成的Java/Kotlin类型可以和实体相互转化,具备如下功能
-
以原始实体类型为参数的构造方法:将Jimmer动态实体对象转化为静态DTO对象
-
toEntity()
:将静态DTO对象转化为Jimmer动态实体对象
以BookView
为例
- Java
- Kotlin
Book entity = ...略...
// 实体 -> DTO
BookView dto = new BookView(entity);
// DTO -> 实体
Book anotherEntity = dto.toEntity();
val entity: Book = ...略...
// 实体 -> DTO
val dto = BookView(entity)
// DTO -> 实体
val anotherEntity: Book = dto.toEntity()
3.2 input特有功能
和Output DTO相比,Input DTO存在如下不同
-
如果实体id属性配置了自动增长策略,那么input DTO中的id属性是nullable的。
信息如此设计的原因在于,当实体的id属性具备自动增长策略时,保存对象就不一定需要id属性。
然而,这并非表示Jimmer 会如同以JPA为代表的其他ORM一样,简单地认为认为没有id属性表示insert操作而有id属性表示update操作。
Jimmer在这方面有更智能的策略,请参考保存指令/保存模式,本文不再赘述。
如果不接受这种默认行为,开发人员也可以按照一下两种方式之一编写DTO代码
-
让DTO类型根本没有id属性
input BookInput {
#allScalars(this)
-id
} -
让DTO类型的id属性不能为null
input BookInput {
#allScalars(this)
id!
}
-
-
input DTO中只能定义可以保存的属性,如简单属性、普通ORM关联属性和id-view属性。 不能定义无法保存的属性,如transient属性、公式属性、计算属性和远程关联,否则会导致编译错误。
-
input DTO对nullable属性有强大的全面的支持
提示对于原实体中允许为null的属性而言,如何通过Input DTO映射是一个复杂的话题,Jimmer提供全面和强大的支持。
3.3 specification特有功能
specification
和input
的作用类似,用于修饰输入类型,但specification
不提供和实体对象相互转化的能力,而是被用作支持超级QBE查询。
4. 简单属性
可以为DTO类型属性,用于映射原始实体类型中属性,例如
BookView {
id
name
edition
}
这表示,DTO只映射实体中的三个属性:id
、name
和edition
,如下
- Java
- Kotlin
public class BookView implements View<Book> {
private long id;
private String name;
private String edition;
public BookView(Book book) {
...略...
}
@Override
public Book toEntity() {
...略...
}
...省略其他成员...
}
open class BookView(
val id: Long = 0,
val name: String = "",
val edition: Int = 0
) : View<Book> {
constructor(book: Book): this(...略...)
override fun toEntity(): Book {
...略...
}
...省略其他成员...
}