Skip to main content

Using MapStruct

caution

Not recommended for Kotlin.

  • This solution is based on MapStruct, which relies on apt.

  • Kotlin has deprecated kapt in favor of ksp.

So as Kotlin evolves, using kapt may cause more and more problems in the future.

Define Input DTO

Let's look at an example InputDTO (for simplicity, Java version uses Lombok):

BookInput.java
@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;
}
}
  • ❶ 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. Missing id (i.e. not assigning it) represents the object does not have an id.

    • For Input DTOs, static POJO types do not have the concept of missing properties. null represents missing id.

    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 Book.store using the

    mode.

  • ❸ Explicitly specifies this Input DTO wants to edit the entity's many-to-many association Book.authors using the

    mode. The type of associated objects is also fixed to the nested Input DTO type BookInput.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:

BookInputMapper.java
@Mapper
public interface BookInputMapper {

@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
Book toBook(BookInput input);

...Other mapstruct configurations omitted...
}
info

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

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)
);
}

In this example, MapStruct is used to convert BookInput to Book entity and directly persist it.

info
  • For Jimmer entities, Book.id cannot be null. Missing id (i.e. not assigning it) represents the object does not have an id.

  • For Input DTO, static POJO type, BookInput.id being null represents missing id.

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

BookInput.java
@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...
}
info

In this improved example, the previously introduced BookInputMapper is replaced by BookInput.Converter, so the previous BookInputMapper can be deleted.

@PutMapping("/book")
public void saveBook(
@RequestBody BookInput input
) {
// `save(input)` is equivalent to `save(input.toEntity())`
bookRepository.save(input);
}
info

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:

Product.java
@Entity
public interface Product {

...Many entity properties, omitted...
}
  • For insertion which requires specifying relatively more properties, define CreateProductInput:

    CreateProductInput.java
    @Data
    public class CreateProductInput implements Input<Product> {

    ...More fields, omitted...

    @Override
    public Product toEntity() {
    ...
    }
    }
  • For modification which requires specifying relatively fewer properties, define UpdateProductInput:

    UpdateProductInput.java
    @Data  
    public class UpdateProductInput implements Input<Product> {

    ...Fewer fields, omitted...

    @Override
    public Product toEntity() {
    ...
    }
    }

Finally, provide two HTTP APIs:

@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);
}
tip

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.