跳到主要内容

MapStruct

简介

Jimmer拓展了MapStruct,支持使用mapstruct来完成Jimmer动态实体对象和静态DTO对象之间的相互转化。

注意事项

Jimmer的实体对象是动态的 (和Hibernate3引入的标量属性惰性化比较类似),这是较早的MapStruct未曾考虑过的模式。

MapStruct交流后,MapStruct会从1.6.0开始支持这种行为。

警告

因此,请尽可能使用1.6.0或更高版本的MapStruct

优点

  • 和追求快速开发但支持固定转化逻辑的DTO语言不同,mapstruct可以实现任意复杂的转化逻辑。

  • DTO语言直接生成全新的DTO类型不同,mapstruct可以整合现有的DTO类型。

缺点

更推荐使用DTO语言,原因如下

  • 不可忽略的开发成本

    DTO语言为Jimmer量身定制的方案,开发效率是结合其他任何技术方案无法比拟的。

  • 不太适合Output DTO

    DTO语言自动生成的DTO类型具备内置的对象抓取器,因此可以作为查询的输出类型 (虽然不推荐),请参见:

    然而,手动定义的DTO类型没有对应的对象抓取器定义,只支持和动态实体相互转化。 虽然可以为此手动定义对象抓取器,但是存在DTO和对象抓取器形状不一致的风险。 所以,不适合作为Output DTO。

  • Kotlin风险

    • mapstruct是基于apt(Annotation Processor) 的。

      因此,这需要在kotlin中使用kapt,这会明显降低kotlin项目的编译速度。

    • Kotlin已经废弃了kapt,而主张使用ksp

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

依赖和预编译器

对于将静态POJO转化为Jimmer动态对象而言,MapStruct并不知道该如何构建Jimmer对象。因此

  • Jimmer本身的预编译器 (Java的jimmer-apt或Kotlin的jimmer-ksp) 在Draft中生成了一个一些面向MapStruct的代码,让MapStruct可以通过其Builder模式构建Jimmer对象。

  • Jimmer扩展了MapStruct的Annotation Processor,该扩展让MapStruct利用生成的Draft中为MapStruct预留的能力构建Jimmer对象。

    这个扩展叫做jimmer-mapstruct-apt

    • 对于Java而言,jimmer-mapstruct-aptjimmer-apt所包含

    • 对于Kotlin而言,需同时在maven或gradle配置文件中使用jimmer-kspjimmer-mapstruct-apt

      如果使用Gradle插件,当你引入了MapStruct的kapt依赖时,插件会自动引入jimmer-mapstruct-apt的依赖。

pom.xml
...省略其他代码...

<build>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
...省略其他依赖...
</dependencies>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.babyfish.jimmer</groupId>
<artifactId>jimmer-apt</artifactId>
<version>${jimmer.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

...省略其他代码...

这个例子中,我们假设Java中基于lombok编写静态POJO。

语言位置描述
Java和Kotlin引入mapstruct依赖,让用户代码可以使用mapstruct的注解
使用Jimmer的预编译器为动态类型生成相关的源代码,Java使用jimmer-apt,Kotlin使用jimmer-ksp
使用mapstruct的annotation processor生成源代码 (后文会介绍)
仅Java引入lombok的依赖,让用户代码可以使用lombok的注解
使用Lombok的预编译器更改静态POJO类的代码,比如添加getter, setter
仅Kotlin使用jimmer-mapstruct-apt拓展➌

定义POJO

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<Long> authorIds;
}
备注

Java POJO代码中采用了@Nullable注解,仅为提高可读性,无实质性功能

该POJO有三个属性,需要说明一下

  • BookInput.id

    • 这里,BookInput.id是允许为null的。这是必要的,比如,插入数据不需要指定id。

    • 实体对象动态属性Book.id不允许为null (Jimmer禁止id可以为null,请参见映射篇/基础映射/简单映射#@Id)

    二者彼此矛盾,那么,BookInput怎么转化为Book呢?

    提示

    Jimmer约定,如果POJO的属性可为null而动态对象上对应的属性不能为null,那么动态对象的对应属性不会被赋值,保持缺失的状态。

  • BookInput.storeId

    很明显,这是关联id,对实体对象动态属性Book.store

    这种动态对象属性被定义为关联对象,而POJO中却定义为关联id,叫做

  • BookInput.authorIds

    很明显,这是关联id集合,对实体对象动态属性Book.authors

    这种动态对象属性被定义为关联对象,而POJO中却定义为关联id,叫做

其他属性和原始实体的定义完全一样,无需说明

定义Mapper

使用mapstruct最重要的事是定义Mapper,如下

BookInputMapper.java
@Mapper
public interface BookInputMapper {

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

该Mapper提供一个toBook方法,用于把BookInput对象转化为Book对象。

BookInput.idBookInput.idBookInput.nameBookInput.price都是非关联属性,mapstruct能很好地处理它们。

备注

其中,BookInput.id可以为null, 而Book.id不能为null的问题,前面已经讨论过了,这里不再赘述。

关键是BookInput.storeIdBookInput.authorIds应该如何映射,这分为两种情况了。

  • 实体定义了@IdView属性

  • 实体未定义@IdView属性

如果实体定义了@IdView属性

如果实体类型定义了@IdView属性,例如

Book.java
package com.example.model;

import org.babyfish.jimmer.sql.*;
import org.jetbrains.annotations.Nullable;

@Entity
public interface Book {

...省略其他属性...

@ManyToOne
@Nullable
BookStore store();

@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_id"
)
List<Author> authors();

@IdView // 关联对象store的id的视图
Long storeId();

// 关联对象集合authors中所有对象的id的视图
@IdView("authors")
List<Long> authorIds();
}

这种情况下,实体对象和POJO完全对应,Mapper无需做任何修改。

如果实体未定义@IdView属性

如果实体类型并为定义@IdView属性,需要修改Mapper

  • BookInput.storeId转化为只有id的BookStore对象,再赋给Book.store

  • BookInput.authorIds转化为只有id的Author对象的集合,再赋给Book.authors

BookInputMapper.java
@Mapper
public interface BookInputMapper {

@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
@Mapping(target = "store", source = "storeId")
@Mapping(target = "authors", source = "authorIds")
Book toBook(BookInput input);

@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = ".")
BookStore toBookStore(Long id);

@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = ".")
Author toAuthor(Long id);
}

由于mapstruct还支持@Mapping(target = "store.id", source = "storeId")的写法,也可以用下面的写法来简化代码

BookInputMapper.java
@Mapper
public interface BookInputMapper {

@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
@Mapping(target = "store.id", source = "storeId")
@Mapping(target = "authors", source = "authorIds")
Book toBook(BookInput input);

@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = ".")
Author toAuthor(Long id);
}

使用

现在,我们就可以把BookInput转化为Book

BookInput input = ...省略...;
BookInputMapper mapper = Mappers.getMapper(BookInputMapper.class);
Book book = mapper.toBook(input);

让POJO实现Input接口

Jimmer提供了一个简单接口,org.babyfish.jimmer.Input<E>

public interface Input<E> {

E toEntity();
}

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

该接口可以提供语法层面的便利,无论是底层的保存指令还是上层的spring-data基接口JRepository/KRepository,其sava方法都直接接受Input参数,而无需用户调用Mapper完成转化。

如果想要这个语法层面的便利,你可以选择让POJO实现该接口,修改BookInput的代码,如下

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配置...
}
}
  • BookInput类实现了接口org.babyfish.jimmer.Input

  • ❷ 实现Input.toEntity方法,利用MapStruct把当前静态的Input DTO对象转化为动态的Book实体对象。这是这个类唯一的功能