Automating Code with MapStruct

Often, we need to convert domain model to DTO (Data transfer object) and vice versa. This is necessary when transferring data to frontend or remote interfaces. Dealing with complex mappings manually becomes cumbersome and may result in errors.  

In this article, I would like to introduce MapStruct, a Java annotation processor that generates mapper implementations for Java beans at compile time. It uses plain Java method invocations for mapping objects and no reflection or runtime processing is involved.

I will walk you through the steps to integrate MapStruct in a Spring Boot application.

Maven Dependency

Let’s create a sample Spring Boot application and add MapStruct and its processor dependencies in pom.xml.

The mapstruct-processor is used for generating mapper implementation during compilation.

<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct-jdk8</artifactId>
   <version>1.3.0.Final</version>
</dependency>
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct-processor</artifactId>
   <version>1.3.0.Final</version>
   <scope>provided</scope>
</dependency>

Creating JPA Entities and DTOs

Let’s create two entities Library and Book.

The mapstruct-processor is used for generating mapper implementation during compilation.

package com.swathisprasad.mapstruct.dao.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;

import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Data
@EqualsAndHashCode
@Entity
public class Library implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue (generator = "uuid")
    @GenericGenerator (name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
    @Column (name = "id", updatable = false, nullable = false)
    private UUID id;

    @NotNull
    @Column (name = "name", nullable = false)
    private String name;

    @EqualsAndHashCode.Exclude
    @OneToMany(mappedBy = "library", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Book> books = new ArrayList<>();

}
package com.swathisprasad.mapstruct.dao.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;

@Data
@EqualsAndHashCode
@Entity
public class Book implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue (generator="uuid")
    @GenericGenerator (name="uuid", strategy="org.hibernate.id.UUIDGenerator")
    @Column (name="id", updatable=false, nullable=false)
    private UUID id;

    @NotNull
    @Column (name="name", nullable=false)
    private String name;

    @NotNull
    @Column (name="author", nullable=false)
    private String author;

    @NotNull
    @Column (name="published_date", nullable=false)
    private LocalDateTime publishedDate;

    @EqualsAndHashCode.Exclude
    @ToString.Exclude
    @ManyToOne(optional=false)
    @JoinColumn (name="library_id", nullable=false)
    private Library library;

}

Here, we have one-to-many bidirectional relationship between Library and Book. Also, I have added Lombok to the project to avoid writing boilerplate code such as getters, setters, hashCode() and Equals() in the entity classes.

Let’s create corresponding DTO classes.

package com.swathisprasad.mapstruct.dto;

import lombok.Data;

import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import java.util.UUID;

@Data
public class LibraryDTO implements Serializable {

    private UUID id;

    @NotNull
    private String name;


    @NotNull
    private List<BookDTO> bookDTOs = new ArrayList<>();

}
package com.swathisprasad.mapstruct.dto;

import lombok.Data;

import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;

@Data
public class BookDTO implements Serializable{

    private UUID id;

    @NotNull
    private String name;

    @NotNull
    private String author;

    @NotNull
    private LocalDateTime publishedDate;

    private UUID libraryId;
}

Bean Mappers

Now, we are going to create mapper interfaces in which we will make use of MapStruct.

package com.swathisprasad.mapstruct.dto.mapper;

import com.swathisprasad.mapstruct.dao.entity.Library;
import com.swathisprasad.mapstruct.dto.LibraryDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

import java.util.UUID;

@Mapper(componentModel = "spring", uses = {BookMapper.class})
public interface LibraryMapper extends IEntityMapper<LibraryDTO, Library> {

    @Mapping(source = "books", target = "bookDTOs")
    LibraryDTO toDto(final Library library);

    Library toEntity(final LibraryDTO libraryDTO);

    default Library fromId(final UUID id) {

        if (id == null) {
            return null;
        }

        final Library library=new Library();

        library.setId(id);

        return library;
    }
}

As you notice here, we are not writing any implementation since MapStruct generates for us. The mapper interfaces must be annotated with @Mapper annotation. Here, componentModel attribute which generates a singleton-scoped Spring bean mapper and it can be injected directly when we need it. Since we have defined one-to-many relationship between Library and Book, we need to map children i.e., list of Book objects, we inject other BookMapper through uses attribute.  

We tell Mapstruct to map the children in one-to-many relationship i.e., Book in our case through @Mapping annotation.

package com.swathisprasad.mapstruct.dto.mapper;


import com.swathisprasad.mapstruct.dao.entity.Book;
import com.swathisprasad.mapstruct.dto.BookDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

import java.util.List;
import java.util.UUID;

@Mapper(componentModel = "spring", uses = {LibraryMapper.class})
public interface BookMapper extends IEntityMapper<BookDTO, Book> {

    @Mapping(source="library.id", target="libraryId")
    BookDTO toDto(final Book book);

    List<BookDTO> toDto(final List<Book> book);

    @Mapping(source="libraryId", target="library")
    Book toEntity(final BookDTO bookDTO);

    List<Book> toEntity(final List <BookDTO> bookDTOs);

    default Book fromId(final UUID id) {

        if (id == null) {
            return null;
        }

        final Book book=new Book();
        book.setId(id);

        return book;
    }
}

We can execute mvn clean install or mvn clean verify to trigger MapStruct processing, which will generate the implementation class under /target/generated-sources/annotations/.

Testing the Mapper Interfaces

Let’s create JPA repositories for Library and Book.

package com.swathisprasad.mapstruct.dao.repository;

import com.swathisprasad.mapstruct.dao.entity.Library;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.UUID;

@Repository
public interface ILibraryRepository extends JpaRepository<Library, UUID> {

}
package com.swathisprasad.mapstruct.dao.repository;

import com.swathisprasad.mapstruct.dao.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.UUID;

@Repository
public interface IBookRepository extends JpaRepository<Book, UUID> {
}

Create a LibraryService for saving Library and Book entities in database.

package com.swathisprasad.mapstruct.service;

import com.swathisprasad.mapstruct.dao.entity.Book;
import com.swathisprasad.mapstruct.dao.entity.Library;
import com.swathisprasad.mapstruct.dao.repository.IBookRepository;
import com.swathisprasad.mapstruct.dao.repository.ILibraryRepository;
import com.swathisprasad.mapstruct.dto.LibraryDTO;
import com.swathisprasad.mapstruct.dto.mapper.BookMapper;
import com.swathisprasad.mapstruct.dto.mapper.LibraryMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
@Transactional
public class LibraryService {

    private final ILibraryRepository libraryRepository;
    private final IBookRepository bookRepository;
    private final LibraryMapper libraryMapper;
    private final BookMapper bookMapper;

    public LibraryService(final ILibraryRepository libraryRepository, final IBookRepository bookRepository,
                          final LibraryMapper libraryMapper, final BookMapper bookMapper) {
        this.libraryRepository = libraryRepository;
        this.bookRepository = bookRepository;
        this.libraryMapper = libraryMapper;
        this.bookMapper = bookMapper;
    }

    public LibraryDTO save(final LibraryDTO libraryDTO) {
        final Library library = libraryMapper.toEntity(libraryDTO);

        final Library createdLibrary = libraryRepository.save(library);
        final List<Book> books = new ArrayList<>();

        libraryDTO.getBookDTOs().forEach(bookDTO -> {

            final Book book = bookMapper.toEntity(bookDTO);
            book.setLibrary(createdLibrary);
            books.add(book);

        });
        createdLibrary.setBooks(books);
        bookRepository.saveAll(books);

        return libraryMapper.toDto(createdLibrary);
    }
}

Here is the test case:

package com.swathisprasad.mapstruct.service;

import com.swathisprasad.mapstruct.Application;
import com.swathisprasad.mapstruct.dto.BookDTO;
import com.swathisprasad.mapstruct.dto.LibraryDTO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith (SpringExtension.class)
@SpringBootTest (classes = Application.class)
public class LibraryServiceTest{

    @Autowired
    private LibraryService libraryService;

    @Test
    public void saveLibrary(){
        final LibraryDTO libraryDTO=new LibraryDTO();
        libraryDTO.setName("Library");
        final List<BookDTO> books=new ArrayList<>();
        final BookDTO book1=new BookDTO();
        book1.setName("Book1");
        book1.setAuthor("Author1");
        book1.setPublishedDate(LocalDateTime.now());
        books.add(book1);

        final BookDTO book2=new BookDTO();
        book2.setName("Book2");
        book2.setAuthor("Author2");
        book2.setPublishedDate(LocalDateTime.now().minusDays(10));
        books.add(book2);

        libraryDTO.setBookDTOs(books);
        final LibraryDTO createdLibraryDTO = libraryService.save(libraryDTO);

        final List<BookDTO> createdBooks=createdLibraryDTO.getBookDTOs();
        assertThat(createdBooks).hasSize(2);
        createdBooks.forEach(bookDTO -> assertThat(bookDTO.getLibraryId()).isEqualTo(createdLibraryDTO.getId()));
    }
}

Conclusion

MapStruct aims at simplifying work by automating it as much as possible. This article has just scratched the surface of MapStruct’s capabilities. It has a lot more features. Checkout the complete API guide here.

The complete source code for this article can be found on my GitHub repository.  Let me know if you have any comments or suggestions.

Advertisements

Discussion

This site uses Akismet to reduce spam. Learn how your comment data is processed.