Save Aggregate Root
Overview
Although Jimmer's data saving capability is designed for arbitrary complex data structures, in order to show how to safely expose the data saving capability step by step, this article only discusses saving a single object, and saving complex data structures will be described in subsequent articles.
For more practical guidance, this article discusses two situations:
-
Homogeneous change scenario:
This is a relatively simple business scenario where the form structure of the
INSERT
operation and theUPDATE
operation is consistent. -
Heterogeneous change scenario:
This is a relatively complex business scenario where the form structures of the
INSERT
operation and theUPDATE
operation are inconsistent.
In addition, since the save directive only requires one function call, encapsulating it with Repository
does not make much sense. In order to simplify unnecessary complexity, this article no longer defines Repository
, but lets Controller
use sqlClient
directly.
Homogeneous Change Scenario
Define Input DTO
Since we have some understanding of the DTO language in Query Arbitrary Shape/Exposing Features/Return Output DTO, this article will not repeat the discussion.
-
Install DTO language Intellij plugin: https://github.com/ClearPlume/jimmer-dto (This process is not required but highly recommended)
-
Create a new directory
src/main/dto
-
Under
src/main/dto
, create a fileBook.dto
and write the following code:Book.dtoexport com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
input BookInput {
#allScalars(this)
}
...Omit other DTO definitions...infoUnlike the Output DTO in Query Arbitrary Shape/Exposing Features, the input DTO here uses the
input
modifier
Generated Code
After compilation, Jimmer will automatically generate the following types:
- 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) { ❹
...omitted...
}
@Override
public Book toEntity() { ❺
...omitted...
}
...Omit other members...
}
@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 = ❺
...
}
-
❶ Remind developers that this class is automatically generated by Jimmer
-
❷ Unlike the Output DTO in Query Arbitrary Shape/Exposing Features/Return Output DTO, after using the
input
modifier in the DTO language, the generated class will implement theInput<Book>
interface instead of theView<Book>
interface. -
❸ If an auto increment strategy (such as automatic change, sequence, UUID, snowflake ID) is configured for the
id
attribute of the original entity, using theinput
modifier in the DTO language will cause the id attribute of the DTO to be null.The final DTO object will be converted to an entity object through ❺ and then saved by Jimmer. If the id attribute of the DTO is null, then after conversion to the entity, the id attribute of the entity will be in an unspecified state.
For save operations that do not explicitly specify
INSERT
mode orUPDATE
mode:-
If the
id
attribute of the entity object is specified, judge whether it should beINSERT
orUPDATE
according toid
-
If the
id
attribute of the entity object is not specified, judge whether it should beINSERT
orUPDATE
according tokey
(in this casename
andedition
)
-
-
❹ Convert entity to DTO
-
❺ Convert DTO to entity
Write HTTP Service
Since DTO can be converted to entities, you can program like this:
- Java
- Kotlin
BookInput input = ...omitted...;
sqlClient.save(input.toEntity());
val input: BookInput = ...omitted...
sqlClient.save(input.toEntity())
In fact, Jimmer provides a more convenient way, so that even calling the toEntity
method to convert the DTO to an entity is not necessary, so the code can be simplified to:
- Java
- Kotlin
BookInput input = ...omitted...;
sqlClient.save(input);
val input: BookInput = ...omitted...
sqlClient.save(input)
Next, you can implement the Controller
:
- Java
- Kotlin
@RestController
public class BookController {
private final JSqlClient sqlClient;
public BookController(JSqlClient sqlClient) {
this.sqlClient = sqlClient;
}
@PutMapping("/book")
public 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
}
Heterogeneous Change Scenario
Requirements
Here we assume that there are two types of book management roles:
-
Provide two roles that can save books:
-
Operator: Can only modify the price of existing books
-
Administrator: Can create and edit all information of books
-
-
For the Administrator role, the Web API for creating new books and the Web API for editing books need to be separated
Write DTO
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
/**
* Input for the `Operator` role to modify books, can only modify the `price` attribute
*/
input BookOperatorUpdateInput {
id! // Override default behavior, id cannot be null
price
}
/**
* Input for the `Administrator` role to create new books, no id attribute
*/
input BookAdministratorCreateInput {
#allScalars(this)
-id // Creating new books does not need id
}
/**
* Input for the `Administrator` role to modify books, id attribute cannot be null
*/
input BookAdministratorUpdateInput {
#allScalars(this)
id! // Override default behavior, id cannot be null
}
...Omit other DTO definitions...
Generated Code
After compilation, the following three types are automatically generated:
-
BookOperatorUpdateInput
- Java
- Kotlin
BookOperatorUpdateInput/**
* Input for the `Operator` role to modify books, can only modify the `price` attribute
*/
@GeneratedBy(
file = "<yourproject>/src/main/dto/Book.dto"
)
public class BookOperatorUpdateInput implements Input<Book> {
private long id;
@NotNull
private BigDecimal price;
...Omit other methods...
}BookOperatorUpdateInput/**
* Input for the `Operator` role to modify books, can only modify the `price` attribute
*/
@GeneratedBy(
file = "<yourproject>/src/main/dto/Book.dto"
)
data class BookOperatorUpdateInput(
val id: Long,
val price: BigDecimal
) : Input<Book> {
...Omit other methods...
} -
BookAdministratorCreateInput
- Java
- Kotlin
BookAdministratorCreateInput/**
* Input for the `Administrator` role to create new books, no id attribute
*/
@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;
...Omit other methods...
}BookAdministratorCreateInput/**
* Input for the `Administrator` role to create new books, no id attribute
*/
@GeneratedBy(
file = "<yourproject>/src/main/dto/Book.dto"
)
data class BookAdministratorCreateInput(
val name: String,
val edition: Int,
val price: BigDecimal
) : Input<Book> {
...Omit other methods...
} -
BookAdministratorUpdateInput
- Java
- Kotlin
BookAdministratorUpdateInput/**
* Input for the `Administrator` role to modify books, id attribute cannot be 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;
...Omit other methods...
}BookAdministratorUpdateInput/**
* Input for the `Administrator` role to modify books, id attribute cannot be 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> {
...Omit other methods...
}
Write HTTP Service
- Java
- Kotlin
@RestController
public class BookController {
private final JSqlClient sqlClient;
public BookController(JSqlClient sqlClient) {
this.sqlClient = sqlClient;
}
@Secured("ADMINISTRATOR")
@PostMapping("/book")
public int createBookByAdministrator(
@RequestBody BookAdministratorCreateInput input
) {
return sqlClient
.insert(input)
.getTotalAffectedRowCount();
}
@Secured("OPERATOR")
@PutMapping("/book/byOperator")
public int updateBookByOperator(
@RequestBody BookOperatorUpdateInput input
) {
return sqlClient
.update(input)
.getTotalAffectedRowCount();
}
@Secured("ADMINISTRATOR")
@PutMapping("/book")
public 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
}
It is not difficult to find that no matter how diversified the Input DTO parameters are, Jimmer completes the data saving with one method call.