What are DTOs?
DTO stands for Data Transfer Object. They are simple objects used to transfer data between layers of your application, like between the controller and the service layer. Think of them as carriers of information.
Why use DTOs?
- Decoupling: DTOs decouple your controller layer from your database entities. Changes to your database schema don't necessarily require changes to your controller, and vice-versa.
- Security: You can control exactly what data is exposed to the client. You don't want to directly expose your database entities, which might contain sensitive information or internal details.
- Data Transformation: DTOs allow you to transform data as needed. For example, you might want to format a date differently for the client than it's stored in the database.
- Simplified Serialization/Deserialization: DTOs can be tailored for easy serialization (converting to JSON, XML, etc.) and deserialization (converting from JSON, XML, etc.).
- Reduced Network Traffic: You can include only the necessary data in the DTO, reducing the amount of data transferred over the network.
Creating a DTO
Let's say we have a User entity with fields like id, firstName, lastName, email, and password. We don't want to expose the password to the client. We can create a DTO to represent the user data that will be sent to the client.
package com.example.demo.dto;
import lombok.Data; // Requires Lombok dependency
@Data // Lombok annotation for getters, setters, toString, equals, and hashCode
public class UserDto {
private Long id;
private String firstName;
private String lastName;
private String email;
// No password field!
}
Explanation:
@Data(Lombok): This annotation automatically generates getters, setters,toString(),equals(), andhashCode()methods, reducing boilerplate code. You'll need to add the Lombok dependency to yourpom.xml(see Dependencies section below).- Fields: The DTO only includes the fields we want to expose. Notice the absence of the
passwordfield.
Using DTOs in your Controller
Now, let's modify our controller to use the UserDto. We'll also need a way to convert between the User entity and the UserDto. We'll cover that in the next section (ModelMapper or manual mapping).
package com.example.demo.controller;
import com.example.demo.dto.UserDto;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
UserDto userDto = userService.getUserDtoById(id); // Get the DTO from the service
if (userDto == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(userDto);
}
}
Explanation:
- The controller now returns a
UserDtoinstead of aUserentity. - We call a new method
getUserDtoById()in theUserServiceto retrieve the DTO.
Mapping Between Entities and DTOs
We need a way to convert between the User entity and the UserDto. There are several approaches:
1. Manual Mapping:
This is the most explicit approach. You write the code to copy the data from one object to the other.
// In UserService
public UserDto getUserDtoById(Long id) {
User user = userRepository.findById(id).orElse(null);
if (user == null) {
return null;
}
UserDto userDto = new UserDto();
userDto.setId(user.getId());
userDto.setFirstName(user.getFirstName());
userDto.setLastName(user.getLastName());
userDto.setEmail(user.getEmail());
return userDto;
}
2. ModelMapper (Recommended):
ModelMapper is a library that simplifies object mapping. It automatically maps fields with the same name between objects.
Add Dependency:
<dependency> <groupId>org.modelmapper</groupId> <artifactId>modelmapper</artifactId> <version>3.1.1</version> <!-- Check for the latest version --> </dependency>Configure ModelMapper:
// In a configuration class (e.g., AppConfig) import org.modelmapper.ModelMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean public ModelMapper modelMapper() { return new ModelMapper(); } }Use ModelMapper in your Service:
// In UserService import org.modelmapper.ModelMapper; @Autowired private ModelMapper modelMapper; public UserDto getUserDtoById(Long id) { User user = userRepository.findById(id).orElse(null); if (user == null) { return null; } return modelMapper.map(user, UserDto.class); }
3. MapStruct:
MapStruct is another powerful object mapping library that uses code generation to create efficient mapping code. It's more complex to set up than ModelMapper but can offer better performance.
DTOs for Request Bodies (Input)
DTOs aren't just for responses. You can also use them for request bodies to receive data from the client. This allows you to validate the input data before it reaches your service layer.
package com.example.demo.dto;
import lombok.Data;
@Data
public class CreateUserDto {
private String firstName;
private String lastName;
private String email;
private String password; // We'll handle password hashing in the service layer
}
// In your Controller
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@PostMapping
public ResponseEntity<UserDto> createUser(@RequestBody CreateUserDto createUserDto) {
// Validate createUserDto (using Spring Validation - see next section)
User user = userService.createUser(createUserDto);
UserDto userDto = modelMapper.map(user, UserDto.class); // Convert to DTO for response
return ResponseEntity.status(HttpStatus.CREATED).body(userDto);
}
Explanation:
CreateUserDtodefines the structure of the data we expect from the client.@RequestBodybinds the JSON request body to theCreateUserDtoobject.- We'll use Spring Validation (covered in the next section) to validate the
CreateUserDtobefore processing it.
Dependencies (pom.xml)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>