Skip to main content

MapStruct

Introduction

Jimmer extends MapStruct to support using mapstruct to complete the mutual conversion between Jimmer dynamic entity objects and static DTO objects.

Notes

Jimmer's entity objects are dynamic (similar to scalar attribute lazy loading introduced in Hibernate 3), which is a pattern that earlier versions of MapStruct did not consider.

After communicating with MapStruct, MapStruct will support this behavior starting from 1.6.0.

caution

Therefore, please use 1.6.0 or higher version of MapStruct whenever possible.

Advantages

  • Unlike DTO Language which pursues fast development but supports fixed conversion logic, mapstruct can implement arbitrarily complex conversion logic.

  • Unlike DTO Language which directly generates brand new DTO types, mapstruct can integrate existing DTO types.

Disadvantages

DTO Language is more recommended for the following reasons:

  • Non-negligible development costs

    DTO Language is a solution tailored for Jimmer with development efficiency that cannot be compared when combined with any other technical solutions.

  • Not very suitable for Output DTO

    The DTO types automatically generated by the DTO language have built-in Object Fetchers, so they can be used as query output types (although not recommended), please refer to:

    However, manually defined DTO types do not have corresponding Object Fetcher definitions, and only support mutual conversion with dynamic entities. Although Object Fetchers can be manually defined for this, there is a risk that the DTO and Object Fetcher shapes will be inconsistent. So it is not suitable as Output DTO.

  • Kotlin risks

    • mapstruct is based on apt (Annotation Processor).

      Therefore, this requires using kapt in Kotlin, which will significantly reduce the compilation speed of Kotlin projects.

    • Kotlin has deprecated kapt in favor of ksp.

      Therefore, using kapt may encounter problems in the future as Kotlin evolves.

Dependencies and Preprocessors

For converting static POJOs to Jimmer dynamic objects, MapStruct does not know how to build Jimmer objects. So

  • Jimmer's own preprocessor (Java's jimmer-apt or Kotlin's jimmer-ksp) generates some MapStruct-oriented code in Draft, allowing MapStruct to build Jimmer objects through its Builder mode.

  • Jimmer extends MapStruct's Annotation Processor. This extension allows MapStruct to utilize the capabilities reserved for MapStruct in the generated Draft to build Jimmer objects.

    This extension is called jimmer-mapstruct-apt

    • For Java, jimmer-mapstruct-apt is included in jimmer-apt

    • For Kotlin, jimmer-ksp and jimmer-mapstruct-apt must be used together in the maven or gradle configuration file.

      If you use Gradle plugin Jimmer, the plugin will automatically configure it for you when you have the MapStruct kapt dependency.

pom.xml
...omit other code...

<build>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
...omit other dependencies...
</dependencies>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.babyfish.jimmer</groupId>
<artifactId>jimmer-apt</artifactId>
<version>${jimmer.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

...omit other code...

In this example, we assume static POJOs are written in Java using lombok.

LanguageLocationDescription
Java and KotlinIntroduce mapstruct dependency for user code to use mapstruct annotations
Use Jimmer's preprocessor to generate related source code for dynamic types, Java uses jimmer-apt, Kotlin uses jimmer-ksp
Use mapstruct's annotation processor to generate source code (introduced later)
Java onlyIntroduce lombok dependency for user code to use lombok annotations
Use Lombok preprocessor to modify static POJO class code, e.g. add getters, setters
Kotlin onlyUse jimmer-mapstruct-apt to extend ➌

Define POJO

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<Long> authorIds;
}
note

The @Nullable annotation is used in the Java POJO code only to improve readability and has no functional effect

Three properties of this POJO need to be explained:

  • BookInput.id

    • Here, BookInput.id is allowed to be null. This is necessary, for example, the id does not need to be specified when inserting data.

    • The dynamic property Book.id of the entity object does not allow null (Jimmer prohibits id from being null, please refer to Mapping/Basic Mapping/@Id)

    The two contradict each other, so how to convert BookInput to Book?

    tip

    Jimmer agrees that if the property of the POJO can be null while the corresponding property of the dynamic object cannot be null, the corresponding property of the dynamic object will not be assigned and will remain missing.

  • BookInput.storeId

    This is obviously an associated id for the dynamic entity object property Book.store.

    This kind of dynamic object property is defined as an associated object, but in the POJO it is defined as an associated id, called a

    .

  • BookInput.authorIds

    This is obviously a collection of associated ids, for the dynamic entity object property Book.authors.

    This kind of dynamic object property is defined as an associated objects, but in the POJO it is defined as an associated ids, called a

    .

The other properties are exactly the same as the original entity definition and need no explanation.

Define Mapper

The most important thing when using mapstruct is to define the Mapper, as follows

BookInputMapper.java
@Mapper
public interface BookInputMapper {

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

This Mapper provides a toBook method to convert a BookInput object to a Book object.

BookInput.id, BookInput.id, BookInput.name and BookInput.price are all non-associated properties that mapstruct can handle well.

note

The issue that BookInput.id can be null while Book.id cannot be null has been discussed before, so it won't be repeated here.

The key is how BookInput.storeId and BookInput.authorIds should be mapped, which falls into two cases:

  • The entity defines @IdView properties

  • The entity does not define @IdView properties

If the entity defines @IdView properties

If the entity type defines @IdView properties, for example:

Book.java
package com.example.model;

import org.babyfish.jimmer.sql.*;
import org.jetbrains.annotations.Nullable;

@Entity
public interface Book {

...omit other properties...

@ManyToOne
@Nullable
BookStore store();

@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_id"
)
List<Author> authors();

@IdView // id view of associated object store
Long storeId();

// id view of all objects in associated collection authors
@IdView("authors")
List<Long> authorIds();
}

In this case, the entity object and POJO correspond completely, and the Mapper does not need any modification.

If the entity does not define @IdView properties

If the entity type does not define @IdView properties, the Mapper needs to be modified:

  • Convert BookInput.storeId to a BookStore object with only the id, then assign it to Book.store

  • Convert BookInput.authorIds to a collection of Author objects with only ids, then assign it to Book.authors

BookInputMapper.java
@Mapper
public interface BookInputMapper {

@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
@Mapping(target = "store", source = "storeId")
@Mapping(target = "authors", source = "authorIds")
Book toBook(BookInput input);

@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = ".")
BookStore toBookStore(Long id);

@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = ".")
Author toAuthor(Long id);
}

Since mapstruct also supports @Mapping(target = "store.id", source = "storeId"), the following syntax can also be used to simplify the code:

BookInputMapper.java
@Mapper
public interface BookInputMapper {

@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
@Mapping(target = "store.id", source = "storeId")
@Mapping(target = "authors", source = "authorIds")
Book toBook(BookInput input);

@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = ".")
Author toAuthor(Long id);
}

Usage

Now we can convert BookInput to Book:

BookInput input = ...omit...;
BookInputMapper mapper = Mappers.getMapper(BookInputMapper.class);
Book book = mapper.toBook(input);

Make POJO implement Input interface

Jimmer provides a simple interface, org.babyfish.jimmer.Input<E>

public interface Input<E> {

E toEntity();
}

Dynamic objects will never implement this interface, this interface should be implemented by user-defined static POJO classes. Its function is very simple, just convert the current static POJO to a dynamic object.

This interface can provide convenience at the syntax level. Whether it is the underlying save command or the top-level spring-data base interface JRepository/KRepository, its sava method directly accepts Input parameters, without the user having to call the Mapper to complete the conversion.

If you want this convenience at the syntax level, you can choose to have the POJO implement this interface by modifying the BookInput code as follows:

BookInput.java
@Data
public class BookInput implements Input<Book> {

private static final Converter CONVERTER =
Mappers.getMapper(Converter.class);

...omit private fields...

@Override
public Book toEntity() {
return CONVERTER.toBook(this);
}

@Mapper
interface Converter {

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

...omit other mapstruct configuration...
}
}
  • BookInput class implements interface org.babyfish.jimmer.Input

  • ❷ Implement Input.toEntity method, use MapStruct to convert the current static Input DTO object to the dynamic Book entity object. This is the only function of this class.