Skip to main content

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 the UPDATE operation is consistent.

  • Heterogeneous change scenario:

    This is a relatively complex business scenario where the form structures of the INSERT operation and the UPDATE 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.

  1. Install DTO language Intellij plugin: https://github.com/ClearPlume/jimmer-dto (This process is not required but highly recommended)

  2. Create a new directory src/main/dto

  3. Under src/main/dto, create a file Book.dto and write the following code:

    Book.dto
    export com.yourcompany.yourproject.model.Book
    -> package com.yourcompany.yourproject.model.dto

    input BookInput {
    #allScalars(this)
    }
    ...Omit other DTO definitions...
    info

    Unlike 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:

BookInput.java
@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...
}
  • ❶ 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 the Input<Book> interface instead of the View<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 the input 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 or UPDATE mode:

    • If the id attribute of the entity object is specified, judge whether it should be INSERT or UPDATE according to id

    • If the id attribute of the entity object is not specified, judge whether it should be INSERT or UPDATE according to key (in this case name and edition)

  • ❹ Convert entity to DTO

  • ❺ Convert DTO to entity

Write HTTP Service

Since DTO can be converted to entities, you can program like this:

BookInput input = ...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:

BookInput input = ...omitted...;
sqlClient.save(input);

Next, you can implement the Controller:

BookController.java
@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();
}
}

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

Book.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

    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...
    }
  • BookAdministratorCreateInput

    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...
    }
  • BookAdministratorUpdateInput

    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...
    }

Write HTTP Service

BookController.java
@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();
}
}

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.