Using MapStruct
Define Input DTO
Let's look at an example InputDTO
(for simplicity, Java version uses 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
)
}
-
❶ If the id is designated some auto-generation strategy, it is not required. This is also a feature of save commands, see details in Save Modes/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 and difficult to convert between. Don't worry, Jimmer provides automated solutions, discussed later.
-
-
❷ 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.AuthorItem
.
MapStruct Converter
Jimmer extends MapStruct which can be used to handle conversions between dynamic entities and static Input DTOs. How to use the relevant extensions is detailed in Object/DTO Conversion/MapStruct, this article will not repeat it.
Define the BookInputMapper
interface:
- Java
- Kotlin
@Mapper
public interface BookInputMapper {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
Book toBook(BookInput input);
...Other mapstruct configurations omitted...
}
@Mapper
interface BookInputMapper {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
fun toBook(BookInput input): Book
...Other mapstruct configurations omitted...
}
MapStruct's annotation processor needs to be used to generate the implementation class for this interface at compile time. Details are explained in Object Section/DTO Mapping/MapStruct and not repeated here.
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)
}
}
In this example, MapStruct is used to convert BookInput
to Book
entity and directly persist it.
-
For Jimmer entities,
Book.id
cannot be null. Missingid
(i.e. not assigning it) represents the object does not have anid
. -
For Input DTO, static POJO type,
BookInput.id
being null represents missingid
.
Jimmer has built-in extensions to MapStruct that will not assign BookInput.id
to Book.id
if it is null, so there are no issues.
Improvement
To better integrate with Jimmer, developers can make BookInput
implement the org.babyfish.jimmer.Input<E>
interface:
public interface Input<E> {
E toEntity();
}
Dynamic objects will never implement this interface. It should be implemented by user-defined static POJO classes. Its functionality is simple: convert the current static POJO to a dynamic object.
Make BookInput
implement Input<Book>
:
- Java
- Kotlin
@Data
public class BookInput implements Input<Book> {
private static final Converter CONVERTER =
Mappers.getMapper(Converter.class);
...Private fields omitted...
@Override
public Book toEntity() {
return CONVERTER.toBook(this);
}
@Mapper
interface Converter {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
Book toBook(BookInput input);
...Other mapstruct configurations omitted...
}
...AuthorItem definition omitted...
}
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
...Other mapstruct configurations omitted...
}
companion object {
@JvmStatic
private val CONVERTER =
Mappers.getMapper(Converter::class.java)
}
...AuthorItem definition omitted...
}
In this improved example, the previously introduced BookInputMapper
is replaced by BookInput.Converter
, so the previous BookInputMapper
can be deleted.
- 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)
}
Leveraging the Input<E>
interface to change coding style is recommended but not mandatory.
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
:- Java
- Kotlin
CreateProductInput.java@Data
public class CreateProductInput implements Input<Product> {
...More fields, omitted...
@Override
public Product toEntity() {
...
}
}CreateProductInput.ktdata class CreateProductInput(
...More fields, omitted...
) : Input<Product> {
override fun toEntity(): Product {
...
}
} -
For modification which requires specifying relatively fewer properties, define
UpdateProductInput
:- Java
- Kotlin
UpdateProductInput.java@Data
public class UpdateProductInput implements Input<Product> {
...Fewer fields, omitted...
@Override
public Product toEntity() {
...
}
}UpdateProductInput.ktdata class UpdateProductInput(
...Fewer fields, omitted...
) : Input<Product> {
override fun toEntity(): Product {
...
}
}
Finally, provide 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 always use MapStruct to convert them into the uniform dynamic entity type, 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.