DTO — Data Transfer Object
នៅក្នុង Spring Boot
មេរៀននេះពន្យល់លំអិតអំពី DTO, គោលការណ៍, Security benefit, របៀប Implement, Mapping Strategy, Validation, និង Best Practices ជាមួយ Code ឧទាហរណ៏ជាក់ស្តែង។
DTO (Data Transfer Object) គឺជា Design Pattern មួយ ដែលប្រើសម្រាប់ ផ្ទេរទិន្នន័យ រវាងស្រទាប់ (Layers) ផ្សេងៗនៃ Application ។ DTO មិនមាន Business Logic ទេ — វាគ្រាន់តែជា Container ដែលផ្ទុកទិន្នន័យ ដើម្បី transport ពីកន្លែងមួយទៅកន្លែងមួយទៀត។
ការប្រើ Entity ដោយផ្ទាល់ (ដោយមិនមាន DTO) អាចបង្ករឲ្យមានបញ្ហាធំៗ:
❌ Return Entity ដោយផ្ទាល់
- ✗បង្ហាញ password, role, isDeleted ដល់ Client
- ✗Security Risk ខ្ពស់ណាស់
- ✗ការផ្លាស់ប្ដូរ DB Column ប៉ះពាល់ API ភ្លាមៗ
- ✗Circular Reference ជាមួយ JPA Relations
- ✗ពិបាក Validate Request Data
- ✗Return Data ច្រើនជាងការចាំបាច់
✅ ប្រើ DTO
- ✓Control ជាក់លាក់ — ចង់ return field ណា
- ✓Security ខ្ពស់ — លាក់ sensitive data
- ✓API Stable — DB ផ្លាស់ API មិនប្ដូរ
- ✓ដោះស្រាយ Circular Reference
- ✓Validate Request ងាយ ជាមួយ Annotations
- ✓Flexible format — DP ផ្ទុយពី DB
ស្រមៃថាអ្នកមាន User Entity ដែលផ្ទុកទិន្នន័យទាំងអស់ ប៉ុន្ដែចង់ return ត្រឹម id, name, email ប៉ុណ្ណោះ:
@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; // ❌ ទាំងនេះ មិនគួរ expose ដល់ Client private String password; // Sensitive! private String role; // Internal! private Boolean isDeleted; // Internal! private LocalDateTime createdAt; // Getters & Setters... }
// ✅ Return តែ fields ដែលអ្នកចង់ public public class UserResponseDTO { private Long id; private String name; private String email; // Constructor public UserResponseDTO(Long id, String name, String email) { this.id = id; this.name = name; this.email = email; } // ✅ password, role, isDeleted — NEVER exposed! // Getters only (no setters — immutable) public Long getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } }
record ជំនួស class ធម្មតា — កាន់តែខ្លី ស្អាត ជាង:
public record UserResponseDTO(Long id, String name, String email) {}
ជាធម្មតា យើងបែងចែក DTO ជាពីរប្រភេទសំខាន់:
// DTO ទទួលទិន្នន័យពី Client (POST/PUT requests) public class CreateUserRequestDTO { @NotBlank(message = "Name មិនអាចទទេ") @Size(min = 2, max = 50) private String name; @Email(message = "Email format មិនត្រឹមត្រូវ") @NotBlank(message = "Email មិនអាចទទេ") private String email; @NotBlank @Size(min = 8, message = "Password យ៉ាងតិច 8 តួអក្សរ") private String password; // Getters & Setters (or use Lombok @Data) }
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; // POST /api/users — Create user @PostMapping public ResponseEntity<UserResponseDTO> createUser( @Valid @RequestBody CreateUserRequestDTO request) { UserResponseDTO response = userService.createUser(request); return ResponseEntity.status(201).body(response); } // GET /api/users/{id} — Get user by ID @GetMapping("/{id}") public ResponseEntity<UserResponseDTO> getUserById( @PathVariable Long id) { UserResponseDTO response = userService.findById(id); return ResponseEntity.ok(response); } // GET /api/users — Get all users @GetMapping public ResponseEntity<List<UserResponseDTO>> getAllUsers() { return ResponseEntity.ok(userService.findAll()); } // PUT /api/users/{id} — Update user @PutMapping("/{id}") public ResponseEntity<UserResponseDTO> updateUser( @PathVariable Long id, @Valid @RequestBody UpdateUserRequestDTO request) { UserResponseDTO response = userService.updateUser(id, request); return ResponseEntity.ok(response); } }
@Service public class UserService { @Autowired private UserRepository userRepository; @Autowired private PasswordEncoder passwordEncoder; // Create user — Request DTO → Entity → Response DTO public UserResponseDTO createUser(CreateUserRequestDTO request) { // Step 1: DTO → Entity User user = new User(); user.setName(request.getName()); user.setEmail(request.getEmail()); user.setPassword(passwordEncoder.encode(request.getPassword())); user.setRole("USER"); user.setIsDeleted(false); // Step 2: Save to DB User saved = userRepository.save(user); // Step 3: Entity → Response DTO return toResponseDTO(saved); } // Find by ID public UserResponseDTO findById(Long id) { User user = userRepository.findById(id) .orElseThrow(() -> new RuntimeException("User not found: " + id)); return toResponseDTO(user); } // Find all users public List<UserResponseDTO> findAll() { return userRepository.findAll() .stream() .map(this::toResponseDTO) .collect(Collectors.toList()); } // ─── Private Helper Method ─── private UserResponseDTO toResponseDTO(User user) { return new UserResponseDTO( user.getId(), user.getName(), user.getEmail() ); } }
private toResponseDTO() method ដាច់ដោយឡែក
ដើម្បីឲ្យ code ស្អាត និង Reusable ។ ឬ អាចបង្កើត UserMapper class ដាច់ស្រុត។
មាន 3 វិធីសំខាន់ក្នុងការ Map DTO ↔ Entity:
Manual Mapping — សរសេរដោយខ្លួនឯង
ច្បាស់លាស់ (explicit), ងាយ Debug, ល្អសម្រាប់ Project តូចឬ fields មួយចំនួន
MapStruct — Code Generation (★ ណែនាំ)
Generate Mapper code ដោយ Annotation processing — Performance ល្អ, Type-safe, ល្អសម្រាប់ Project ធំ
ModelMapper — Reflection-based
ងាយប្រើ, Auto-map fields ដែលឈ្មោះដូចគ្នា, Performance ទាបជាង MapStruct
// pom.xml: mapstruct + mapstruct-processor dependencies @Mapper(componentModel = "spring") public interface UserMapper { // Entity → Response DTO (auto-map same-name fields) UserResponseDTO toResponseDTO(User user); // Request DTO → Entity @Mapping(target = "id", ignore = true) @Mapping(target = "role", constant = "USER") @Mapping(target = "isDeleted", constant = "false") User toEntity(CreateUserRequestDTO dto); // List mapping — MapStruct auto-handles collections List<UserResponseDTO> toResponseDTOList(List<User> users); } // ─── Usage in Service ─── @Autowired private UserMapper userMapper; public UserResponseDTO createUser(CreateUserRequestDTO request) { User user = userMapper.toEntity(request); User saved = userRepository.save(user); return userMapper.toResponseDTO(saved); }
Spring Boot ប្រើ Jakarta Validation (Bean Validation 3.0) ដើម្បី Validate
ទិន្នន័យពី Client — ត្រូវ add spring-boot-starter-validation dependency ជាមុន:
| Annotation | ការពន្យល់ (Khmer) | ឧទាហរណ៏ |
|---|---|---|
| @NotNull | មិនអនុញ្ញាត null (ទទេ blank អាចបាន) | @NotNull |
| @NotBlank | មិនអនុញ្ញាត null, empty, whitespace-only | @NotBlank |
| ត្រូជា Email format valid | ||
| @Size | String length ឬ Collection size | @Size(min=2, max=50) |
| @Min / @Max | តម្លៃ numeric តិច/ច្រើនបំផុត | @Min(18) |
| @Pattern | Match Regex pattern | @Pattern(regexp="...") |
| @Positive | Number ត្រូវជាលេខវិជ្ជមាន (> 0) | @Positive |
| @Future | Date ត្រូវនៅពេលអនាគត | @Future |
public class RegisterRequestDTO { @NotBlank(message = "ឈ្មោះមិនអាចទទេ") @Size(min = 2, max = 50, message = "ឈ្មោះត្រូវ 2-50 តួអក្សរ") private String name; @NotBlank(message = "Email មិនអាចទទេ") @Email(message = "Email format មិនត្រឹមត្រូវ") private String email; @NotBlank(message = "Password មិនអាចទទេ") @Size(min = 8, message = "Password ត្រូវការយ៉ាងតិច 8 តួ") private String password; @NotNull(message = "អាយុមិនអាចខ្វះ") @Min(value = 18, message = "អ្នកត្រូវមានអាយុ 18 ឡើង") @Max(value = 120) private Integer age; @Pattern( regexp = "^(012|017|096|097|098|086)[0-9]{6}$", message = "លេខទូរស័ព្ទ Cambodia មិនត្រឹមត្រូវ" ) private String phone; }
@RestControllerAdvice public class GlobalExceptionHandler { // Handle @Valid validation errors @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, String>> handleValidation( MethodArgumentNotValidException ex) { Map<String, String> errors = new LinkedHashMap<>(); ex.getBindingResult() .getFieldErrors() .forEach(err -> errors.put(err.getField(), err.getDefaultMessage()) ); return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(errors); } } /* Response JSON ប្រសិនបើ Validation fail: { "name": "ឈ្មោះមិនអាចទទេ", "email": "Email format មិនត្រឹមត្រូវ", "age": "អ្នកត្រូវមានអាយុ 18 ឡើង" } */
@Valid នៅចំពោះ @RequestBody ក្នុង Controller
ឬ validation នឹងមិន run ទោះបី Annotation មាននៅ DTO ក៏ដោយ!
ដាក់ DTO ក្នុង Package ច្បាស់លាស់
ចាត់តាំង folder: dto/request/, dto/response/ ដើម្បីងាយ maintain
ប្រើ Lombok ដើម្បីកាត់ Boilerplate
@Data, @Builder, @NoArgsConstructor, @AllArgsConstructor — Code ខ្លីច្រើន
Response DTO គួរ Immutable
ប្រើ Java Record ឬ @Value Lombok — ការពារ accidental mutation
Message Validation ជាភាសាខ្មែរ/អង់គ្លេស
ប្រើ messages.properties ដើម្បី centralize error messages ជំនួសសរសេរ hardcode
Generic Response Wrapper
Wrap Response ក្នុង ApiResponse<T> ជា standard format — { status, message, data }
// Generic Response Wrapper — ប្រើ return uniform format @Data @AllArgsConstructor @NoArgsConstructor public class ApiResponse<T> { private String status; private String message; private T data; public static <T> ApiResponse<T> success(T data) { return new ApiResponse<>("success", "ជោគជ័យ", data); } public static <T> ApiResponse<T> error(String message) { return new ApiResponse<>("error", message, null); } } // Usage in Controller: @GetMapping("/{id}") public ResponseEntity<ApiResponse<UserResponseDTO>> getUser(...) { UserResponseDTO user = userService.findById(id); return ResponseEntity.ok(ApiResponse.success(user)); } /* Response: { "status": "success", "message": "ជោគជ័យ", "data": { "id": 1, "name": "សុខ", "email": "sok@mail.com" } } */
@Valid ត្រូវដាក់ចំពោះ @RequestBody ក្នុង Controller method ដើម្បី trigger Bean Validation annotations ដូចជា @NotBlank, @Email, @Size ។@Valid (ពី jakarta.validation) ជា annotation ត្រឹមត្រូវ សម្រាប់ activate validation ។@Mapping(target = "id", ignore = true) ដើម្បីប្រាប់ mapper ឲ្យ skip field នោះពេល mapping ។@Mapping(target = "fieldName", ignore = true) ។ @JsonIgnore ប្រើសម្រាប់ Jackson serialization ដាច់ពី Mapping ។@NotBlank ដំណើរការ strict ជាងគេ — reject null, empty string (""), និង whitespace-only (" ") ។ @NotEmpty reject null & empty ប៉ុន្ដែ allow whitespace ។@NotBlank ជា annotation ដ៏ strict ជាងគេ reject null, "", " " ។ @NotNull ទទួល blank string ។ @NotEmpty ទទួល whitespace string ។