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文件。默认情况下,文件路径和名称决定文件中声明
所使用的实体。单个声明也可以通过for选择其他实体,因此一个文件可以描述多个实体的DTO。
有关多目标文件、import、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 {
...略...
}
...省略其他成员...
}
5. allScalars
DTO语言支持一个特别的宏属性,#allScalars,表示映射实体中的所有标量属性
BookView {
#allScalars
}
按照官方例子中Book类型的定义
-
Book本身的标量属性有id、name、edition和price -
Book继承了TenantAware,TenantAware接口定义了属性tenant -
Book也继承了CommonEntity(多继承),CommonEntity接口定义了属性createdTime和modifiedTime
#allScalars表示自动映射实体所有标量属性,包括继承的。
如果这不是你所要的行为,那么
-
可以使用
allScalars(Book),表示只映射Book类型本身定义的所有标量属性,不包含继承的属性。 -
也可以使用
allScalars(TenantAware)和allScalars(CommonEntity)表示特定超类型的属性。
对于当前类型Book而言,#allScalars(Book)也可以写作#allScalars(this)。
事实上,#allScalars支持多个参数,举例如下
| 宏表达式 | 自动映射的属性 |
|---|---|
| #allScalars | createdTime, modifiedTime, tenant, id, name, edition, price |
| #allScalars(this) | id, name, edition, price |
| #allScalars(TenantAware) | tenant |
| #allScalars(CommonEntity) | createdTime、modifiedTime |
| #allScalars(this, TenantAware) | tenant, id, name, edition, price |
| #allScalars(this, CommonEntity) | createdTime、modifiedTime, id, name, edition, price |
| #allScalars(TenantAware, CommonEntity) | createdTime、modifiedTime, tenant |
| #allScalars(this, TenantAware, CommonEntity) | createdTime, modifiedTime, tenant, id, name, edition, price |
-
#allScalars宏可以出现在属性声明中的任意位置。 -
如果为
#allScalars宏指定参数,则每个参数必须为当前实体或其基类型,否则会导致编译报错。
6. 负属性
前文所讲的#allScalars宏,会批量化地让DTO映射多个属性。负属性可以去掉某些属性
BookView {
#allScalars
-tenant
}
和上面一样,如果按官方例子中Book类型的继承关系来理解的话,#allScalars会加入属性createdTime、modifiedTime、tenant、id、name、edition和price。
而-tenant从中减去了tenant属性,最终加入属性createdTime、modifiedTime、id、name、edition和price。
负属性指定的其实不是实体属性名,而是DTO中对应属性的名称,在这里二者相同,并无差异。
稍后的章节重命名会揭示二者区别。
不难发现,对于这个例子而言,还有一种与之等价的写法
BookView {
// 排除`TenantAware`
#allScalars(Book, CommonEntity)
}
7. 可空性
默认情况下,
-
specification中所有属性都默认可null -
否则,DTO属性的可空性和实体中原始属性的可空性一样。
我们可以采用问号?或!修饰DTO属性,改变DTO属性的可空性。
7.1. ?
我们可以采用?修饰DTO属性,让其可null。
如果实体中原属性已经可null,则会导致编译错误
例如
input UpdateBookInput {
#allScalars
price?
}
你甚至可以对allScalars采用?,让所有自动映射的非关联属性全部可以为null
input UpdateBookInput {
#allScalars?
}
-
Specification类型不允许使用
?注意由于
specification默认所有属性可以为null,所以在specification内部为属性指定修饰符?将会导致编译错误。 -
当实体的原属性不允许为null时
对于实体中的不允许为null原始属性而言,如果DTO对象的对应属性为null,那么将改DTO对象转化为实体对象后,实体对象中原始属性不会被赋值。
-
当实体的原属性允许为null时
提示对于原实体中允许为null的属性而言,如何通过Input DTO映射是一个复杂的话题,Jimmer提供全面和强大的支持。
限于篇幅问题,这个问题单独形成一篇文章,请参见修改篇/保持指令/Input DTO/处理空值。
7.2. !
我们可以采用!修饰DTO属性,让其非null。
如果实体中原属性已经非null,则会导致编译错误
其使用场景受限,只能用于以下三种情况。
-
在
input类型中修饰id属性如果id属性被配置自动增长策略,那么对应的input DTO类型将会把id属性设置为可null。
然而,这种行为并不总是符合用户预期,可以在
input类型中使用!修饰id属性,让其非null,例如Book.dtoinput BookUpdateInfo {
#allScalars
id!
}注意如果用
?修饰其它属性,将会导致编译错误 -
如果
input被unsafe关键字修饰,则可以将任何可null的实体属性转化为非null,例如Book.dtounsafe input BookUpdateInfo {
#allScalars
store! {
...略...
}
}信息对于一个实体对象而言,如果改属性对应的值为null,在使用Input DTO的构造方法将其转化为Input DTO对象是,由于DTO中对应的属性不允许为null,将会导致异常。
这就是
unsafe关键字的意义。 -
对于
specification而言,由于所有属性都被默认为null,所以可以将任何可null的实体属性转化为非null,例如Book.dtospecification BookSpecification {
#allScalars
edition!
}
8. 重命名
可以使用as关键字为属性设置别名,让实体属性名和DTO属性名不同。
as有两种用法,既可以精确地为一个属性设置别名,也可以模糊地为多个属性设置别名
8.1. 重命名单个属性
BookView {
name as bookName
}
8.2. 重命名多个属性
假设有一个实体类型叫做Robot
OrderView {
as (^ -> oldSystem) { ❶
prop1
prop2
}
as ($ -> ForNewSystem) { ❷
prop3
prop4
}
}
其中
-
❶ 为
prop1和prop2添加前缀oldSystem,其中^表示起始位置 -
❷ 为
prop3和prop4添加后缀ForNewSystem,其中$表示结尾位置
生成的RobotView类如下
- Java
- Kotlin
public class RobotView implements View<Robot> {
private String oldSystemProp1;
private String oldSystemProp2;
private String prop3ForNewSystem;
private String prop4ForNewSystem;
...省略其他成员...
}
open class RobotView(
val oldSystemProp1 = "",
val oldSystemProp2 = "",
val prop3ForNewSystem = "",
val prop4ForNewSystem = ""
) : View<Robot> {
...省略其他成员...
}
可以在as() {...}块中定义绝大部分属性,当然包括#allScalars,例如
RobotView {
as(^ -> robot) {
#allScalars
}
}
下面罗列as() {...}块的所有用法
| 代码示范 | 作用描述 |
|---|---|
| as(^ -> prefix) | 添加前缀 |
| as(^prefix ->) | 删除前缀 |
| as(^prefix -> newPrefix) | 替换前缀 |
| as($ -> suffix) | 添加后缀 |
| as($suffix ->) | 删除后缀 |
| as($suffix -> newSuffix) | 替换后缀 |
| as(infix -> ) | 删除任何位置的内容 |
| as(infix -> newInfix) | 替换任何位置的内容 |
^和$不能同时出现,因为这表示精确重命名单个属性,这和已有的功能重复
9. 枚举映射
默认情况下,实体的枚举类型属性被映射成DTO属性后仍然是枚举类型。
你可以将枚举类型映射为数字或字符串。
-
映射为数字
AuthorView {
#allScalars
gender -> {
MALE: 100
FEMALE: 101
}
}生成如下代码
- Java
- Kotlin
BookView.javapublic class AuthorView implements View<AuthorView> {
private int gender;
...省略其他成员...
}BookView.ktopen class RobotView(
val gender: Int,
...省略其他成员...
) : View<Robot> {
...省略其他成员...
} -
映射为字符串
AuthorView {
#allScalars
gender -> {
MALE: "Male"
FEMALE: "Female"
}
}生成如下代码
- Java
- Kotlin
BookView.javapublic class AuthorView implements View<AuthorView> {
private String gender;
...省略其他成员...
}BookView.ktopen class RobotView(
val gender: String,
...省略其他成员...
) : View<Robot> {
...省略其他成员...
}
10. 关联属性
对关联属性的处理是DTO语言一个非常强大的功能,存在四种用法
- 直接使用
- 递归关联
- 调用id函数
- 调用flat函数
10.1. 普通关联
-
基本用法
Robot.dtoinput CompositeBookInput {
#allScalars(Book)
store {
#allScalars(BookStore)
-id
}
authors {
#allScalars(Author)
-id
}
}生成的代码为
- Java
- Kotlin
CompositeBookInput.javapublic class CompositeBookInput implements Input<Book> {
@Nullable
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 String name;
@Nullable
private String website;
...省略其他成员...
}
public static class TargetOf_authors implements Input<Author> {
private String firstName;
private String lastName;
private Gender gender;
...省略其他成员...
}
...省略其他成员...
}CompositeBookInput.ktopen class CompositeBookInput(
val id: Long? = null,
val name: String = "",
val edition: Int = 0,
val price: BigDecimal,
val store: TargetOf_store? = null,
val authors: List<TargetOf_authors> = emptyList(),
) : Input<Book> {
open class TargetOf_store(
val name: String = "",
val website: String? = null,
) : Input<BookStore> {
...省略其他成员...
}
open class TargetOf_authors(
public val firstName: String = "",
public val lastName: String = "",
public val gender: Gender,
) : Input<Author> {
...省略其他成员...
}
...省略其他成员...
} -
重命名
如果要对关联属性进行重命名,
as子句应该在关联定义块之前,例如Book.dtoinput CompositeBookInput {
authors as authorList {
...略...
}
...略...
}
配置Output View的关联加载
Output DTO可以配置每个关联的加载方式。配置指令需要紧挨着写在关联属性之前:
BookDetailView {
#allScalars
// 在根SQL语句中加载该引用关联
!fetchType(JOIN_ALWAYS)
store {
id
name
}
// 配置该集合的二次查询
!where(firstName ilike '%a%' or lastName ilike '%a%')
!orderBy(lastName asc, firstName asc)
!batch(32)
!limit(20)
authors {
id
firstName
lastName
}
}
-
!fetchType(SELECT | JOIN_IF_NO_CACHE | JOIN_ALWAYS)控制引用关联的加载方式。JOIN_ALWAYS对non-null引用使用inner join,对nullable引用使用left join;DTO不会独立 指定SQL join type。 -
!where(...)使用简单谓词过滤关联行。 -
!orderBy(...)、!limit(limit, offset)和!batch(size)用于配置集合加载, 其中offset可以省略。 -
需要任意Jimmer DSL逻辑时,可以用
!filter(FilterType)代替!where和!orderBy。 递归关联还支持!depth和!recursion。
这些指令影响生成的fetcher,而不影响生成的DTO数据类,并且仅适用于output DTO。 完整语法以及自定义filter/recursion的实现方式请参阅 Output DTO关联配置。
10.2 递归关联
对于实体中可递归的属性,比如,附带例子中的TreeNode.parent或TreeNode.childNodes,可以使用星号*将DTO对应的关联属性标记为递归属性。
TreeNodeView {
#allScalars
childNodes*
}
10.3. id函数
对于
而言, 虽然我们当然选择在实体中定义@IdView属性并在DTO中简单地引用它们, 但是,我们不能总是寄希望于实体类型总是声明了@IdView属性,更好的方案不应该有此假设。因此,DTO语言支持对关联属性调用id函数来达到同样目的:
BookView {
id(store)
id(authors) as authorIds
...略...
}
这段代码的功能和前面的例子完全一样,但不再假设实体类型总是声明了@IdView属性,是更好的方案。
10.4. flat函数
-
对view和input而言,
flat函数只能用于引用关联 (一对一或多对一),不能用于集合关联 (一对多或多对多),用于把关联DTO的属性提升到当前DTO类型,得到不含关联的平坦对象。 -
对于specification而言,
flat函数没有上述限制,可以用于集合关联。请查看 @Super QBE。
以官方例子中的TreeNode实体为例 (事实上,这个例子是错误的,我们稍后会讨论这个问题)
FlatTreeNodeView {
#allScalars(TreeNode)
flat(parent) {
#allScalars(TreeNode)
}
}
如上文所述,这个例子是错误的,因为聚合根和关联对象都有属性id和name,无条件把关联对象的属性往聚合根上提取,必然导致名字冲突,最终导致DTO语言编译报错。
正确的做法,是和用户多属性模糊重命名的as(...) {}块结合使用。让我们来看一个新的例子
FlatTreeNodeView {
#allScalars(TreeNode) // id, name
flat(parent) {
as(^ -> parent) {
// parentId, parentName
#allScalars(TreeNode)
}
}
}
这样,我们得到了一个平坦的DTO类型,具有4个属性:id、name、parentId和parentName。
甚至还可以嵌套使用flat函数,将多级关联转化为平坦对象
FlatTreeNodeView {
#allScalars(TreeNode)
flat(parent) {
as(^ -> parent) {
#allScalars(TreeNode)
}
flat(parent) {
as(^ -> grandParent) {
#allScalars(TreeNode)
}
}
}
}
这样,我们得到了一个平坦的DTO类型,具有6个属性:id、name、parentId、parentName、grandParentId、grandParentName。
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(cola -> {
cola.setId(4L);
cola.setName("Coca cola");
cola.applyParent(drinks -> {
drinks.setId(3L);
drinks.setName("Drinks");
drinks.applyParent(food -> {
food.setId(2L);
food.setName("Food");
})
})
});
// 将层次化的Entity转化为扁平的DTO
FlatTreeNodeView view = new FlatTreeNodeView(treeNode);
System.out.println(view);
val treeNode = TreeNode {
id = 4L
name = "Coca cola"
parent {
id = 3L
name = "Drinks"
parent {
id = 2L
name = "Food"
}
}
}
// 将层次化的Entity转化为扁平的DTO
val view = FlatTreeNodeView(treeNode)
println(view)
打印结果为 (为了方便阅读,这里进行了格式化)
com.yourcompany.yourproject.FlatTreeNodeView(
id = 4,
name = Coco cola,
parentId = 3,
parentName = Drinks,
grandParentId = 2,
grandParentName = "Food"
)
flat方案也有缺点,对**output ** DTO而言,flat方案是有争议的,并不推荐盲目采用。
请参见
10.5. fold函数
fold是flat的结构逆操作。
-
flat是把嵌套对象展开到当前DTO。 -
fold是把当前层级的多个属性重新分组为一个嵌套DTO对象。
最简单的例子如下:
BookFoldView {
id
fold(summary) {
name
edition
}
}
这个DTO不再把name和edition暴露为顶层属性,而是通过一个嵌套属性summary来表达它们。
- Java
- Kotlin
Book book = Immutables.createBook(draft -> {
draft.setId(10L);
draft.setName("GraphQL in Action");
draft.setEdition(3);
});
BookFoldView view = new BookFoldView(book);
System.out.println(view);
val book = Book {
id = 10L
name = "GraphQL in Action"
edition = 3
}
val view = BookFoldView(book)
println(view)
打印结果大致如下:
BookFoldView(
id = 10,
summary = BookFoldView.Summary(
name = GraphQL in Action,
edition = 3
)
)
默认情况下,fold后的嵌套属性是非null的。如果需要可空对象,可以使用?:
BookNullableFoldView {
id
fold(summary)? {
name
edition
}
}
和flat一样,fold也可以嵌套:
BookNestedFoldView {
id
name
fold(summary) {
name
fold(detail) {
name
edition
}
}
}
它也可以和flat组合使用:
BookFoldInsideFlatView {
id
flat(store) {
fold(key) {
name
}
}
}
此时,嵌套对象仍然会被flat提升,因此生成的属性名会变成storeKey。
fold支持view、input和specification。
-
对
input而言,调用toEntity()时,fold中的属性会被重新拆回当前实体,不会额外创建一层实体结构。 -
对
specification而言,fold只是DTO结构工具,用于分组查询参数,并不会改变底层查询语义。