[Java Backend Zero to Hello] BÀI 6.3: DTO & MAPPER
📚 Bài viết thuộc series Java Backend Zero to Hello 📌 Phần: Phase 6: REST API & Best Practices | Bài 62/86
BÀI 6.3: DTO & MAPPER
Mục tiêu
- Hiểu DTO pattern
- Tách biệt Entity và DTO
- Sử dụng MapStruct
- Áp dụng vào project thực tế
1. TẠI SAO CẦN DTO?
Vấn đề khi trả Entity trực tiếp
// ❌ Trả entity trực tiếp
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
}
Vấn đề:
- Lộ thông tin nhạy cảm (password, internal fields)
- Khó thay đổi API mà không ảnh hưởng DB
- Lazy loading exception
- Circular reference
Giải pháp - DTO
// ✅ Trả DTO
@GetMapping("/{id}")
public UserResponse getById(@PathVariable Long id) {
User user = userRepository.findById(id).orElseThrow();
return UserResponse.from(user);
}
2. CÁC LOẠI DTO
2.1 Request DTO
public record CreateUserRequest(
@NotBlank String name,
@Email String email,
@Min(0) Integer age
) {}
2.2 Response DTO
public record UserResponse(
Long id,
String name,
String email,
Integer age,
LocalDateTime createdAt
) {
public static UserResponse from(User user) {
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getAge(),
user.getCreatedAt()
);
}
}
2.3 Update DTO
public record UpdateUserRequest(
@Size(min = 2, max = 100) String name,
@Email String email,
@Min(0) @Max(150) Integer age
) {}
2.4 Summary DTO (cho list)
public record UserSummary(
Long id,
String name,
String email
) {}
3. MAP THỦ CÔNG
3.1 Static factory method
public record UserResponse(...) {
public static UserResponse from(User user) {
return new UserResponse(...);
}
public static List<UserResponse> fromList(List<User> users) {
return users.stream().map(UserResponse::from).toList();
}
}
3.2 Service method
@Service
public class UserService {
public UserResponse toResponse(User user) {
return new UserResponse(...);
}
}
4. MAPSTRUCT
MapStruct tự động generate code map giữa các object.
4.1 Dependency
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
4.2 Mapper interface
@Mapper(componentModel = "spring")
public interface UserMapper {
UserResponse toResponse(User user);
List<UserResponse> toResponseList(List<User> users);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
User toEntity(CreateUserRequest request);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
void updateEntity(UpdateUserRequest request, @MappingTarget User user);
}
4.3 Sử dụng
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
public UserResponse getById(Long id) {
User user = userRepository.findById(id).orElseThrow();
return userMapper.toResponse(user);
}
public UserResponse create(CreateUserRequest request) {
User user = userMapper.toEntity(request);
User saved = userRepository.save(user);
return userMapper.toResponse(saved);
}
}
4.4 Custom mapping
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "fullName", source = "name")
@Mapping(target = "emailAddress", source = "email")
UserResponse toResponse(User user);
@Mapping(target = "createdAt",
expression = "java(java.time.LocalDateTime.now())")
User toEntity(CreateUserRequest request);
}
5. MODELMAPPER
Thư viện mapping phổ biến khác.
5.1 Dependency
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version>
</dependency>
5.2 Cấu hình
@Configuration
public class ModelMapperConfig {
@Bean
public ModelMapper modelMapper() {
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.STRICT)
.setFieldMatchingEnabled(true)
.setSkipNullEnabled(true);
return mapper;
}
}
5.3 Sử dụng
@Service
@RequiredArgsConstructor
public class UserService {
private final ModelMapper modelMapper;
public UserResponse toResponse(User user) {
return modelMapper.map(user, UserResponse.class);
}
}
6. SO SÁNH
| MapStruct | ModelMapper |
|---|---|
| Compile-time | Runtime |
| Type-safe | Reflection |
| Nhanh hơn | Chậm hơn |
| Code rõ ràng | Ít code |
| Cần khai báo | Tự động |
7. BEST PRACTICES
7.1 ✅ NÊN
// Tách biệt Entity và DTO
public class User { ... } // Entity
public record UserResponse(...) { ... } // DTO
// Dùng MapStruct cho project lớn
@Mapper(componentModel = "spring")
public interface UserMapper { ... }
// Dùng record cho DTO (immutable)
public record UserResponse(Long id, String name) {}
7.2 ❌ TRÁNH
// Trả entity trực tiếp
return userRepository.findById(id); // ❌
// Dùng entity làm DTO
public class UserDTO extends User { ... } // ❌
// Map thủ công nhiều field
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getAge(),
user.getCreatedAt(),
user.getUpdatedAt(),
// ... 20 fields
);
8. VÍ DỤ HOÀN CHỈNH
8.1 Entity
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
private String password; // Không lộ ra ngoài
private Integer age;
private LocalDateTime createdAt;
// getters, setters
}
8.2 DTOs
public record CreateUserRequest(
@NotBlank String name,
@Email String email,
@NotBlank @Size(min = 8) String password,
@Min(0) Integer age
) {}
public record UpdateUserRequest(
String name,
String email,
@Min(0) Integer age
) {}
public record UserResponse(
Long id,
String name,
String email,
Integer age,
LocalDateTime createdAt
) {}
public record UserSummary(
Long id,
String name
) {}
8.3 Mapper
@Mapper(componentModel = "spring")
public interface UserMapper {
UserResponse toResponse(User user);
List<UserResponse> toResponseList(List<User> users);
UserSummary toSummary(User user);
@Mapping(target = "id", ignore = true)
@Mapping(target = "password", ignore = true)
@Mapping(target = "createdAt", ignore = true)
User toEntity(CreateUserRequest request);
@Mapping(target = "id", ignore = true)
@Mapping(target = "password", ignore = true)
@Mapping(target = "createdAt", ignore = true)
void updateEntity(UpdateUserRequest request, @MappingTarget User user);
}
8.4 Service
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
public UserResponse create(CreateUserRequest request) {
User user = userMapper.toEntity(request);
user.setPassword(passwordEncoder.encode(request.password()));
User saved = userRepository.save(user);
return userMapper.toResponse(saved);
}
public UserResponse update(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id).orElseThrow();
userMapper.updateEntity(request, user);
User saved = userRepository.save(user);
return userMapper.toResponse(saved);
}
public UserResponse getById(Long id) {
return userMapper.toResponse(
userRepository.findById(id).orElseThrow()
);
}
public List<UserSummary> getAll() {
return userRepository.findAll().stream()
.map(userMapper::toSummary)
.toList();
}
}
9. BÀI TẬP THỰC HÀNH
Bài 1: User DTO
Tạo đầy đủ DTO cho User: Create, Update, Response, Summary.
Bài 2: MapStruct
Cấu hình MapStruct và tạo mapper cho User.
Bài 3: Service Integration
Tích hợp mapper vào UserService.
10. TÓM TẮT
| Khái niệm | Mô tả |
|---|---|
| DTO | Data Transfer Object |
| Entity | Ánh xạ database |
| MapStruct | Code generation |
| ModelMapper | Reflection-based |
| @Mapping | Custom mapping |
| @MappingTarget | Update existing |
Bài tiếp theo: 6.4 SOLID Principles
🧭 Điều Hướng Series
⬅️ Bài trước: BÀI 6.2: API DOCUMENTATION (OPENAPI/SWAGGER)
📋 Lộ trình tổng quan: Xem Toàn Bộ Series
➡️ Bài tiếp theo: BÀI 6.4: SOLID PRINCIPLES
All rights reserved