Using DTO Language
Jimmer provides DTO language.
Developers can use this language to quickly define the shape of the Input DTO to be saved. After compilation:
-
The corresponding Java/Kotlin Input DTO classes will be generated automatically
-
The mutual conversion logic between Input DTOs and entities will be generated automatically
-
The object fetcher that matches its shape will be automatically generated. (This feature is unrelated to input DTOs, so it is not discussed in this article)
Developers use the auto-generated Input DTOs as method parameters, and persist them in one line of code (Internal logic: Call the auto-generated conversion logic to convert the Input DTO to a dynamic entity object, then directly persist using the save command).
This is the most efficient way to eliminate the pain of Input DTO explosion.
Defining DTO Shape
This article focuses on explaining how to save static DTO types, not a systematic introduction to DTO language. Please refer to Object/DTO conversion/DTO Language to know more.
Assume the fully qualified name of the Book
class is com.yourcompany.yourproject.model.Book
. You can:
-
In the project where entity is defined, create directory
src/main/dto
-
Under
src/main/dto
, create subdirectories following the package path of the entity, i.e.com/yourcompany/yourproject/model
-
Under the directory created in previous step, create the file
Book.dto
. The file must have the same name as the entity class and the extension must bedto
. -
Edit this file and use DTO language to define various DTO shapes for the Book entity:
Book.dtoinput BookInput {
#allScalars(Book)
id(store)
authors {
#allScalars(Author)
-id
}
}
input SimpleBookInput { ... }
...Other Input DTO shapes omitted...infoInput DTO shapes used as input parameters must be decorated with the
input
modifier.The specific reasons are elaborated in Object/DTO conversion/DTO Language and not repeated here.
Auto-Generated DTO Types
Jimmer is responsible for compiling dto files and automatically generating DTO types conforming to these shapes.
If Java/Kotlin source files other than dto files are modified, directly clicking the Run button in IDE can trigger recompilation of dto files.
However, if no Java/Kotlin files other than dto files are modified, simply clicking the Run button in IDE will not trigger dto file recompilation unless rebuild is explicitly triggered!
If you are using Gradle as your build tool, you can also use the third-party Gradle plugin provided by the community to solve this problem: jimmer-gradle
Take BookInput
in the above code as an example. After successful compilation of this dto file, the following DTO types will be auto-generated:
- Java
- Kotlin
package com.yourcompany.yourproject.model.dto;
import com.yourcompany.yourproject.model.Book;
import org.babyfish.jimmer.Input;
@GeneratedBy(file = "<your_project>/src/main/dto/Book.dto")
public class BookInput implements Input<Book> {
@Nullable
private Long id; ❶
private String name;
private int edition;
private BigDecimal price;
@Nullable
private Long storeId; ❷
private List<TargetOf_authors> authors; ❸
public BookInput(Book book) { ❹
...
}
@Override
public Book toEntity() { ❺
...
}
@Data
public static class TargetOf_authors {
private String firstName;
private String lastName;
private Gender gender;
...Other members omitted...
}
...Other members omitted...
}
package com.yourcompany.yourproject.model.dto
import com.yourcompany.yourproject.model.Book
import org.babyfish.jimmer.Input
@GeneratedBy(file = "<your_project>/src/main/dto/Book.dto")
data class BookInput(
val id: Long? = null, ❶
val name: String = "",
val edition: Int = 0,
val price: BigDecimal,
val storeId: Long? = null, ❷
val authors: List<TargetOf_authors> = emptyList() ❸
): Input<Book> {
constructor(book: Book) : this(...) ❹
override fun toEntity(): Book = ... ❺
data class TargetOf_authors(
val firstName: String,
val lastName: String,
val gender: Gender
) {
...Other members omitted...
}
...Other members omitted...
}
-
❶ If the id is designated some auto-generation strategy, it is not required. This is also a feature of save commands, see details in Mutation/Save Command/Save Mode/Summary.
info-
For Jimmer entities,
id
cannot be null. Missingid
(i.e. not assigning it) represents the object does not have anid
. -
For Input DTOs, static POJO types do not have the concept of missing properties.
null
represents missingid
.
This seems contradictory, but can be handled simply: If Input DTO's
id
isnull
, the converted entity object will not have anid
(althoughid
of entity cannot be assignednull
, dynamic objects allow unspecified properties). -
-
❷ Explicitly specifies this Input DTO wants to edit the entity's many-to-one association
mode.Book.store
using the -
❸ Explicitly specifies this Input DTO wants to edit the entity's many-to-many association
mode. The type of associated objects is also fixed to the nested Input DTO typeBook.authors
using theBookInput.TargetOf_authors
. -
❹ Convert dynamic entity to static Input DTO
-
❺ Convert static Input DTO to dynamic entity
HTTP API
The DTO language generates relatively complete code, so we can quickly expose HTTP APIs to persist data:
- Java
- Kotlin
@PutMapping("/book")
public void saveBook(
@RequestBody BookInput input
) {
// `save(input)` is equivalent to `save(input.toEntity())`
bookRepository.save(input);
}
@PutMapping("/book")
fun saveBook(
@RequestBody input: BookInput
) {
// `save(input)` is equivalent to `save(input.toEntity())`
bookRepository.save(input)
}
Best Practices
In real projects, we often face the problem that entities may have many properties, and
-
Relatively more properties need to be specified during insertion
-
Relatively fewer properties need to be specified during modification
The Book
entity we consistently use as example has few properties and is inconvenient to demonstrate this.
So I make up an entity type Product
with more properties:
- Java
- Kotlin
@Entity
public interface Product {
...Many entity properties, omitted...
}
@Entity
interface Product {
...Many entity properties, omitted...
}
-
For insertion which requires specifying relatively more properties, define
CreateProductInput
. -
For modification which requires specifying relatively fewer properties, define
UpdateProductInput
.
So the Product.dto
code looks like:
input CreateProductInput {
...More properties, omitted...
}
input UpdateProductInput {
...Fewer properties, omitted...
}
After compilation, CreateProductInput
and UpdateProductInput
Java/Kotlin classes will be auto-generated. So we can quickly implement the following two HTTP APIs:
- Java
- Kotlin
@PostMapping("/product")
public void createProduct(
// `CreateProductInput` has relatively more properties
@RequestBody CreateProductInput input
) {
productRepository.insert(input);
}
@PutMapping("/product")
public void updateProduct(
// `UpdateProductInput` has relatively fewer properties
@RequestBody UpdateProductInput input
) {
productRepository.update(input);
}
@PostMapping("/product")
fun createProduct(
// `CreateProductInput` has relatively more properties
@RequestBody input: CreateProductInput
) {
productRepository.insert(input)
}
@PutMapping("/product")
fun updateProduct(
// `UpdateProductInput` has relatively fewer properties
@RequestBody input: UpdateProductInput
) {
productRepository.update(input)
}
As you can see, no matter how many different Input DTO
types need to be defined for the same entity according to project requirements, we can quickly generate Input DTO types and related data conversion logic using DTO language, then persist in one line of code calling save commands.
Even if the project is more complex, e.g. people with different identities can edit data structures of different shapes, this pattern can still be applied repeatedly to handle such scenarios easily.