保存聚合根
概述
尽管Jimmer的数据保存能力为任意复杂的数据结构而设计,但为了循序渐进地讲解如何安全地暴露数据保存能力,本文仅讨论保存孤单对象,复杂数据结构的保存在后续文章讲述。
为更具实战指导意义,本文分两种情况讨论
-
增改同构场景:
这是一种相对简单的业务场景,
INSERT
操作和UPDATE
操作的表单结构一致。 -
增改异构场景:
这是一种相对复杂的业务场景,
INSERT
操作和UPDATE
操作的表单结构不一致。
另外,因为save指令只需一个函数调用,采用Repository
对其进行封装意义不大。为了简化非必要复杂性,本文不再定义Repository
,而是让Controller
直接使用sqlClient
。
增改同构场景
定义Input DTO
由于在查询任意形状/暴露功能/返回输出DTO一文中,为讲解Output DTO,我们已经对DTO语言有了一定了解,本文不做重复性阐述。
-
安装DTO语言Intellij插件:https://github.com/ClearPlume/jimmer-dto (此过程不是必须的,但非常推荐)
-
新建目录
src/main/dto
-
在
src/main/dto
下建立一个文件Book.dto
,编写代码如下Book.dtoexport com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
input BookInput {
#allScalars(this)
}
...省略其他DTO类型定义...信息和查询任意形状/暴露功能/返回输出DTO一文中的Output DTO不同,这里输入DTO采用
input
修饰符
生成的代码
编译,Jimmer将自动生成如下类型
- Java
- Kotlin
@GeneratedBy( ❶
file = "<yourproject>/src/main/dto/Book.dto"
)
public class BookInput implements Input<Book> { ❷
@Nullable ❸
private Long id;
@NotNull
private String name;
private int edition;
@NotNull
private BigDecimal price;
public BookInput(@NotNull Book base) { ❹
...略...
}
@Override
public Book toEntity() { ❺
...略...
}
...省略其他成员...
}
@GeneratedBy( ❶
file = "<yourproject>/src/main/dto/Book.dto"
)
data class BookInput(
id: Long?, ❸
name: String,
edition: Int,
price: BigDecimal
) : Input<Book> { ❷
constructor(base: Book): ❹
this(...)
override fun toEntity(): Book = ❺
...
}
-
❶ 提醒开发人员,这个类是Jimmer自动生成的
-
❷ 和查询任意形状/暴露功能/返回输出DTO一文中的Output DTO不同, 在DTO语言中使用
input
修饰符后,生成的类会实现Input<Book>
接口,而非View<Book>
接口。 -
❸ 如果为原实体的
id
属性配置了自动增长策略 (比如自动变化,序列,UUID,雪花ID),那么在DTO语言中使用input
修饰符会导致DTO的id属性为null。最终DTO对象要通过❺转化为实体对象再由Jimmer保存,如果DTO的id属性为null,那么转化为实体后,实体的id属性就会处于未指定状状态。
对于没有明确指定
INSERT
模式或UPDATE
模式的保存操作而言。-
如果实体对象
id
属性被指定了,按照id
判断应该INSERT
还是UPDATE
-
如果实体对象
id
属性没有被指定,按照key
*(本例中为name
和edition
) *判断应该INSERT
还是UPDATE
-
-
❹ 将实体转化为DTO
-
❺ 将DTO转化为实体
编写HTTP服务
既然DTO可以转化为实体,可以如此编程
- Java
- Kotlin
BookInput input = ...略...;
sqlClient.save(input.toEntity());
val input: BookInput = ...略...
sqlClient.save(input.toEntity())
事实上,Jimmer提供更简便的方法,连调用toEntity
方法将DTO转化为实体也不需要,所以,可以简化代码
- Java
- Kotlin
BookInput input = ...略...;
sqlClient.save(input);
val input: BookInput = ...略...
sqlClient.save(input)
接下来,就可以实现Controller
了。
- Java
- Kotlin
@RestController
public class BookController {
private final JSqlClient sqlClient;
public BookController(JSqlClient sqlClient) {
this.sqlClient = sqlClient;
}
@PutMapping("/book")
pubic int saveBook(
@RequestBody BookInput input
) {
return sqlClient
.save(input)
.getTotalAffectedRowCount();
}
}
class BookController(
private val sqlClient: KSqlClient
) {
@PutMapping("/book")
fun saveBook(
@RequestBody input: BookInput
): Int =
sqlClient
.save(input)
.totalAffectedRowCount
}
增改异构场景
需求
这里我们假设存在两种图书管理者角色:
-
提供两种可以保存书籍的角色
-
Operator: 仅能修改已有书籍的价格
-
Administrator: 能新建和编辑书籍的所有信息
-
-
对于Administrator角色而言,新建书籍的Web Api和编辑书籍的Web Api需要分开
编写DTO
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
/**
* 供`Operator`角色修改书籍的input, 只能修改`price`属性
*/
input BookOperatorUpdateInput {
id! //覆盖默认行为,id不能为null
price
}
/**
* 供`Administrator`角色新建书籍的input, 没有id属性
*/
input BookAdministratorCreateInput {
#allScalars(this)
-id //新建书籍不需要id
}
/**
* 供`Administrator`角色修改书籍的input, id属性不能为null
*/
input BookAdministratorUpdateInput {
#allScalars(this)
id! //覆盖默认行为,id不能为null
}
...省略其他DTO定义...
生成的代码
编译,自动生成如下三个类型
-
BookOperatorUpdateInput
- Java
- Kotlin
BookOperatorUpdateInput/**
* 供`Operator`角色修改书籍的input, 只能修改`price`属性
*/
@GeneratedBy(
file = "<yourproject>/src/main/dto/Book.dto"
)
public class BookOperatorUpdateInput implements Input<Book> {
private long id;
@NotNull
private BigDecimal price;
...省略其他方法...
}BookOperatorUpdateInput/**
* 供`Operator`角色修改书籍的input, 只能修改`price`属性
*/
@GeneratedBy(
file = "<yourproject>/src/main/dto/Book.dto"
)
data class BookOperatorUpdateInput(
val id: Long,
val price: BigDecimal
) : Input<Book> {
...省略其他方法...
} -
BookAdministratorCreateInput
- Java
- Kotlin
BookAdministratorCreateInput/**
* 供`Administrator`角色新建书籍的input, 没有id属性
*/
@GeneratedBy(
file = "<yourproject>/src/main/dto/Book.dto"
)
public class BookAdministratorCreateInput implements Input<Book> {
@NotNull
private String name;
private int edition;
@NotNull
private BigDecimal price;
...省略其他方法...
}BookAdministratorCreateInput/**
* 供`Administrator`角色新建书籍的input, 没有id属性
*/
@GeneratedBy(
file = "<yourproject>/src/main/dto/Book.dto"
)
data class BookAdministratorCreateInput(
val name: String,
val edition: Int,
val price: BigDecimal
) : Input<Book> {
...省略其他方法...
} -
BookAdministratorUpdateInput
- Java
- Kotlin
BookAdministratorUpdateInput/**
* 供`Administrator`角色修改书籍的input, id属性不能为null
*/
@GeneratedBy(
file = "<yourproject>/src/main/dto/Book.dto"
)
public class BookAdministratorUpdateInput implements Input<Book> {
@NotNull
private String name;
private int edition;
@NotNull
private BigDecimal price;
private long id;
...省略其他方法...
}BookAdministratorUpdateInput/**
* 供`Administrator`角色修改书籍的input, id属性不能为null
*/
@GeneratedBy(
file = "<yourproject>/src/main/dto/Book.dto"
)
data class BookAdministratorUpdateInput(
val name: String,
val edition: Int,
val price: BigDecimal,
val id: Long
) : Input<Book> {
...省略其他方法...
}
编写HTTP服务
- Java
- Kotlin
@RestController
public class BookController {
private final JSqlClient sqlClient;
public BookController(JSqlClient sqlClient) {
this.sqlClient = sqlClient;
}
@Secured("ADMINISTRATOR")
@PostMapping("/book")
pubic int createBookByAdministrator(
@RequestBody BookAdministratorCreateInput input
) {
return sqlClient
.insert(input)
.getTotalAffectedRowCount();
}
@Secured("OPERATOR")
@PutMapping("/book/byOperator")
pubic int updateBookByOperator(
@RequestBody BookOperatorUpdateInput input
) {
return sqlClient
.update(input)
.getTotalAffectedRowCount();
}
@Secured("ADMINISTRATOR")
@PutMapping("/book")
pubic int updateBookByAdministrator(
@RequestBody BookAdministratorUpdateInput input
) {
return sqlClient
.update(input)
.getTotalAffectedRowCount();
}
}
class BookController(
private val sqlClient: KSqlClient
) {
@Secured("ADMINISTRATOR")
@PutMapping("/book")
fun createBookByAdministrator(
@RequestBody input: BookAdministratorCreateInput
): Int =
sqlClient
.insert(input)
.totalAffectedRowCount
@Secured("OPERATOR")
@PutMapping("/book/byOperator")
fun createBookByAdministrator(
@RequestBody input: BookOperatorUpdateInput
): Int =
sqlClient
.update(input)
.totalAffectedRowCount
@Secured("ADMINISTRATOR")
@PutMapping("/book/byOperator")
fun updateBookByAdministrator(
@RequestBody input: BookAdministratorUpdateInput
): Int =
sqlClient
.update(input)
.totalAffectedRowCount
}
不难发现,无论Input DTO参数如何多样化,Jimmer一律一个方法调用完成数据保存。