DTO
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
Class Product trong hệ thống có cả giá nhập hàng, là thông tin nội bộ. Nếu
method trả thẳng Product ra ngoài thì khách hàng thấy luôn giá nhập. Nếu
method nhận thẳng Product từ body thì client tự đặt được id. DTO tách
phần dữ liệu đi qua API ra khỏi class dùng bên trong.
Khái niệm
📦 DTO (Data Transfer Object): class chỉ chứa dữ liệu đi vào hoặc đi ra qua API, tách riêng khỏi class dùng bên trong hệ thống.
📝 Record: kiểu class chỉ chứa dữ liệu không đổi, Java tự sinh constructor, method đọc từng trường, equals và toString.
record ProductResponse(int id, String name, long price) có sẵn constructor
ba tham số và ba method đọc id(), name(), price(). Record hợp với DTO
vì DTO chỉ chở dữ liệu. Thường mỗi tài nguyên có hai loại DTO:
| DTO | Hướng | Chứa gì |
|---|---|---|
ProductResponse |
server → client | những gì client được xem |
CreateProductRequest |
client → server | những gì client được phép gửi |
Ví dụ
// ProductsController.java
package com.shop.api;
import java.net.URI;
import java.util.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
class ProductsController {
private final List<Product> products =
new ArrayList<>();
@PostMapping
public ResponseEntity<ProductResponse> create(
@RequestBody
CreateProductRequest request) {
var product = new Product();
product.setId(products.size() + 1);
product.setName(request.name());
product.setPrice(request.price());
product.setCostPrice(
request.price() * 70 / 100);
products.add(product);
URI location = URI.create(
"/api/products/" + product.getId());
return ResponseEntity.created(location)
.body(toResponse(product));
}
private ProductResponse toResponse(Product p) {
return new ProductResponse(
p.getId(), p.getName(), p.getPrice());
}
}
// Product.java
package com.shop.api;
class Product {
private int id;
private String name = "";
private long price;
private long costPrice;
public int getId() { return id; }
public String getName() { return name; }
public long getPrice() { return price; }
public long getCostPrice() { return costPrice; }
public void setId(int id) { this.id = id; }
public void setName(String name) {
this.name = name;
}
public void setPrice(long price) {
this.price = price;
}
public void setCostPrice(long costPrice) {
this.costPrice = costPrice;
}
}
// CreateProductRequest.java
package com.shop.api;
record CreateProductRequest(String name, long price) {
}
// ProductResponse.java
package com.shop.api;
record ProductResponse(
int id, String name, long price) {
}Productlà class bên trong, cócostPricelà giá nhập.CreateProductRequestkhông cóidhaycostPrice, nên client không gửi được hai giá trị đó.ProductResponsekhông cócostPrice, nên giá nhập không bao giờ lộ ra.toResponsechép dữ liệu từProductsang DTO. Viết tay như vậy là đủ cho hầu hết dự án.URI.create(...)dựng thẳng URL cho headerLocation, dùng khi chưa có method đọc riêng để trỏ tới.
Thử ngay
Giữ các file cũ, thay ProductsController.java, Product.java và thêm
CreateProductRequest.java, ProductResponse.java theo phần Ví dụ, rồi
chạy lại server. Tạo file product.json:
{ "name": "Pen", "price": 5000, "costPrice": 1, "id": 999 }Rồi gửi:
curl -i -X POST http://localhost:5000/api/products -H "Content-Type: application/json" -d @product.jsonĐoán trước khi chạy: client cố gửi id là 999 và costPrice là 1.
Response trả về id bằng bao nhiêu, và có costPrice không?
Xem kết quả
HTTP/1.1 201
Location: /api/products/1
{"id":1,"name":"Pen","price":5000}id là 1, do server tự cấp. CreateProductRequest không có id và
costPrice, nên hai giá trị client gửi bị bỏ qua. Response cũng không có
costPrice.
Lỗi hay gặp
Trả thẳng class bên trong ra ngoài. Mọi trường có getter đều lộ ra, kể cả giá nhập.
// SAI — response có luôn costPrice
// ItemsController.java
package com.shop.api;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/items")
class ItemsController {
@GetMapping("/{id}")
public Product getById(@PathVariable int id) {
var product = new Product();
product.setId(id);
product.setCostPrice(3500);
return product;
}
}// ĐÚNG — trả qua DTO, không có costPrice
// ItemsController.java
package com.shop.api;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/items")
class ItemsController {
@GetMapping("/{id}")
public ProductResponse getById(
@PathVariable int id) {
return new ProductResponse(id, "Pen", 5000);
}
}Nhận thẳng class bên trong từ body. Client gửi thêm id hay costPrice
là gán được luôn qua setter. Luôn nhận qua DTO chỉ chứa những gì client được
phép gửi, như CreateProductRequest ở trên.
Tóm tắt
- DTO là class chỉ chứa dữ liệu đi qua API, tách khỏi class bên trong.
- DTO response quyết định client được xem gì.
- DTO request quyết định client được gửi gì, chặn việc tự đặt
idhay giá nhập. - Record khai báo DTO trên một dòng. Chép dữ liệu sang DTO bằng một method nhỏ.
Tự kiểm tra
0/3 câuClass User có passwordHash. Method xử lý GET /api/users/1 nên trả về kiểu gì?
Record CreateOrderRequest không có trường status. Client gửi JSON kèm "status": "PAID". Chuyện gì xảy ra?
Vì sao DTO request để tạo sản phẩm không nên có id?