Spring Boot MapStruct Tutorial

Introduction

MapStruct is a Java library that simplifies the mapping between different object models, such as Data Transfer Objects (DTOs) and entities. It generates type-safe mapping code at compile time, reducing boilerplate code and potential runtime errors. In this tutorial, we will demonstrate how to use MapStruct in a Spring Boot application to handle CRUD (Create, Read, Update, Delete) operations using a bookstore example with two entities: Author and Book.

To learn more about MapStruct, check out this guide: MapStruct.

How MapStruct Works 

MapStruct generates the implementation code for mapping methods defined in interfaces annotated with @Mapper. MapStruct processes these interfaces during the compilation process and generates the necessary mapping logic. This approach ensures the mapping code is type-safe, efficient, and easy to maintain.

Prerequisites

  1. Java Development Kit (JDK) 17 or later
  2. Apache Maven installed
  3. An IDE like IntelliJ IDEA or Eclipse

Step 1: Create a Spring Boot Project

You can create a Spring Boot project using Spring Initializr or your IDE.

Using Spring Initializr

  1. Go to Spring Initializr.
  2. Select the following options:
    • Project: Maven Project
    • Language: Java
    • Spring Boot: 3.0.0 or later
    • Group: com.example
    • Artifact: bookstore
    • Name: bookstore
    • Package name: com.example.bookstore
    • Packaging: Jar
    • Java: 17 or later
  3. Add the following dependencies:
    • Spring Web
    • Spring Data JPA
    • H2 Database
    • Spring Boot Starter Test
  4. Click "Generate" to download the project zip file.
  5. Extract the zip file and open the project in your IDE.

Step 2: Add MapStruct Dependencies

Add the following dependencies to your pom.xml file:

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- H2 Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- MapStruct -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.5.3.Final</version>
    </dependency>

    <!-- MapStruct Processor -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.5.3.Final</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Step 3: Configure Application Properties

Add the following properties to src/main/resources/application.properties:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

Step 4: Create Entity and DTO Classes

Create the Author Entity

Create a new Java class named Author in the com.example.bookstore package:

package com.example.bookstore;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Create the Book Entity

Create a new Java class named Book in the com.example.bookstore package:

package com.example.bookstore;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String isbn;

    @ManyToOne
    private Author author;

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public Author getAuthor() {
        return author;
    }

    public void setAuthor(Author author) {
        this.author = author;
    }
}

Create the Author DTO

Create a new Java class named AuthorDTO in the com.example.bookstore package:

package com.example.bookstore;

public class AuthorDTO {

    private Long id;
    private String name;

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Create the Book DTO

Create a new Java class named BookDTO in the com.example.bookstore package:

package com.example.bookstore;

public class BookDTO {

    private Long id;
    private String title;
    private String isbn;
    private AuthorDTO author;

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public AuthorDTO getAuthor() {
        return author;
    }

    public void setAuthor(AuthorDTO author) {
        this.author = author;
    }
}

Step 5: Create the Mapper Interface

Create a new Java interface named BookstoreMapper in the com.example.bookstore package:

package com.example.bookstore;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper
public interface BookstoreMapper {

    BookstoreMapper INSTANCE = Mappers.getMapper(BookstoreMapper.class);

    @Mapping(source = "author", target = "author")
    BookDTO bookToBookDTO(Book book);

    @Mapping(source = "author", target = "author")
    Book bookDTOToBook(BookDTO bookDTO);

    AuthorDTO authorToAuthorDTO(Author author);

    Author authorDTOToAuthor(AuthorDTO authorDTO);
}

Explanation: The BookstoreMapper interface defines the mapping methods between Book, Author and their corresponding DTOs. 

The @Mapper annotation indicates that this is a MapStruct mapper interface. The Mappers.getMapper method creates an instance of the mapper.

Step 6: Create the Repository Interfaces

AuthorRepository

Create a new Java interface named AuthorRepository in the com.example.bookstore package:

package com.example.bookstore;

import org.springframework.data.jpa.repository.JpaRepository;

public interface AuthorRepository extends JpaRepository<Author, Long> {
}

BookRepository

Create a new Java interface named BookRepository in the com.example.bookstore package:

package com.example.bookstore;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {
}

Step 7: Create the Service Classes

AuthorService

Create a new Java class named AuthorService in the com.example.bookstore package:

package com.example.bookstore;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class AuthorService {

    @Autowired
    private AuthorRepository authorRepository;

    @Autowired
    private BookstoreMapper bookstoreMapper;

    public List<AuthorDTO> findAll() {
        return authorRepository.findAll().stream()
                .map(bookstoreMapper::authorToAuthorDTO)
                .collect(Collectors.toList());
    }

    public Optional<AuthorDTO> findById(Long id) {
        return authorRepository.findById(id)
                .map(bookstoreMapper::authorToAuthorDTO);
    }

    public AuthorDTO save(AuthorDTO authorDTO) {
        Author author = bookstoreMapper



.authorDTOToAuthor(authorDTO);
        Author savedAuthor = authorRepository.save(author);
        return bookstoreMapper.authorToAuthorDTO(savedAuthor);
    }

    public void deleteById(Long id) {
        authorRepository.deleteById(id);
    }
}

BookService

Create a new Java class named BookService in the com.example.bookstore package:

package com.example.bookstore;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class BookService {

    @Autowired
    private BookRepository bookRepository;

    @Autowired
    private BookstoreMapper bookstoreMapper;

    public List<BookDTO> findAll() {
        return bookRepository.findAll().stream()
                .map(bookstoreMapper::bookToBookDTO)
                .collect(Collectors.toList());
    }

    public Optional<BookDTO> findById(Long id) {
        return bookRepository.findById(id)
                .map(bookstoreMapper::bookToBookDTO);
    }

    public BookDTO save(BookDTO bookDTO) {
        Book book = bookstoreMapper.bookDTOToBook(bookDTO);
        Book savedBook = bookRepository.save(book);
        return bookstoreMapper.bookToBookDTO(savedBook);
    }

    public void deleteById(Long id) {
        bookRepository.deleteById(id);
    }
}

Explanation: The AuthorService and BookService classes contain methods for CRUD operations. 

They use AuthorRepository and BookRepository to interact with the database and BookstoreMapper to map between Author, Book and their corresponding DTOs.

Step 8: Create the Controller Classes

AuthorController

Create a new Java class named AuthorController in the com.example.bookstore package:

package com.example.bookstore;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/authors")
public class AuthorController {

    @Autowired
    private AuthorService authorService;

    @GetMapping
    public List<AuthorDTO> getAllAuthors() {
        return authorService.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<AuthorDTO> getAuthorById(@PathVariable Long id) {
        return authorService.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public AuthorDTO createAuthor(@RequestBody AuthorDTO authorDTO) {
        return authorService.save(authorDTO);
    }

    @PutMapping("/{id}")
    public ResponseEntity<AuthorDTO> updateAuthor(@PathVariable Long id, @RequestBody AuthorDTO authorDTO) {
        return authorService.findById(id)
                .map(existingAuthor -> {
                    authorDTO.setId(existingAuthor.getId());
                    return ResponseEntity.ok(authorService.save(authorDTO));
                })
                .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteAuthor(@PathVariable Long id) {
        return authorService.findById(id)
                .map(author -> {
                    authorService.deleteById(id);
                    return ResponseEntity.noContent().build();
                })
                .orElse(ResponseEntity.notFound().build());
    }
}

BookController

Create a new Java class named BookController in the com.example.bookstore package:

package com.example.bookstore;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService;

    @GetMapping
    public List<BookDTO> getAllBooks() {
        return bookService.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<BookDTO> getBookById(@PathVariable Long id) {
        return bookService.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public BookDTO createBook(@RequestBody BookDTO bookDTO) {
        return bookService.save(bookDTO);
    }

    @PutMapping("/{id}")
    public ResponseEntity<BookDTO> updateBook(@PathVariable Long id, @RequestBody BookDTO bookDTO) {
        return bookService.findById(id)
                .map(existingBook -> {
                    bookDTO.setId(existingBook.getId());
                    return ResponseEntity.ok(bookService.save(bookDTO));
                })
                .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        return bookService.findById(id)
                .map(book -> {
                    bookService.deleteById(id);
                    return ResponseEntity.noContent().build();
                })
                .orElse(ResponseEntity.notFound().build());
    }
}

Step 9: Create the Main Application Class

Create a main application class named BookstoreApplication in the com.example.bookstore package:

package com.example.bookstore;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BookstoreApplication {

    public static void main(String[] args) {
        SpringApplication.run(BookstoreApplication.class, args);
    }
}

Explanation: The BookstoreApplication class contains the main method, which is the entry point of the Spring Boot application. The @SpringBootApplication annotation is a convenience annotation that adds all the following:

  • @Configuration: Tags the class as a source of bean definitions for the application context.
  • @EnableAutoConfiguration: Tells Spring Boot to start adding beans based on classpath settings, other beans, and various property settings.
  • @ComponentScan: Tells Spring to look for other components, configurations, and services in the specified package.

Step 10: Test the Application

Start your Spring Boot application and use tools like Postman or curl to test the CRUD operations for both Author and Book entities.

Create an Author

  • Method: POST
  • URL: http://localhost:8080/authors
  • Body:
    {
        "name": "Chetan Bhagat"
    }
    

Get All Authors

  • Method: GET
  • URL: http://localhost:8080/authors

Create a Book

  • Method: POST
  • URL: http://localhost:8080/books
  • Body:
    {
        "title": "Five Point Someone",
        "isbn": "978-8129104595",
        "author": {
            "id": 1,
            "name": "Chetan Bhagat"
        }
    }
    

Get All Books

  • Method: GET
  • URL: http://localhost:8080/books

Get a Book by ID

  • Method: GET
  • URL: http://localhost:8080/books/{id}

Update a Book

  • Method: PUT
  • URL: http://localhost:8080/books/{id}
  • Body:
    {
        "title": "Two States",
        "isbn": "978-8129115300",
        "author": {
            "id": 1,
            "name": "Chetan Bhagat"
        }
    }
    

Delete a Book

  • Method: DELETE
  • URL: http://localhost:8080/books/{id}

Conclusion

In this tutorial, we demonstrated how to use MapStruct to handle CRUD operations in a Spring Boot application using a bookstore example with two entities: Author and Book. We covered the creation of entities, DTOs, mappers, repositories, services, and controllers. 

By following these steps, you can efficiently use MapStruct to map between different object models and simplify your codebase. This approach ensures that your code is type-safe and easy to maintain and reduces the boilerplate code required for manual mapping.

Comments