DEV Community

Thomas
Thomas

Posted on • Updated on • Originally published at bootify.io

Using MapStruct with Maven and Lombok

MapStruct is a Java library to simplify data transfer between classes and avoid writing boilerplate code. The following article will show the steps to automate the mapping between a JPA entity and a DTO in a Spring Boot application.

Backgrounds

For our example, we use the following technologies:

As developers, all we need to do is provide an interface that defines the desired mapping methods. MapStruct then generates the implementing class at build time - so our initial setup is a bit more extensive.

Maven Setup

In our pom.xml, we first need to add the dependency that will later allow us to define our mapper.

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.2.Final</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Furthermore, we need to extend the maven-compiler-plugin to activate the code generation of MapStruct.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.10.1</version>
    <configuration>
        <source>11</source>
        <target>11</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.24</version>
            </path>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.5.2.Final</version>
            </path>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok-mapstruct-binding</artifactId>
                <version>0.2.0</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode

Lombok is our first annotation processor, followed directly by MapStruct. Another reference to lombok-mapstruct-binding is necessary for these two libraries to work together. Without Lombok, only the mapstruct-processor would be needed at this point.

Mapper Interface

Now let's get to the core of our example - the mapper. In our example, there is a "CarPart" entity that has a primary key, two data fields as well as a reference to a "Supplier".

@Entity
@Getter
@Setter
public class CarPart {

    @Id
    @Column(nullable = false, updatable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String typeCode;

    @Column(nullable = false)
    private OffsetDateTime releaseDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "supplier_id", nullable = false)
    private Supplier supplier;

}
Enter fullscreen mode Exit fullscreen mode

  Our JPA Entity example

Our DTO contains all fields of the entity with further annotations for validation when used in a RestController. The reference to the Supplier is specified in form of the foreign key id.

@Getter
@Setter
public class CarPartDTO {

    private Long id;

    @NotNull
    @Size(max = 255)
    private String typeCode;

    @NotNull
    private OffsetDateTime releaseDate;

    @NotNull
    private Long supplier;

}
Enter fullscreen mode Exit fullscreen mode

  Our example DTO

With these classes in place, we can now define the first version of our mapper. The "CarPartMapper" contains two methods for mapping in both directions. By annotating the interface with @Mapper, MapStruct will parse it and provide an implementing class. With componentModel = "spring" the class is added to Spring's application context and can be referenced later on in our service using @Autowired.

@Mapper(
        componentModel = "spring",
        unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface CarPartMapper {

    @Mapping(target = "supplier", ignore = true)
    CarPartDTO updateCarPartDTO(CarPart carPart, @MappingTarget CarPartDTO carPartDTO);

    @Mapping(target = "id", ignore = true)
    @Mapping(target = "supplier", ignore = true)
    CarPart updateCarPart(CarPartDTO carPartDTO, @MappingTarget CarPart carPart,
            @Context SupplierRepository supplierRepository);

}
Enter fullscreen mode Exit fullscreen mode

  First version of our Mapper

The provided methods are automatically parsed by MapStruct. One parameter is the source object and the parameter with @MappingTarget defines the target object. Without a @MappingTarget the target object would be newly initialized; this however isn' t desired in our case. The class generated by MapStruct will automatically map all fields with the same name - in our case id, typeCode and releaseDate.

With the @Mapping annotation we can add special handlings for single fields. Since the id field is automatically generated by our persistence layer (GenerationType.IDENTITY), it should not be taken from the DTO to the entity. For the reference to the Supplier we want to add a custom handling, so this field should be ignored as well.

Mapping the foreign key

Lastly, we want to handle the reference to the Supplier by adding the following two default methods to our interface:

@AfterMapping
default void afterUpdateCarPartDTO(CarPart carPart, @MappingTarget CarPartDTO carPartDTO) {
    carPartDTO.setSupplier(carPart.getSupplier() == null ? null : carPart.getSupplier().getId());
}

@AfterMapping
default void afterUpdateCarPart(CarPartDTO carPartDTO, @MappingTarget CarPart carPart,
        @Context SupplierRepository supplierRepository) {
    if (carPartDTO.getSupplier() != null && (carPart.getSupplier() == null || !carPart.getSupplier().getId().equals(carPartDTO.getSupplier()))) {
        final Supplier supplier = supplierRepository.findById(carPartDTO.getSupplier())
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "supplier not found"));
        carPart.setSupplier(supplier);
    }
}
Enter fullscreen mode Exit fullscreen mode

  Adding custom behaviour with @AfterMapping

By annotating them with @AfterMapping we tell MapStruct to call the method after the initial mapping. With @Context we can add additional parameters to the method - we use this ability here to provide the SupplierRepository. If the specified Supplier does not exist, a 404 error should be returned.

With this, our mapper is ready and can be used in our service.

public Long create(final CarPartDTO carPartDTO) {
    final CarPart carPart = new CarPart();
    carPartMapper.updateCarPart(carPartDTO, carPart, supplierRepository);
    return carPartRepository.save(carPart).getId();
}
Enter fullscreen mode Exit fullscreen mode

  Using our new Mapper for creating a CarPart

Bootify provides a free tool to create a Spring Boot application with a custom database schema and REST API. The Professional Plan also provides an option for MapStruct, so the required mappers are automatically generated and added to your code base.

» Learn more

Top comments (0)