跳到主要内容

使用MapStruct

警告

不推荐在Kotlin下使用此方案。

因此,随着Kotlin的演化,未来使用kapt可能会遇到越来越多的问题。

定义Input DTO

让我们来看一个InputDTO的例子 (为了简单,Java版本采用Lombok)

BookInput.java
@Data
public class BookInput {

@Nullable
private Long id;

private String name;

private int edition;

private BigDecimal price;

@Nullable
private Long storeId;

private List<AuthorItem> authors;

@Data
public static class AuthorItem {

private String firstName;

private String lastName;

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

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

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

    二者似乎是矛盾的,难以转化。别担心,Jimmer给出自动化的解决方案,后文论述。

  • ❷ 明确指定此InputDTO想以

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

  • ❸ 明确指定此InputDTO想以

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

Mapstruct转化器

Jimmer拓展了mapstruct框架,可以用它来处理动态实体和静态Input DTO之间的转化问题。 如何使用相关拓展在对象篇/DTO转换/MapStruct中有详细介绍,本文不做重复。

定义BookInputMapper接口,如下

BookInputMapper.java
@Mapper
public interface BookInputMapper {

@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
Book toBook(BookInput input);

...省略其他mapstruct配置...
}
信息

需要使用mapstruct框架的预编译器在编译时生成此接口的实现类。这部分内容在对象篇/DTO转换/mapstruct中有详细的说明,本文不再赘述。

HTTP API

private static final BookInputMapper BOOK_INPUT_MAPPER = 
Mappers.getMapper(BookInputMapper.class);

@PutMapping("/book")
public void saveBook(
@RequestBody BookInput input
) {
bookRepository.save(
BOOK_INPUT_MAPPER.toBook(input)
);
}

在这个例子中,利用mapstruct将BookInput转化为Book实体,直接保存即可。

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

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

Jimmer内置了mapstruct的扩展,如果BookInput.id为null,则不会赋给Book.id,所以没有任何问题。

改进

为了更好地和Jimmer配合,开发人员可以让BookInput实现org.babyfish.jimmer.Input<E>接口。

public interface Input<E> {

E toEntity();
}

动态对象永远不会实现此结构,该接口应该由用户自定义的静态POJO类来实现。其功能非常简单,就是把当前静态POJO转化为动态对象。

BookInput实现Input<Book>,如下

BookInput.java
@Data
public class BookInput implements Input<Book> {

private static final Converter CONVERTER =
Mappers.getMapper(Converter.class);

...省略私有字段...

@Override
public Book toEntity() {
return CONVERTER.toBook(this);
}

@Mapper
interface Converter {

@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
Book toBook(BookInput input);

...省略其他mapstruct配置...
}

...省略内部类AuthorItem的定义...
}
信息

在这个改进的例子中,之前介绍过的BookInputMapperBookInput.Converter取代,因此可以删除之前介绍的BookInputMapper

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

利用Input<E>接口改变开发风格是建议性的,不是强制的。

最佳实践

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

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

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

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

Product.java
@Entity
public interface Product {

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

    CreateProductInput.java
    @Data
    public class CreateProductInput implements Input<Product> {

    ...较多字段,略...

    @Override
    public Product toEntity() {
    ......
    }
    }
  • 针对修改时需要指定的属性相对较少的情况,定义UpdateProductInput

    UpdateProductInput.java
    @Data
    public class UpdateProductInput implements Input<Product> {

    ...较少字段,略...

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

最后,提供两个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类型。最终都是利用mapstruct将其转化为类型统一的动态实体对象,然后用一行代码调用保存指令即可。

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