Spring Boot 3.x Java 17+ Intermediate REST API

DTO — Data Transfer Object
នៅក្នុង Spring Boot

មេរៀននេះពន្យល់លំអិតអំពី DTO, គោលការណ៍, Security benefit, របៀប Implement, Mapping Strategy, Validation, និង Best Practices ជាមួយ Code ឧទាហរណ៏ជាក់ស្តែង។

📖9 មេរៀន
💻15+ Code Examples
🧪Quiz Interactive
📱Responsive Design
01
DTO គឺជាអ្វី?
What is a Data Transfer Object?

DTO (Data Transfer Object) គឺជា Design Pattern មួយ ដែលប្រើសម្រាប់ ផ្ទេរទិន្នន័យ រវាងស្រទាប់ (Layers) ផ្សេងៗនៃ Application ។ DTO មិនមាន Business Logic ទេ — វាគ្រាន់តែជា Container ដែលផ្ទុកទិន្នន័យ ដើម្បី transport ពីកន្លែងមួយទៅកន្លែងមួយទៀត។

⬡ DTO Flow Architecture
ClientBrowser / Mobile
Request DTOValidation
ControllerREST Layer
ServiceBusiness Logic
EntityJPA / DB
Clientទទួលទិន្នន័យ
Response DTOFiltered Data
ControllerREST Layer
ServiceMap Entity→DTO
EntityDatabase
💡
គំនិតសំខាន់ DTO ដំណើរការដូចជា "ភ្នាក់ងារ" ដែលទទួល-ផ្ញើទិន្នន័យ ដោយមិនបង្ហាញ Database Structure ពិតប្រាកដ ឬ Sensitive Data ណាមួយ ដល់ Client ឡើយ។
02
ហេតុអ្វីត្រូវការ DTO?
Why use DTO instead of Entity directly?

ការប្រើ 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
🔐
Security — ការពារ sensitive fields
🔗
Decoupling — API ≠ Database
Validation — ពិនិត្យ input
🎯
Flexibility — format តាមតម្រូវការ
03
Entity vs DTO — ឧទាហរណ៏
Code comparison with User example

ស្រមៃថាអ្នកមាន User Entity ដែលផ្ទុកទិន្នន័យទាំងអស់ ប៉ុន្ដែចង់ return ត្រឹម id, name, email ប៉ុណ្ណោះ:

User.java
Entity
@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...
}
UserResponseDTO.java
DTO ✅
// ✅ 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; }
}
💡
Modern Java — ប្រើ Record Java 16+ អាចប្រើ record ជំនួស class ធម្មតា — កាន់តែខ្លី ស្អាត ជាង:
public record UserResponseDTO(Long id, String name, String email) {}
04
Request DTO & Response DTO
Two types of DTOs in REST API

ជាធម្មតា យើងបែងចែក DTO ជាពីរប្រភេទសំខាន់:

Request DTO — ទទួលទិន្នន័យពី Client Response DTO — ផ្ញើទិន្នន័យទៅ Client
CreateUserRequestDTO.java
Request 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)
}
UserController.java
Controller
@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);
    }
}
05
Service Layer — ការបំប្លែង DTO
Mapping between DTO and Entity in Service
UserService.java
Service
@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()
        );
    }
}
🎯
Pattern ល្អ ដាក់ mapping logic ក្នុង private toResponseDTO() method ដាច់ដោយឡែក ដើម្បីឲ្យ code ស្អាត និង Reusable ។ ឬ អាចបង្កើត UserMapper class ដាច់ស្រុត។
06
Mapping Strategy
Manual, MapStruct, ModelMapper

មាន 3 វិធីសំខាន់ក្នុងការ Map DTO ↔ Entity:

A

Manual Mapping — សរសេរដោយខ្លួនឯង

ច្បាស់លាស់ (explicit), ងាយ Debug, ល្អសម្រាប់ Project តូចឬ fields មួយចំនួន

B

MapStruct — Code Generation (★ ណែនាំ)

Generate Mapper code ដោយ Annotation processing — Performance ល្អ, Type-safe, ល្អសម្រាប់ Project ធំ

C

ModelMapper — Reflection-based

ងាយប្រើ, Auto-map fields ដែលឈ្មោះដូចគ្នា, Performance ទាបជាង MapStruct

UserMapper.java
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);
}
Performance ប្រៀបធៀប MapStruct > Manual > ModelMapper ។ MapStruct generate bytecode compile-time ដូច្នេះ runtime performance ស្មើ Manual mapping ។ ModelMapper ប្រើ Reflection ដូច្នេះ slow ជាង។
07
Validation នៅក្នុង DTO
Bean Validation with Jakarta Validation

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 ត្រូជា Email format valid @Email
@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
RegisterRequestDTO.java
Validation Example
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;
}
GlobalExceptionHandler.java
Error Handler
@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 ក៏ដោយ!
08
Best Practices
Tips for production-grade DTO design
1

ដាក់ DTO ក្នុង Package ច្បាស់លាស់

ចាត់តាំង folder: dto/request/, dto/response/ ដើម្បីងាយ maintain

2

ប្រើ Lombok ដើម្បីកាត់ Boilerplate

@Data, @Builder, @NoArgsConstructor, @AllArgsConstructor — Code ខ្លីច្រើន

3

Response DTO គួរ Immutable

ប្រើ Java Record ឬ @Value Lombok — ការពារ accidental mutation

4

Message Validation ជាភាសាខ្មែរ/អង់គ្លេស

ប្រើ messages.properties ដើម្បី centralize error messages ជំនួសសរសេរ hardcode

5

Generic Response Wrapper

Wrap Response ក្នុង ApiResponse<T> ជា standard format — { status, message, data }

ApiResponse.java
Generic Wrapper
// 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" }
}
*/
09
លំហាត់ — សាកល្បងចំណេះដឹង
Test your DTO knowledge
សំណួរ ១ / 5
ហេតុអ្វីយើងមិនគួរ Return Entity ដោយផ្ទាល់ពី Controller?
✅ ត្រឹមត្រូវ! Entity ផ្ទុក sensitive fields ដូចជា password, role, isDeleted ដែលគ្មានការ Filter ត្រូវ expose ទៅ Client ។ DTO ជួយ control ថាតើ data ណាខ្លះត្រូវ return ។
❌ ចម្លើយខុស — មូលហេតុចម្បងគឺ Security: Entity ផ្ទុក sensitive data ដូចជា password ដែលមិនគួរ expose ។
សំណួរ ២ / 5
Annotation មួយណាត្រូវដាក់ចំពោះ @RequestBody ដើម្បី Activate Validation?
✅ ត្រឹមត្រូវ! @Valid ត្រូវដាក់ចំពោះ @RequestBody ក្នុង Controller method ដើម្បី trigger Bean Validation annotations ដូចជា @NotBlank, @Email, @Size ។
❌ ចម្លើយខុស — @Valid (ពី jakarta.validation) ជា annotation ត្រឹមត្រូវ សម្រាប់ activate validation ។
សំណួរ ៣ / 5
Mapping Library មួយណាមាន Performance ល្អបំផុត?
✅ ត្រឹមត្រូវ! MapStruct Generate Java bytecode ពេល Compile time ដូច Manual mapping ដូច្នេះ Runtime performance ខ្ពស់ណាស់ ។ ModelMapper ប្រើ Reflection ដែល slow ជាង ។
❌ ចម្លើយខុស — MapStruct ល្អបំផុត ព្រោះ generate code compile-time មិន depend Reflection ។
សំណួរ ៤ / 5
IgnoreProperties annotation ណាត្រូវប្រើ ដើម្បីប្រាប់ MapStruct ឲ្យ skip field ណាមួយ?
✅ ត្រឹមត្រូវ! ក្នុង MapStruct, ប្រើ @Mapping(target = "id", ignore = true) ដើម្បីប្រាប់ mapper ឲ្យ skip field នោះពេល mapping ។
❌ ចម្លើយខុស — MapStruct ប្រើ @Mapping(target = "fieldName", ignore = true) ។ @JsonIgnore ប្រើសម្រាប់ Jackson serialization ដាច់ពី Mapping ។
សំណួរ ៥ / 5
Validation annotation មួយណានឹង Reject String ដែលទទេ (" ") — whitespace only?
✅ ត្រឹមត្រូវ! @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 ។