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
- Java Development Kit (JDK) 17 or later
- Apache Maven installed
- 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
- Go to Spring Initializr.
- 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
- Add the following dependencies:
- Spring Web
- Spring Data JPA
- H2 Database
- Spring Boot Starter Test
- Click "Generate" to download the project zip file.
- 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
Post a Comment
Leave Comment