Tiếp nối chủ đề các phương pháp tạo DTO trong Spring Boot, The Code Root tiếp tục gửi đến các bạn cách tạo DTO cho dữ liệu phức hợp, kết hợp từ nhiều Model lại với nhau.

Xin chào các bạn,

1. Giới thiệu

Trong bài hướng dẫn trước The Code Root đã giới thiệu đến các bạn các phương pháp cơ bản nhất trong kỹ thuật DTO (Data Transfer Object) trả dữ liệu về cho người dùng. Ngoài tác dụng bảo vệ được các dữ liệu thông tin nhạy cảm của người dùng hay giúp tăng Performance cho các Rest API, kỹ thuật DTO còn có thể giúp cho chúng ta trả về được dữ liệu được tổng hợp từ những Model khác nhau. Trong OOP được gọi là Compound Data.

Trong bài trước chúng ta tập trung vào việc chuyển đổi dữ liệu trên chỉ một Model thì bài viết này sẽ hướng dẫn các bạn cách tạo ra các trường mới được xử lý tính toán dựa trên Model chính và các Model liên quan để cho ra dữ liệu như người dùng yêu cầu. 

Ví dụ bài toán mua hàng: chức năng thanh toán: sau khi người dùng hoàn tất chọn hàng, bỏ hàng vào giỏ, và bấm nút "Mua Hàng" thì Hệ Thống sẽ làm nhiệm vụ tạo ra 1 Model "Hoá Đơn' bao gồm thông tin tên người mua, danh sách các món hàng - kèm giá (unit price) và tổng giá tiền của Hoá đơn đó. Vậy các bạn có thể thấy rằng với một bài toán khá đơn giản như vậy cũng đòi hỏi khá nhiều khâu xử lý dữ liệu.

Chính vì vậy yêu cầu trả về dữ liệu phức hợp là rất phổ biến cho mọi business nếu không muốn nói là tất cả.

2. Hướng dẫn code

Phương pháp 1: Sử dụng custom field của Model chính.

Dựa trên sự mở rộng của phương pháp  tạo DTO thủ công, chúng ta sẽ tạo ra những trường "ẩn" có giá trị được tính toán dựa trên các trường chính của Model thông qua các Getter được custom. Các bạn xem ví dụ sau:

import java.util.ArrayList;
import java.util.List;

public class Order {
    private Long id;

    private Long createTime;

    private User customer;

    private List<Product> productList = new ArrayList<>();

    public void setCreateTime(Long createTime) {
        this.createTime = createTime;
    }

    public Long getCreateTime() {
        return createTime;
    }

    public void setCustomer(User customer) {
        this.customer = customer;
    }

    public void setProductList(List<Product> productList) {
        this.productList = productList;
    }

    public List<String> getProductNameList(){
        List<String> productNameList = new ArrayList<>();
        for (Product product : productList){
            productNameList.add(product.getName());
        }

        return productNameList;
    }

    public String getCustomerName(){
        return customer.getFullName();
    }

    public Integer getTotal(){
        Integer total = 0;
        for (Product product : productList){
            total += product.getUnitPrice();
        }
        return total;
    }
}

 

Product

public class Product {

    private Long id;

    private String name;

    private Integer unitPrice;

    public Product(){}

    public Product(Long id, String name, Integer unitPrice) {
        this.id = id;
        this.name = name;
        this.unitPrice = unitPrice;
    }

    public String getName() {
        return name;
    }

    public Integer getUnitPrice() {
        return unitPrice;
    }

}

Và chúng ta cần một API để test thử:

ShopController:

@RestController
public class ShopController {

    @RequestMapping(value = "/order")
    public Order getOrder(@RequestParam(value = "productList") List<Long> productIdList,
                              @RequestParam(value = "userId") Long userId){
        Order order = new Order();
        order.setCreateTime(System.currentTimeMillis());
        User user = Data.userMap.get(userId);
        order.setCustomer(user);

        List<Product> productList = new ArrayList<>();
        Product product;
        for (Long productId : productIdList){
            product = Data.productUnitPrices.get(productId);
            productList.add(product);
        }
        order.setProductList(productList);

        return order;
    }

}

Để API trên có dữ liệu để cho ra output chúng ta cần setup một vài mẩu dữ liệu nhỏ như sau:

public class Data {
    public static Map<Long, Product> productUnitPrices = new HashMap<>();
    public static Map<Long, User> userMap = new HashMap<>();

    static {
        User customer1 = new User(1L, "Customer 1", "HCMC");
        userMap.put(1L, customer1);

        Product product1 = new Product(1L, "Product", 200 );
        Product product2 = new Product(2L, "Product", 300 );

        productUnitPrices.put(1L, product1);
        productUnitPrices.put(2L, product2);
    }
}

Chúng ta test api trên bằng url:

/order?userId=1&productList=1&productList=2

Kết quả sẽ được như sau:

{
    "createTime": 1559202953597,
    "total": 500,
    "productNameList": [
        "Product",
        "Product"
    ],
    "customerName": "Customer 1"
}

Các bạn lưu ý rằng trong class Order không hề có trường customerName và productNameList. Dữ liệu của 2 trường này trong Json trả về được Spring đọc từ Getter để tạo thành. Và các trường custom được định nghĩa trong chính Model đó, cách làm tuy dễ hiểu và thuận tiện nhưng về mặt coding lại làm cho cấu trúc code khá rối do dữ liệu chính (cần được quản lý lưu trữ) và dữ liệu custom đan xen nhau dễ làm cho code mất đi tính bảo trì. Do đó chúng ta sẽ qua cách thứ 2: sử dụng riêng 1 class DTO khác. 

 

Phương pháp 2: Sử dụng class DTO với custom field

Phương pháp 2 cũng tuơng tự với phương pháp ở trên là đều dùng Getter để định nghĩa dữ liệu trả về nhưng có điểm khác là trong Model gốc chúng ta sẽ không define các trường custom mà sẽ đem việc này qua (các) class DTO. Phương pháp này ngoài giúp chúng ta tách biệt code của dữ liệu gốc và dữ liệu custom ngoài ra còn có thể tạo ra nhiều phiên bản DTO khác nhau từ 1 Model gốc. 

Cũng sử dụng ví dụ như trên, và chúng ta sẽ define thêm một class OrderDTO:

mport java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class OrderDTO {
    private User customer;

    private List<Product> productList;

    private Date createTime;

    public void setCustomer(User user) {
        this.customer = user;
    }

    public void setProductList(List<Product> productList) {
        this.productList = productList;
    }

    public void setCreateTime(Date createTs) {
        this.createTime = createTs;
    }

    public List<String> getProductNameList(){
        List<String> productNameList = new ArrayList<>();
        for (Product product : productList){
            productNameList.add(product.getName());
        }

        return productNameList;
    }

    public String getCustomerName(){
        return this.customer.getFullName();
    }

    public Integer getTotal(){
        Integer total = 0;
        for (Product product : productList){
            total += product.getUnitPrice();
        }
        return total;
    }

    public String getCreateTime(){
        return new SimpleDateFormat().format(this.createTime);
    }
}

Và để tiện cho quá trình chuyển đổi hơn thì chúng ta sẽ sử dụng thư viện ModelMapper với khai báo thư viện Maven như sau:

        <dependency>
            <groupId>org.modelmapper.extensions</groupId>
            <artifactId>modelmapper-spring</artifactId>
            <version>2.3.4</version>
        </dependency>

Và lúc này class Order sẽ được xoá đi các trường custom chỉ còn lại các getter và setter cho các trường gốc của nó:

Order

import java.util.ArrayList;
import java.util.List;

public class Order {
    private Long createTime;

    private User customer;

    private List<Product> productList = new ArrayList<>();

    public Long getCreateTime() {
        return createTime;
    }

    public void setCustomer(User customer) {
        this.customer = customer;
    }

    public void setCreateTime(Long createTime) {
        this.createTime = createTime;
    }

    public User getCustomer() {
        return customer;
    }
    
    public List<Product> getProductList() {
        return productList;
    }

    public void setProductList(List<Product> productList) {
        this.productList = productList;
    }
}

Và cuối cùng là Controller

ShopController

@RequestMapping(value = "/order")
    public OrderDTO getOrder(@RequestParam(value = "productList") List<Long> productIdList,
                             @RequestParam(value = "userId") Long userId){
        Order order = new Order();
        order.setCreateTime(System.currentTimeMillis());
        User user = Data.userMap.get(userId);
        order.setCustomer(user);

        List<Product> productList = new ArrayList<>();
        Product product;
        for (Long productId : productIdList){
            product = Data.productUnitPrices.get(productId);
            productList.add(product);
        }
        order.setProductList(productList);
        ModelMapper mapper = new ModelMapper();
        return mapper.map(order, OrderDTO.class);
    }

Và kết quả JSON như sau:

{
    "createTime": "5/30/19 9:15 PM",
    "total": 500,
    "productNameList": [
        "Product 1",
        "Product 2"
    ],
    "customerName": "Customer 1"
}

Các bạn có thể download source code tại đây

Chúc các bạn thành công.

minhnhatict@gmail.com