DTO là gì? Cách sử dụng DTO hiện nay
DTO hoặc Data Transfer Object, là một design pattern được giới thiệu lần đầu bởi Martin Fowler trong cuốn sách EAA. Mục đích sử dụng chính của DTO là giảm số lần gọi các method giữa các tiến trình xử lý, tuy nhiên, việc sử dụng DTO còn có nhiều ứng dụng khác nhau.
DTO là gì?
Một trong những ứng dụng phổ biến của DTO là trong trường hợp cần truyền dữ liệu giữa các lớp khác nhau của một ứng dụng. Khi sử dụng DTO, ta có thể truyền dữ liệu giữa các lớp một cách dễ dàng và tiện lợi hơn, đồng thời giảm thiểu sự phụ thuộc giữa các lớp.
Thêm vào đó, DTO cũng được sử dụng để tối ưu hóa việc truyền dữ liệu qua mạng, giảm thiểu tải cho hệ thống và cải thiện hiệu suất xử lý. Với những ứng dụng đa dạng và tiềm năng lớn, DTO là một design pattern mà các nhà phát triển nên tìm hiểu và sử dụng để cải thiện hiệu suất và hiệu quả cho ứng dụng của mình.
Giảm số lần gọi các method giữa các tiến trình xử lý? Có vẻ khó hiểu nhỉ? Tình huống này sẽ giúp bạn hiểu hơn.
Giả sử bạn đang phát triển một ứng dụng web frontend, cần liên kết với một hệ thống backend REST API. Mỗi lần gọi API để xử lý dữ liệu và nhận kết quả sẽ tốn rất nhiều thời gian. Vì vậy, bạn cần hạn chế số lần gọi API và làm sao để mỗi lần gọi sẽ xử lý được nhiều công việc hơn. Để làm được điều này, các API cần nhận nhiều tham số đầu vào hơn để xử lý nhiều công việc trong một lần.
Trước đây, nếu chỉ xử lý công việc A thì API chỉ cần nhận 2 tham số X1, X2. Nhưng nếu bây giờ nó còn phải xử lý công việc B nữa thì cần đến 4 tham số X1, X2, X3, X4. Việc sử dụng quá nhiều tham số đầu vào sẽ gây khó khăn cho việc lập trình ở cả 2 phía frontend và backend. Hơn nữa, API giờ đây cũng phải trả về nhiều loại dữ liệu hơn cho một request, điều này gây khó khăn cho Java khi nó chỉ cho phép trả về một Object duy nhất có kiểu cụ thể.
Giải pháp ở đây chính là khởi tạo một DTO object chứa tất cả các dữ liệu trong một lần gọi đến API. Nó cần phải được serializable trước khi được truyền qua connection và deserializable để nhận lại DTO ban đầu được gửi từ phía bên kia.
Domain model và DTO
Chúng ta cần phân biệt rõ ràng giữa Domain model và DTO để tránh nhầm lẫn. Trong lập trình hướng đối tượng, Domain model thường được sử dụng để ánh xạ một table trong database. Domain model bao gồm các Entity class, mỗi class đại diện cho một table trong database.
Nói cách khác, Domain model là một phần của lớp Model trong mô hình MVC. Trong khi đó, DTO là một object kết hợp nhiều tham số thành một đối tượng trong một DTO class. DTO thường được sử dụng để truyền dữ liệu giữa các lớp ở các tầng khác nhau của ứng dụng. Vì vậy, việc phân biệt rõ ràng giữa Domain model và DTO là rất quan trọng để tránh nhầm lẫn và đảm bảo tính đúng đắn của ứng dụng.
Sử dụng DTO thế nào?
DTO là một cấu trúc dữ liệu đơn giản để lưu trữ dữ liệu và sử dụng trong quá trình serialization hoặc deserialization. Nó được sử dụng để ánh xạ dữ liệu từ domain model sang DTO và ngược lại thông qua một thành phần gọi là Mapper trong presentation hoặc facade layer.
Để hiểu rõ hơn cách sử dụng DTO, chúng ta sẽ triển khai một API đơn giản sử dụng Spring Boot. Ở đây, chúng ta chỉ cung cấp các đoạn code cần thiết để làm rõ việc sử dụng DTO. Để chạy được ứng dụng hoàn chỉnh, bạn có thể xem link mã nguồn đầy đủ ở cuối bài viết.
Domain model
Trước tiên, chúng ta sẽ có 2 domain class là User và Role được định nghĩa như sau
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String password;
@ManyToMany
private List<Role> roles;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany(mappedBy = “roles”)
public List<User> users;
}
Note: User và Role có mối quan hệ Many-To-Many.
DTO class
Tiếp theo chúng ta sẽ có 3 DTO class gồm UserDTO, RoleDTO và UserCreationDTO. Trong đó UserDTO và RoleDTO dùng để ánh xạ User và Role khi trả dữ liệu về cho client còn UserCreationDTO dùng để thêm một User mới vì khởi tạo User chúng ta không chỉ cần các thông cơ bản của nó như name, password mà còn cần một danh sách Role được ấn định cho User.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO implements Serializable {
private String name;
private List<RoleDTO> roles;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class RoleDTO implements Serializable {
private Long id;
private String name;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserCreationDTO implements Serializable {
private String name;
private String password;
private List<Long> roleIds;
}
Mapper
Như đã đề cập ở trên thì việc chuyển đổi qua lại giữa DTO và Domain model cần có một lớp trung gian ở đây mình dùng Mapper.
Đầu tiên là UserMapper cho phép chuyển đổi UserCreationDTO sang User và từ User sang UserDTO.
public class UserMapper {
private static UserMapper INSTANCE;
public static UserMapper getInstance() {
if (INSTANCE == null) {
INSTANCE = new UserMapper();
}
return INSTANCE;
}
public User toEntity(UserCreationDTO dto) {
User user = new User();
user.setName(dto.getName());
user.setPassword(dto.getPassword());
return user;
}
public UserDTO toDTO(User user) {
UserDTO dto = new UserDTO();
dto.setName(user.getName());
dto.setRoles(user.getRoles().stream()
.map(role -> RoleMapper.getInstance().toDTO(role))
.collect(Collectors.toList()));
return dto;
}
}
Tiếp theo là RoleMapper chứa 2 method cơ bản nhất dùng để chuyển đổi qua lại giữa domain và dto.
public class RoleMapper {
private static RoleMapper INSTANCE;
public static RoleMapper getInstance() {
if (INSTANCE == null) {
INSTANCE = new RoleMapper();
}
return INSTANCE;
}
public Role toEntity(RoleDTO roleDTO) {
Role role = new Role();
role.setName(roleDTO.getName());
return role;
}
public RoleDTO toDTO(Role role) {
RoleDTO dto = new RoleDTO();
dto.setName(role.getName());
dto.setId(role.getId());
return dto;
}
}
Service
Ở phần này chúng ta sẽ quan tâm đến UserService dùng để khởi tạo User mới. Tại đây chúng ta sẽ cần dùng đến Mapper để chuyển đổi từ DTO sang User. Tiến hành lưu xuống database và lại dùng Mapper để chuyển đổi User object đã được lưu xuống database sang DTO và trả về cho client.
@Service
@Transactional(rollbackFor = Throwable.class)
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Override
public UserDTO create(UserCreationDTO dto) {
User user = UserMapper.getInstance().toEntity(dto);
List<Role> roles = roleRepository.findAllById(dto.getRoleIds());
user.setRoles(roles);
return UserMapper.getInstance().toDTO(userRepository.save(user));
}
@Override
public List<UserDTO> findAll() {
return userRepository.findAll().stream()
.map(user -> UserMapper.getInstance().toDTO(user))
.collect(Collectors.toList());
}
}
Như vậy các bạn có thể thấy vai trò của DTO trong trường hợp này, UserCreationDTO đã được dùng để đóng gói tất cả các thông tin cần thiết như thông tin cơ bản của một User và những Role được gán cho nó.
Controller
Tầng này sẽ nhận request từ client và chuyển xuống cho tầng Service xử lý nên các bạn có thể tham khảo qua
@RestController
@RequestMapping(“/user”)
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public UserDTO create(@RequestBody UserCreationDTO dto) {
return userService.create(dto);
}
@GetMapping
public List<UserDTO> findAll() {
return userService.findAll();
}
}
@RestController
@RequestMapping(“/role”)
public class RoleController {
@Autowired
private RoleService roleService;
@PostMapping
public RoleDTO create(@RequestBody RoleDTO dto) {
return roleService.create(dto);
}
}
Để chạy thử thì các bạn cần làm theo các bước sau:
- Khởi tạo một số Role, lưu lại các Role ID trong response trả về (RoleIDs)
- Khởi tạo User với các thông tin cần thiết và RoleIDs đã được lưu ở bước 1
Khởi tạo User request mẫu như sau:
curl –location –request GET ‘http://localhost:8080/user’ \
–header ‘Content-Type: application/json’ \
–data-raw ‘{
“name”: “deft”,
“password”: “123456”,
“roleIds”: [
1
]
}’
Kết quả trả về sẽ như sau:
[{
“name”: “deft”,
“roles”: [
{
“id”: 1,
“name”: “admin”
}
]
}
]
Tóm lược
Hy vọng qua đây các bạn sẽ hiểu rõ hơn về DTO pattern, mình tin rằng đa số chúng ta đều sử dụng nó hằng ngày khi học và phát triển các ứng dụng API.