DTO là gì? Dùng DTO trong những trường hợp nào?

DTO là gì?

DTO hay tên gọi đầy đủ là Data Tranfer Object là một design pattern lần đầu tiên được giới thiệ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ý.

Giảm số  lần gọi các method giữa các tiến trình xử lý? các bạn có thấy khó hiểu không? mình đã rất hoang mang khi đọc khái niệm này lần đầu tiên, tuy nhiên hãy xem qua một tình huống sau.

Giả sử chúng ta đang phát triển một ứng dụng web frontend, cần làm việc với một hệ thống backend REST API. Mỗi lần gọi đến API để xử lý dữ liệu và nhận về kết quả sẽ tốn rất nhiều thời gian do vậy chúng ta cần hạn chế tối đa số lần gọi đến API và làm sao để mỗi lần gọi sẽ xử lý được nhiều vấn đề hơn. Để làm được điều này thì giờ đây các API cần nhận nhiều tham số đầu vào (parameters) hơn để xử lý nhiều công việc trong một lần. 

Nếu như trước đây chỉ xử lý một công việc A thôi thì API chỉ cần nhận 2 tham số X1,X2, nhưng giờ nó được yêu cầu phải xử lý thêm việc B nữa thì có cần đến 4 thâm số X1, X2, X3, X4. Việc xử 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ả nhiều loại dữ liệu hơn cho một request, điều này sẽ rất 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 giữa Domain model và DTO để tránh nhầm lẫn. Domain model là các Entity class dùng để ánh xạ một table trong database còn DTO là một object kết hợp nhiều tham số thành một đặt trong một DTO class.

Sử dụng DTO thế nào?

DTO là một cấu trúc dữ liệu phẳng và không chứa business logic trong đó chỉ dùng để lưu trữ dữ liệu, các method cho phép cập dữ liệu và sử dụng trong quá trình serialization or deserialization. Dữ liệu được ánh xạ từ domain model sang DTO và ngược lại thông qua một thành phần gọi là Mapper được đặt trong presentation hoặc facade layer.

Để làm rõ hơn cách sử dụng DTO thì chúng ta sẽ triển khai một API đơn giản sử dụng Spring Boot. Các bạn lưu ý ở phần này mình chỉ đưa ra một số đoạn code cần thiết để làm rõ việc sử dụng DTO. Còn để chạy được một ứng dụng hoàn chỉnh thì các bạn có thể kéo xuống cuối bài sẽ có link mã nguồn đầy đủ.

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.

Link mã nguồn: dto-pattern

Nguồn tham khảo

https://martinfowler.com/eaaCatalog/dataTransferObject.html

https://www.baeldung.com/java-dto-pattern

3.7 7 votes
Article Rating
Subscribe
Notify of
guest
4 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
4
0
Would love your thoughts, please comment.x
()
x