Bài viết này sẽ đi sâu vào cách dữ liệu biến đổi khi đi qua các layer khác nhau trong một ứng dụng Spring Boot. Chúng ta sẽ cùng tìm hiểu về các khái niệm quan trọng như Entity, Domain Model và DTO, đồng thời khám phá lý do cần thiết của việc phân chia dữ liệu thành nhiều dạng.
Mục Lục
1. Tổng Quan Về Kiến Trúc Spring Boot
1.1. Kiến trúc Source Code và Kiến trúc Dữ liệu
Trong các ứng dụng Spring Boot, chúng ta thường thấy hai mô hình kiến trúc song hành:
- Mô hình MVC (Model-View-Controller): Tập trung vào việc tách biệt logic xử lý, giao diện người dùng và dữ liệu.
- Mô hình 3 lớp (3-tier): Chia ứng dụng thành 3 tầng riêng biệt: Presentation (Web), Application (Service), và Data (Repository).
Khi kết hợp hai mô hình này, ta có một cấu trúc ứng dụng Spring Boot hoàn chỉnh, nơi mỗi tầng (layer) đảm nhận một vai trò cụ thể.
Alt: Sơ đồ kiến trúc Spring Boot 3 lớp minh họa sự tương tác giữa Controller, Service và Repository.
Sơ đồ trên thể hiện cách tổ chức source code, chia thành các Controller, Service và Repository tương ứng với từng layer. Tuy nhiên, nếu xét về mặt tổ chức dữ liệu, sơ đồ sẽ có sự thay đổi. Lúc này, mỗi layer sẽ làm việc với một dạng dữ liệu cụ thể:
- Presentation Layer (Web): Tương tác với DTO (Data Transfer Object).
- Application Layer (Service): Xử lý Domain Model.
- Data Access Layer (Repository): Làm việc với Entity.
Điều này có nghĩa là dữ liệu sẽ có các dạng khác nhau tùy thuộc vào layer mà nó đang được xử lý. Mỗi dạng dữ liệu sẽ có một nhiệm vụ và mục đích riêng, và được thể hiện bằng các class tương ứng trong code. Ví dụ: Controller chỉ nên nhận và trả về DTO, không nên trực tiếp thao tác với Domain Model hoặc Entity.
1.2. Tại Sao Cần Chia Nhiều Dạng Dữ liệu?
Việc chia nhỏ dữ liệu thành nhiều dạng tuân theo nguyên tắc SoC (Separation of Concerns) – chia tách các mối quan tâm trong thiết kế phần mềm. Ứng dụng Spring Boot được chia nhỏ thành các layer, mỗi layer có một trách nhiệm riêng. Nguyên lý đầu tiên của SOLID (Single Responsibility Principle) cũng nhấn mạnh rằng mỗi class chỉ nên thực hiện một nhiệm vụ duy nhất.
Trước đây, khi dữ liệu chỉ có một dạng duy nhất, mỗi layer lại có cách xử lý khác nhau, dẫn đến việc một dữ liệu phải thực hiện nhiều nhiệm vụ khác nhau, vi phạm nguyên tắc Single Responsibility. Do đó, việc chia nhỏ thành nhiều dạng dữ liệu giúp mỗi dạng chỉ tập trung vào một nhiệm vụ cụ thể.
Một lý do quan trọng khác là bảo mật. Nếu chỉ có một dạng dữ liệu duy nhất, các thông tin nhạy cảm có thể bị lộ ra ngoài không cần thiết. Ví dụ, khi tìm kiếm bạn bè trên Facebook, ứng dụng chỉ nên trả về thông tin cơ bản như tên và ảnh đại diện. Nếu chỉ có một dạng dữ liệu, toàn bộ thông tin cá nhân có thể bị trả về, tạo cơ hội cho kẻ xấu lợi dụng. Phân tách dữ liệu thành các dạng riêng biệt là một cách hiệu quả để tăng cường bảo mật cho ứng dụng.
2. Các Dạng Dữ Liệu Trong Spring Boot
2.1. Phân Loại Dữ Liệu
Dữ liệu trong ứng dụng Spring Boot có thể được chia thành hai loại chính:
- Public: Dữ liệu được trao đổi, chia sẻ với bên ngoài qua REST API hoặc giữa các service trong microservice. Dữ liệu này thường ở dạng DTO.
- Private: Dữ liệu được sử dụng trong nội bộ ứng dụng, không nên chia sẻ ra bên ngoài. Dữ liệu này thường nằm trong các Domain Model hoặc Entity.
Từ hai loại này, ta có ba dạng dữ liệu phổ biến:
-
DTO (Data Transfer Object): Class đóng gói dữ liệu để chuyển giữa client và server, hoặc giữa các service trong microservice. Mục đích chính của DTO là giảm lượng thông tin không cần thiết phải truyền tải và tăng cường bảo mật.
Alt: Minh họa luồng dữ liệu sử dụng DTO giữa Client và Server.
-
Domain Model: Class đại diện cho các đối tượng trong nghiệp vụ (business domain) như Customer, Product, Order,… Domain Model thường chứa logic nghiệp vụ liên quan đến đối tượng đó.
-
Entity: Một dạng Domain Model, nhưng tương ứng với một bảng (table) trong cơ sở dữ liệu (database). Entity có thể được ánh xạ (map) trực tiếp vào cơ sở dữ liệu. Chỉ có Entity mới có thể đại diện cho dữ liệu trong cơ sở dữ liệu.
Các dạng dữ liệu thường có hậu tố tương ứng để dễ phân biệt. Ví dụ: Entity User không có hậu tố, Domain Model có thể là UserModel, và DTO có thể là UserDto.
2.2. Nguyên Tắc Chọn Dữ Liệu Cho Từng Layer
Mỗi layer trong mô hình 3 lớp sẽ xử lý, nhận và trả về dữ liệu thuộc các loại xác định. Điều này giúp đảm bảo tính nhất quán và dễ bảo trì của ứng dụng.
Áp dụng vào mô hình 3 lớp, ta có nguyên tắc thiết kế chung:
- Web Layer (Controller): Chỉ nên xử lý DTO. Controller nên nhận dữ liệu từ client dưới dạng DTO và trả về dữ liệu cho client cũng dưới dạng DTO.
- Service Layer: Nhận DTO (từ Controller) hoặc Domain Model (từ các Service khác). Xử lý dữ liệu (có thể tương tác với database) và trả về Web Layer dưới dạng DTO.
- Repository Layer: Chỉ thao tác trên Entity, vì đây là đối tượng thích hợp để ánh xạ vào database.
Đối với các thành phần khác của Spring Boot:
- Custom Repository: Nếu thao tác trực tiếp với database mà không thông qua Repository, lớp này sẽ hoạt động tương tự như Service Layer.
2.3. Model Mapping
Khi dữ liệu di chuyển giữa các layer, nó sẽ được chuyển đổi (map) từ dạng này sang dạng khác. Ví dụ, DTO từ Controller đi vào Service sẽ được map thành Domain Model hoặc Entity. Khi vào Repository, nó bắt buộc phải trở thành Entity. Quá trình này gọi là Model Mapping.
Việc convert giữa các dạng dữ liệu (DTO thành Entity, DTO thành Domain Model,…) thường được thực hiện bằng các thư viện như ModelMapper. Tuy nhiên, có thể viết code copy thuần để thực hiện việc này.
@Getter
public class UserDto {
String name;
String age;
public void loadFromEntity(User entity) {
this.name = entity.getName();
this.age = entity.getAge();
}
}
@Getter
public class User {
String name;
String age;
String crush;
public void loadFromDto(UserDto dto) {
this.name = dto.getName();
this.age = dto.getAge();
}
}
Cách sử dụng:
// Trong controller, convert từ DTO > entity
User user = new User();
user.loadFromDto(userDto);
// Hoặc trả về cũng tương tự, từ Entity > DTO
User user = userService.getUser(username);
UserDto userDto = new UserDto();
userDto.loadFromEntity(user);
return userDto;
Một cách đơn giản hơn là copy dữ liệu trong constructor:
User user = new User(userDto); // DTO > entity
UserDto userDto = new UserDto(user); // Entity > DTO
3. Ứng Dụng Thực Tế
Trong thực tế, việc áp dụng các nguyên tắc trên có thể linh hoạt hơn. Không phải lúc nào cũng tuân theo một khuôn mẫu cứng nhắc như:
Controller nhận DTO > Service chuyển DTO thành Model/Entity > Repository nhận Entity vào DB
Repository lấy Entity ra > Service xử lý thành DTO > Controller trả về DTO
Có nhiều trường hợp khác có thể xảy ra:
- Controller có thể nhận tham số primitive (int, float,…) thay vì DTO.
- Có thể nhận hoặc trả về một List DTO.
Do đó, cần điều chỉnh cho phù hợp với yêu cầu cụ thể của dự án.
Ví dụ, thông thường Service sẽ thực hiện mapping sang DTO và ngược lại, Controller chỉ nhận DTO. Nhưng đôi khi, để giảm tải cho Service, việc mapping này có thể do Controller đảm nhiệm. Tuy nhiên, cần lưu ý rằng Controller nên giữ cho “mỏng” (ít code nhất có thể).
Dù áp dụng cách nào, nguyên tắc chung là việc mapping nên được thực hiện ở “rìa” (edge) của code. Nếu mapping trong Service, việc chuyển đổi nên nằm ở đầu hoặc cuối method.
Ngoài ra, để giảm boilerplate code, có thể giảm bớt sự chặt chẽ nếu không cần thiết. Ví dụ:
- Đôi khi không cần Domain Model, Service có thể chuyển thẳng DTO thành Entity.
- Service có thể trả về Entity hoặc Model nếu chúng đơn giản và không chứa thông tin nhạy cảm. Lúc này, có thể bỏ qua DTO để tránh sự phức tạp không cần thiết.
Việc sử dụng DTO đôi khi gây tranh cãi, và có người coi đó là một anti-pattern. Tuy nhiên, DTO vẫn có nhiều ưu điểm và có thể tùy biến để phù hợp và hiệu quả hơn.
Tóm lại, việc hiểu rõ các khái niệm Entity, Domain Model và DTO, cũng như cách chúng biến đổi qua các layer khác nhau trong ứng dụng Spring Boot, là rất quan trọng để xây dựng các ứng dụng mạnh mẽ, bảo mật và dễ bảo trì.
