Entity不會直接拿來接收請求(Request)或是回傳請求(Response),因為如果直接回傳Entity會暴露資料表的欄位,同理,直接讓Entity接收請求的參數值,沒有經過任何檢查,就把資料存進資料庫,會有mass assignment的風險。因此不管是Request還是Response都要在Entity前面新增一個DTO()為資料把關。
商品分類由管理員維護,新增一筆之後前端要拿到剛建立的資料。Controller 呼叫完 Service 手上已有 Category,最短的寫法是直接回出去。
@PostMapping("/category/create_category")
public ResponseEntity<Category> createCategory(@Valid @RequestBody CategoryRequest request,@AuthenticationPrincipal String operator) {
Category created = categoryService.createCategory(request, operator);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
這段程式可以正常運作,Jackson 會把每個欄位序列化成 JSON。
Category 照著 categories 表長,九個欄位會全部進到回應裡。查詢分類清單時,任何登入帳號收到的是:
{
"categoryId": 3,
"name": "飲料",
"sortOrder": 1,
"version": 0,
"isDeleted": 0,
"createdBy": "admin",
"createdAt": "2025-05-02T10:14:33",
"updatedBy": null,
"updatedAt": "2025-05-02T10:14:33"
}
事實上,前端會用到的就只有前三個,這時候回傳 entity 等於告訴大家「API 回應的形狀就是資料表的形狀」。sort_order 哪天拆成兩欄,JSON 的 key 就跟著變,原本和前端約定好的API契約同時壞掉。schema 是內部實作,本該可以自由調整;API 契約是對外接口,改動要協調。綁在一起,資料庫重構就多出一輪跨團隊協調。因此,對外只留前端有理由知道的欄位,而不是把整個Entity傳出去。
做法是新增一個ResponseDTO:
public record CategoryResponse(Integer categoryId, String name, Integer sortOrder, Integer version) {
public static CategoryResponse from(Category c) {
return new CategoryResponse(c.getCategoryId(), c.getName(), c.getSortOrder(), c.getVersion());
}
}
回應要收斂,請求更要。若接收端直接收 entity:
public Category create(@RequestBody Category category) { ... }
body 裡有哪個 key,就往物件同名的欄位填。完全不區分哪些欄位該由 client端決定、完全沒檢查丟過來的值是否正確,多打幾個 key 就照單全收,資料從請求進到資料庫裏面時,完全沒有隔離,下面列出沒有隔離的後果:
{ "name": "飲料", "sortOrder": 1, "createdBy": "alice", "isDeleted": 1, "categoryId": 3 }
| 多塞的欄位 | 後果 |
|---|---|
createdBy |
稽核欄位變成 client 說了算,事後追查會指向別人 |
isDeleted |
建出一筆一出生就是已刪除狀態的資料 |
categoryId |
主鍵本來由資料庫發號,變成 client 指定 |
不如換一個只宣告 client 該填欄位的型別,新增一個RequestDTO:
@Data
public class CategoryRequest {
@NotBlank(message = "必須輸入 name")
private String name;
@NotNull(message = "必須輸入 sortOrder")
private Integer sortOrder;
}
createdBy 在這裡沒有地方落腳,它由伺服器從登入身分取得,client 無從偽造。
同一筆新增請求上有三種物件,各自活在不同深度:

Entity 確實會回到 Controller,Service 交回來的就是 Category。分界不在看不看得到,而是它進得來,出不去。
| 類型 | 職責 | 誰決定它的形狀 |
|---|---|---|
| Request DTO | client 能提供什麼、驗證規則 | API 契約 |
| Entity | 對應資料表結構 | 資料庫 schema |
| Response DTO | client 該看到什麼 | API 契約 |
代價是每個端點都要多寫一次轉換,欄位增減時兩邊都得改;換到的是改資料表不必通知前端。