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
.
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
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'sjimmer-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 injimmer-apt
-
For Kotlin,
jimmer-ksp
andjimmer-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.
-
- Java(Maven)
- Java(Gradle)
- Kotlin(Maven)
- Kotlin(Gradle.kts)
- Java(Gradle Plugin)
- Kotlin(Gradle Plugin)
...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...
dependencies {
implementation "org.projectlombok:lombok:${lombok.version}" ➀
implementation "org.mapstruct:mapstruct:${mapstructVersion}" ➊
annotationProcessor "org.projectlombok:lombok:${lombok.version}" ➁
annotationProcessor "org.babyfish.jimmer:jimmer-apt:${jimmerVersion}" ➋
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" ➌
...omit other dependencies...
}
...omit other code...
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<dependencies>
<dependency> ➊
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
...omit other dependencies...
</dependencies>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<compilerPlugins>
<compilerPlugin>ksp</compilerPlugin>
</compilerPlugins>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId> ➌
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.babyfish.jimmer</groupId> ⓐ
<artifactId>jimmer-mapstruct-apt</artifactId>
<version>${jimmer.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
<dependencies>
<dependency>
<groupId>com.dyescape</groupId>
<artifactId>kotlin-maven-symbol-processing</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.babyfish.jimmer</groupId> ➋
<artifactId>jimmer-ksp</artifactId>
<version>${jimmer.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
...omit other code...
plugins {
id("com.google.devtools.ksp") version "1.7.10-1.0.6"
kotlin("kapt") version "1.7.10"
...omit other plugins...
}
dependencies {
implementation("org.mapstruct:mapstruct:${mapstructVersion}") ➊
ksp("org.babyfish.jimmer:jimmer-ksp:${jimmerVersion}") ➋
kapt("org.mapstruct:mapstruct-processor:${mapstructVersion}") ➌
kapt("org.babyfish.jimmer:jimmer-mapstruct-apt:${jimmerVersion}") ⓐ
...omit other dependencies...
}
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/ksp/main/kotlin")
}
}
plugins {
id "tech.argonariod.gradle-plugin-jimmer" version "latest.release"
...omit other plugins...
}
jimmer {
version = "${jimmerVersion}"
...omit other configurations...
}
dependencies {
implementation "org.projectlombok:lombok:${lombok.version}" ➀
implementation "org.mapstruct:mapstruct:${mapstructVersion}" ➊
annotationProcessor "org.projectlombok:lombok:${lombok.version}" ➁
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" ➍
// there's no need to add org.babyfish.jimmer:jimmer-apt to dependencies manually
// when mapstruct-processor dependency is detected,the gradle plugin will add jimmer-apt to dependencies automatically
...omit other dependencies...
}
plugins {
id("tech.argonariod.gradle-plugin-jimmer") version "latest.release"
id("com.google.devtools.ksp") version "1.7.10+"
kotlin("kapt") version "1.7.10"
...omit other plugins...
}
jimmer {
version = "${jimmerVersion}"
...omit other configurations...
}
dependencies {
implementation("org.mapstruct:mapstruct:${mapstructVersion}") ➊
kapt("org.mapstruct:mapstruct-processor:${mapstructVersion}") ➌
...omit other dependencies...
}
In this example, we assume static POJOs are written in Java using lombok.
Language | Location | Description |
---|---|---|
Java and Kotlin | ➊ | Introduce 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 only | ➀ | Introduce lombok dependency for user code to use lombok annotations |
➁ | Use Lombok preprocessor to modify static POJO class code, e.g. add getters, setters | |
Kotlin only | ⓐ | Use jimmer-mapstruct-apt to extend ➌ |
Define POJO
- 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<Long> authorIds;
}
data class BookInput(
val id: Long? = null,
val name: String,
val edition: Int,
val price: BigDecimal,
val storeId: Long?,
val authorIds: List<Long>
)
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
toBook
?tipJimmer 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
- Java
- Kotlin
@Mapper
public interface BookInputMapper {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
Book toBook(BookInput input);
}
@Mapper
interface BookInputMapper {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
fun toBook(input: BookInput): Book
}
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.
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:
- Java
- Kotlin
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();
}
package com.example.model
import org.babyfish.jimmer.sql.*
@Entity
interface Book {
...omit other properties...
@ManyToOne
val store: BookStore?
@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_id"
)
val authors: List<Author>
@IdView // id view of associated object store
val storeId: Long?
// id view of all objects in associated collection authors
@IdView("authors")
val authorIds: List<Long>
}
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 aBookStore
object with only the id, then assign it toBook.store
-
Convert
BookInput.authorIds
to a collection ofAuthor
objects with only ids, then assign it toBook.authors
- Java
- Kotlin
@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);
}
@Mapper
interface BookInputMapper {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
@Mapping(target = "store", source = "storeId")
@Mapping(target = "authors", source = "authorIds")
fun toBook(input: BookInput): Book
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = ".")
fun toBookStore(id: Long?): BookStore
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = ".")
fun toAuthor(id: Long?): Author
}
Since mapstruct also supports @Mapping(target = "store.id", source = "storeId")
, the following syntax can also be used to simplify the code:
- Java
- Kotlin
@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);
}
@Mapper
interface BookInputMapper {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
@Mapping(target = "store.id", source = "storeId")
@Mapping(target = "authors", source = "authorIds")
fun toBook(input: BookInput): Book
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "id", source = ".")
fun toAuthor(id: Long?): Author
}
Usage
Now we can convert BookInput
to Book
:
- Java
- Kotlin
BookInput input = ...omit...;
BookInputMapper mapper = Mappers.getMapper(BookInputMapper.class);
Book book = mapper.toBook(input);
val input: BookInput = ...omit...
val mapper = Mappers.getMapper(BookInputMapper::class.java)
val 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:
- Java
- Kotlin
@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...
}
}
data class BookInput(
...omit...
): Input<Book> { ❶
override fun toEntity(): Book = ❷
CONVERTER.toBook(this)
@Mapper
internal interface Converter {
@BeanMapping(unmappedTargetPolicy = ReportingPolicy.IGNORE)
fun toBook(input: BookInput): Book
...omit other mapstruct configuration...
}
companion object {
@JvmStatic
private val CONVERTER =
Mappers.getMapper(Converter::class.java)
}
}
-
❶
BookInput
class implements interfaceorg.babyfish.jimmer.Input
-
❷ Implement
Input.toEntity
method, use MapStruct to convert the current staticInput DTO
object to the dynamicBook
entity object. This is the only function of this class.