使用MapStruct
定义Input DTO
让我们来看一个InputDTO
的例子 (为了简单,Java版本采用Lombok)
- Java
- Kotlin
@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;
}
}
data class BookInput(
val id: Long? = null, ❶
val name: String,
val edition: Int,
val price: BigDecimal,
val storeId: Long?, ❷
val authors: List<AuthorItem> ❸
) {
data class AuthorItem(
val firstName: String,
val lastName: String,
val 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接口,如下
- Java
- Kotlin
@Mapper
public interface BookInputMapper {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
Book toBook(BookInput input);
...省略其他mapstruct配置...
}
@Mapper
interface BookInputMapper {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
fun toBook(BookInput input): Book
...省略其他mapstruct配置...
}
需要使用mapstruct框架的预编译器在编译时生成此接口的实现类。这部分内容在对象篇/DTO转换/mapstruct中有详细的说明,本文不再赘述。
HTTP API
- Java
- Kotlin
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)
);
}
@PutMapping("/book")
fun saveBook(
@RequestBody input: BookInput
) {
bookRepository.save(
BOOK_INPUT_MAPPER.toBook(input)
)
companion object {
private val BOOK_INPUT_MAPPER =
Mappers.getMapper(BookInputMapper::class.java)
}
}
在这个例子中,利用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>
,如下
- Java
- Kotlin
@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的定义...
}
data class BookInput(
...略...
): Input<Book> {
override fun toEntity(): Book =
CONVERTER.toBook(this)
@Mapper
internal interface Converter {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
fun toBook(input: BookInput): Book
...省略其他mapstruct配置...
}
companion object {
@JvmStatic
private val CONVERTER =
Mappers.getMapper(Converter::class.java)
}
...省略内部类AuthorItem的定义...
}
在这个改进的例子中,之前介绍过的BookInputMapper
被BookInput.Converter
取代,因此可以删除之前介绍的BookInputMapper
- Java
- Kotlin
@PutMapping("/book")
public void saveBook(
@RequestBody BookInput input
) {
// `save(input)`等价于`save(input.toEntity())`
bookRepository.save(input);
}
@PutMapping("/book")
fun saveBook(
@RequestBody input: BookInput
) {
// `save(input)`等价于`save(input.toEntity())`
bookRepository.save(input)
}
利用Input<E>
接口改变开发风格是建议性的,不是强制的。
最佳实践
在实际项目中,常常面临一个实际的问题,实体的属性可能非常多,而且
-
插入时需要指定的属性相对较多
-
修改时需要指定的属性相对较少
我们一致用作例子的Book
等实体属性很少,不方便演示,因此,我虚构一个属性较多的实体类型:Product
。
- Java
- Kotlin
@Entity
public interface Product {
...省略实体属性...
}
@Entity
interface Product {
...省略实体属性...
}
-
针对插入时需要指定的属性相对较多的情况,定义
CreateProductInput
- Java
- Kotlin
CreateProductInput.java@Data
public class CreateProductInput implements Input<Product> {
...较多字段,略...
@Override
public Product toEntity() {
...略...
}
}CreateProductInput.ktdata class CreateProductInput(
...较多字段,略...
) : Input<Product> {
@Override
public Product toEntity() {
...略...
}
} -
针对修改时需要指定的属性相对较少的情况,定义
UpdateProductInput
- Java
- Kotlin
UpdateProductInput.java@Data
public class UpdateProductInput implements Input<Product> {
...较少字段,略...
@Override
public Product toEntity() {
...略...
}
}UpdateProductInput.ktdata class UpdateProductInput(
...较少字段,略...
) : Input<Product> {
@Override
public Product toEntity() {
...略...
}
}
最后,提供两个HTTP API
- Java
- Kotlin
@PostMapping("/product")
public void createProduct(
// `CreateProductInput`属性相对多
@RequestBody CreateProductInput input
) {
productRepository.insert(input);
}
@PutMapping("/product")
public void updateProduct(
// `UpdateProductInput`属性相对少
@RequestBody UpdateProductInput input
) {
productRepository.update(input);
}
@PostMapping("/product")
fun createProduct(
// `CreateProductInput`属性相对多
@RequestBody input: CreateProductInput
) {
productRepository.insert(input)
}
@PutMapping("/product")
fun updateProduct(
// `UpdateProductInput`属性相对少
@RequestBody input: UpdateProductInput
) {
productRepository.update(input)
}
由此可见,无论项目的业务特色决定需要为同一实体定义多少了不同的Input DTO
类型。最终都是利用mapstruct将其转化为类型统一的动态实体对象,然后用一行代码调用保存指令即可。
哪怕项目的业务更复杂一些,比如不同身份的人可以编辑的数据结构的形状不同,也可以不断套用此模式轻松应对。