跳到主要内容

DTO组合

DTO语言提供两种互补的组合机制:

机制用途是否生成类型
复用DTO将已有DTO用作关联属性的具体类型
片段向当前DTO添加一组可复用属性

它们解决的问题不同。复用DTO会保留对象边界,而片段只向正在编译的DTO贡献属性。

复用DTO类型

通常,带有内联代码块的关联属性会生成一个嵌套DTO类型:

BookView {
store {
id
name
}
}

如果多个DTO需要相同的关联对象结构,可以只声明一次,然后在->之后引用它:

package com.example.api.dto

import com.example.model.*

StoreView for BookStore {
id
name
}

AuthorView for Author {
id
firstName
lastName
}

BookView for Book {
id
name
store -> StoreView
authors -> AuthorView
}

生成的BookView.store属性类型是StoreViewBookView.authors属性类型是 List<AuthorView>。Jimmer会复用被引用DTO的抓取器和转换元数据,而不会为这些属性 生成TargetOf_storeTargetOf_authors类。

复用DTO也可以和别名一起使用:

BookView for Book {
store as storeInfo -> StoreView
}

关联本身仍然决定生成的属性是单值还是列表。现有的?!规则继续控制关联边界的 可空性。

输出和输入DTO

输出DTO中的关联必须引用兼容的输出View,输入DTO中的关联必须引用Input DTO:

input StoreInput for BookStore {
id
name
}

input BookInput for Book {
id
name
store -> StoreInput
}

关联目标实体和被引用DTO所表示的实体必须完全一致。例如,指向BookStore的关联不能 引用无关类型、基类型或子类型的DTO。输出View也不能作为可写的输入类型。

对于Input DTO,父关联保留自己的输入策略和存在性语义;被引用的Input DTO负责其 内部字段以及到不可变对象的转换。

Specification

Specification也支持相同的关联复用语法:

fragment AuthorFilters for Author {
like/i(firstName, lastName) as name
}

specification StoreSpecification for BookStore {
ge(name)
le(name)
}

specification AuthorSpecification for Author {
#include(AuthorFilters)
}

specification BookSpecification for Book {
like/i(name)
store -> StoreSpecification
authors -> AuthorSpecification
}

生成的BookSpecification会直接使用StoreSpecificationAuthorSpecification,而不会生成TargetOf_storeTargetOf_authors。 单值和列表关联都使用一个嵌套Specification对象:关联基数决定如何应用其中的谓词, 而不改变生成的属性类型。现有查询语义保持不变,例如单值关联使用join,列表关联使用 exists谓词。

被引用的Specification必须表示关联的准确目标类型。它可以使用显式的for目标,也可以 来自已编译的依赖。片段仍然不绑定DTO种类,因此可以导出likegele等函数; 这些声明会在片段被Specification包含时按Specification规则校验。

关联配置

被引用DTO定义关联对象的结构,而抓取配置仍属于使用它的关联:

BookView for Book {
!fetchType(JOIN_ALWAYS)
store -> StoreView

!batch(32)
!orderBy(lastName asc, firstName asc)
authors -> AuthorView
}

!fetchType!batch!where!orderBy等配置不会被复制到StoreViewAuthorView中。

多态和模块边界

通过#types生成的多态View或Input DTO,可以通过其生成的根DTO类型被复用。分支选择、 抓取和转换仍由该DTO的生成元数据处理,单值关联和列表关联都支持这种用法。

可复用DTO也可以来自已编译的依赖模块,像普通类型一样导入生成的DTO即可。其公开的 View<T>Input<T>契约以及生成元数据足以支持复用。

直接或间接的DTO复用环会导致元数据循环初始化,因此会在编译期被拒绝。递归对象图 应继续使用DTO语言已有的显式递归关联功能。

片段

片段是一个在源码层面命名的DTO属性集合:

fragment BookSummaryFields for Book {
id
name
edition
}

BookSummaryView for Book {
#include(BookSummaryFields)
}

BookDetailView for Book {
#include(BookSummaryFields)
price
store -> StoreView
}

#include可以出现在DTO或另一个片段代码块的任意位置。片段不会生成Java/Kotlin类、 运行时元数据、转换器或API Schema;它只把最终属性声明贡献给包含它的DTO。

片段可以使用常规的、能够产生属性的DTO语法,包括:

  • 显式属性、别名、注解和自定义属性
  • #allScalars等宏
  • 内联关联、复用DTO关联以及关联配置
  • flatfold和递归关联
  • 其他片段

片段内部不允许使用#types,因为它定义的是生成的DTO类型层次,而不是属性集合。

片段本身不声明它属于View、Input还是Specification。片段中的属性会按照包含它的具体DTO 类型进行校验。因此,只要其中的所有声明对相应DTO类型都合法,同一个片段就可以被多种 DTO类型使用。

增量组合和排除属性

每个片段会先解析自己的宏、所包含的片段、正向属性和排除属性,然后导出最终的有序 属性集合:

fragment BookScalars for Book {
#allScalars
-price
}

BookView for Book {
#include(BookScalars)
price
}

BookScalars不会导出price,所以BookView可以重新添加它。包含片段的DTO也可以 通过排除属性删除由片段或宏提供的属性:

BookView for Book {
#include(BookScalars)
-edition
}

片段之间的组合是增量式的。片段只导出自己的最终属性,不能删除兄弟片段提供的属性。

冲突

两个来源不能产生相同的最终DTO属性名:

fragment A for Book {
name
}

fragment B for Book {
name
}

BookView for Book {
#include(A)
#include(B) // 编译错误:重复的属性别名“name”
}

即使两个声明的结构完全相同,或者它们来自同一个公共片段,这仍然是错误。根DTO中的 排除属性也不能隐藏这种冲突,因为冲突检查发生在应用排除属性之前。

在组合边界上,关联属性是不可分割的。即使嵌套结构相同,两个片段只要都导出store 就会冲突,它们的嵌套属性不会被递归合并。

目标和可见性

片段按照它声明的实体目标进行解析。它可以被同一类型的DTO包含,也可以被继承了相应 映射属性的子类型DTO包含;基类型DTO不能包含为子类型声明的片段。

片段只存在于源码中,只能在当前DTO编译中复用。可以通过同包名称、import或全限定名 引用片段。与生成的DTO类型不同,片段不能从已编译的依赖模块中导入。

片段包含环是编译错误,错误信息会给出包含路径。

多目标DTO文件

文件名或export语句仍然为没有for的声明提供默认实体目标。单个DTO或片段可以覆盖 这个目标:

Book.dto
import com.example.model.Author

BookView {
id
name
}

AuthorView for Author {
id
firstName
lastName
}

如果每个声明都明确指定了目标,文件可以完全没有默认目标。这很适合把相关DTO放在同一个 生成包中:

Catalog.dto
package com.example.api.dto

import com.example.model.*

StoreView for BookStore {
id
name
}

BookView for Book {
id
name
store -> StoreView
}

package语句指定生成DTO类型所在的包,同时也是解析DTO和片段名称时的默认命名空间。 同一生成包中的声明无需import即可互相引用。DTO语言支持显式、分组、别名和通配符import; 如果多个通配符匹配产生歧义,编译会失败。

如果使用处理器选项jimmer.source.includesjimmer.source.excludes,过滤会应用到 每个DTO和片段的实际实体目标,包括通过for指定的目标。includes是白名单,随后 excludes优先生效。多个包或类型前缀可以用逗号或分号分隔。

发布DTO源码Bundle

库可以只把.dto源码作为资源发布,由每个使用方生成自己的Java或Kotlin DTO类。 这个资源构件不需要包含预编译的DTO类。

在构件中加入以下marker:

META-INF/jimmer/dto-bundle.properties

例如,Gradle生产方可以把src/main/dto作为资源根目录打包:

sourceSets {
main {
resources.srcDir("src/main/dto")
}
}

把marker放在 src/main/resources/META-INF/jimmer/dto-bundle.properties

空marker表示DTO根目录就是classpath根目录。例如,DTO源码可以在构件中保存为 com/example/model/Book.dto。如果构件使用其他根目录,请指定相对资源路径:

path=jimmer-dto

目前只支持path属性。Jimmer通过精确的classpath名称查找marker,然后只扫描已标记 构件中的.dto文件。普通JAR以及Gradle或IDE产生的展开class/resource目录都受支持。

在使用方把DTO构件加入处理器classpath:

Kotlin/KSP
dependencies {
implementation("com.example:catalog-model:1.0")
ksp("com.example:catalog-dto:1.0")
}
Java/APT
dependencies {
implementation("com.example:catalog-model:1.0")
annotationProcessor("com.example:catalog-dto:1.0")
}

DTO源码引用的不可变模型类型仍必须位于普通编译classpath。DTO bundle依赖只向APT或 KSP提供源码声明。

默认启用bundle发现。可以在不影响本地DTO目录的情况下将其关闭:

Kotlin/KSP
ksp {
arg("jimmer.dto.bundle.enabled", "false")
}
Java/APT
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("-Ajimmer.dto.bundle.enabled=false")
}

如何选择

如果关联对象应具有命名的公开类型,以及独立的元数据和转换契约,请复用DTO。如果多个 DTO需要相同属性,但仍应是彼此独立的生成类型,请使用片段。

两种机制都不会引入DTO类继承:复用DTO保留嵌套对象边界,而片段把声明展开到当前DTO中。