跳到主要内容

使用DTO语言

Jimmer提供了DTO语言

开发人员可以使用此语言快速定义要保存的Input DTO的形状,编译后

  • 相关的Java/Kotlin Input DTO类将会被自动生成

  • Input DTO和实体之间的彼此转化逻辑会被自动生成

  • 与之形状吻合的对象抓取器会被自动生成。(这个功能和input DTO无关,所以本文不做论述)

开发人员用自动生成的Input DTO作为API的入参,一行代码保存它即可 (内部逻辑:调用自动生成的转化逻辑把Input DTO转化为动态实体对象,再用保存指令直接保存)

信息

这是消除Input DTO爆炸带来的痛苦的最高效方案

定义DTO的形状

本文侧重于讲解如何保存静态DTO类型,并非系统性介绍DTO语言,请参考对象篇/DTO转换/DTO语言以了解完整的DTO语言。

假如Book类的全名为com.yourcompany.yourpoject.model.Book,你可以

  1. 在实体定义所在项目中,建立目录src/main/dto

  2. src/main/dto下,按实体类型所处的包路径建立子目录com/yourcompany/yourpoject/model

  3. 在上一步建立的目录下,建立文件Book.dto,文件必须和实体类同名,扩展名必须为dto

  4. 编辑此文件,利用DTO语言,定义Book实体的各种DTO形状

    Book.dto
    BookInput {

    #allScalars(Book)

    id(store)

    authors {
    #allScalars(Author)
    -id
    }
    }

    SimpleBookInput { ...略... }

    ...省略其他Input DTO形状定义...
    信息

    用作输入参数的Input DTO的形状定义必须用input修饰符修饰。

    具体原因已经在对象篇/DTO转换/DTO语言中有详细的阐述,本文不再赘述

自动生成DTO类型

Jimmer负责编译dto文件,自动生成符合这些形状的DTO类型。

警告

如果除了dto文件外还有其他Java/Kotlin原代码文件被修改了,直接点击IDE中运行按钮可以导致dto文件的重新编译

但是,如果除了dto文件外没有其他Java/Kotlin文件被修改,简单地点击IDE中运行按钮并不会导致dto文件被重新编译,除非显式地rebuild!

如果你使用的构建工具是Gradle,也可以使用社区提供的第三方Gradle插件来解决这个问题: jimmer-gradle

以上面代码中的BookInput为例,此dto文件被Jimmer成功编译后,会自动生成如下DTO类型

BookInput.java
package com.yourcompany.yourpoject.model.dto;

import com.yourcompany.yourpoject.model.Book;
import org.babyfish.jimmer.Input;

@GenertedBy(file = "<your_project>/src/main/dto/Book.dto")
public class BookInput implements Input<Book> {

@Nullable
private Long id;

private String name;

private int edition;

private BigDecimal price;

@Nullable
private Long storeId;

private List<TargetOf_authors> authors;

public BookInput(Book book) {
......
}

@Override
public Book toEntity() {
......
}

@Data
public static class TargetOf_authors {

private String firstName;

private String lastName;

private Gender gender;

...省略其他成员...
}

...省略其他成员...
}
  • ❶ 如果id被指定了自动生成策略,则id不是必须的。这也是保存指令的一个特性,具体细节请参考保存模式/总结

    信息
    • 对于Jimmer实体而言,id不可能为null,靠id属性的缺失 (即,不赋值) 来表达对象没有id的情况。

    • 对于Input DTO而言,静态的POJO类型没有属性缺失的概念,靠null来表达没有id的情况。

    二者看似矛盾,但其实可以简单地处理:如果Input DTO的id为null,转化后的实体对象无id (虽然id不允许被赋为null,但动态对象可以不对属性赋值)

  • ❷ 明确指定此InputDTO想以

    的方式编辑实体的多对一关联Book.store。其中,

  • ❸ 明确指定此InputDTO想以

    的方式编辑实体的多对过关联Book.authors, 关联对象的类型也被内嵌的InputDTO类型BookInput.TargetOf_authors固化。

  • ❹ 将动态实体转化为静态InputDTO

  • ❺ 将静态InputDTO转化为动态实体

HTTP API

DTO语言生成了相对完备的代码,所以,我们可以快速对外暴露保存数据的HTTP API

@PutMapping("/book")
public void saveBook(
@RequestBody BookInput input
) {
// `save(input)`等价于`save(input.toEntity())`
bookRepository.save(input);
}

最佳实践

在实际项目中,常常面临一个实际的问题,实体的属性可能非常多,而且

  • 插入时需要指定的属性相对较多

  • 修改时需要指定的属性相对较少

我们一致用作例子的Book等实体属性很少,不方便演示,因此,我虚构一个属性较多的实体类型:Product

Product.java
@Entity
public interface Product {

...省略实体属性...
}
  • 针对插入时需要指定的属性相对较多的情况,定义CreateProductInput

  • 针对修改时需要指定的属性相对较少的情况,定义UpdateProductInput

所以,Product.dto的代码看起来应该如下

input CreateProductInput {
...较多属性,略...
}

input UpdateProductInput {
...较少属性,略...
}

编译后,将会自动生成Java/Kotlin类CreateProductInputUpdateProductInput。所以,我们可以快速实现如下两个HTTP API

@PostMapping("/product")
public void createProduct(
// `CreateProductInput`属性相对多
@RequestBody CreateProductInput input
) {
productRepository.insert(input);
}

@PutMapping("/product")
public void updateProduct(
// `UpdateProductInput`属性相对少
@RequestBody UpdateProductInput input
) {
productRepository.update(input);
}
提示

由此可见,无论项目的业务特色决定需要为同一实体定义多少了不同的Input DTO类型。最终都是利用DTO语言快速生成Input DTO类型和相关数据转化逻辑,然后用一行代码调用保存指令即可。

哪怕项目的业务更复杂一些,比如不同身份的人可以编辑的数据结构的形状不同,也可以不断套用此模式轻松应对。