併發更新策略(樂觀鎖)
框架預設不在 audit base class 放 @Version,因此一般 Entity 採
last-write-wins。當 lost update 會造成財務、庫存、額度、狀態機或長時間
編輯資料的實質損害時,才對該 Entity opt in optimistic locking。
完整契約
Entity
@Version
@Column(nullable = false)
private Long version;
- 使用
Long;version 是 provider 管理的數值,不是時間點。 - 不初始化,不提供一般 setter。
- 應用程式、mapper 與 property-copy 不得設定或遞增 version。
API
- Read response 回傳目前
version。 - 受保護的 update request 必須回送 client 當初讀到的 expected version。
- 缺少 expected version 是無效請求,不得悄悄降級為 last-write-wins。
- 衝突回 409
urn:appfuse:error:optimistic-lock;client 重新載入並讓使用者 決定如何合併,不 blind retry。
Application service
先比較 client expected version,再修改 managed Entity;絕對不要把 client version 寫回 Entity。
@Transactional
public Order update(String id, UpdateOrderRequest request) {
Order order = orderRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Order ${0} not found", id));
if (!Objects.equals(order.getVersion(), request.version())) {
throw new ConflictException("Order was modified; reload and try again");
}
order.changeAddress(request.address());
return order;
}
這裡有兩層保護:
- application comparison 擋住「client 讀取後、request 送出前」已發生的更新。
- Hibernate flush 的
UPDATE ... WHERE id = ? AND version = ?擋住 「load / compare 後、transaction commit 前」發生的競態。
第二層才是 authoritative database check。若只比較、不加 @Version,
仍有 check-then-act race;若只加 @Version、不讓 expected version 往返,
則只能保護同一 transaction 內的競態,無法辨識 stale HTTP client。
PATCH 與 mapper
version是 request concurrency token,不是可 patch 的 Entity property。PropertyMap、Bean mapper 與 generic copy 必須排除version。- Request DTO 可命名
version或expectedVersion;進入 service 後只比較。 - Flush 可在 transaction commit 發生;需要在 service 邊界穩定映射例外時,
明確
flush()。
限制
- JPQL / Criteria bulk update 與 native SQL 會繞過一般 Entity version 行為; 必須明確更新 version 並檢查 affected row count,或不要對受保護資料使用 bulk update。
- Cascade 更新的每個 mutable Entity 都有自己的 concurrency 邊界;不要假設 aggregate root 的 version 自動涵蓋所有獨立 child row。
- 多租戶隔離與 optimistic locking 正交;tenant predicate 與 version predicate 都必須存在。
- HTTP
ETag/If-Match可承載同一 expected-version 契約,但不可同時 維護兩套彼此不同步的 token。
速查
| 問題 | 規範 |
|---|---|
| 全域預設 | last-write-wins |
何時加 @Version | silent overwrite 會造成損害 |
| Client version | read 回傳、update 必填 |
| Service 如何使用 | 與 managed Entity version 比較 |
可以 setVersion(clientVersion) 嗎 | 不可以 |
| 最終競態由誰判定 | Hibernate flush / affected row count |
| 衝突處理 | 409、reload、人工合併;不 blind retry |
欄位定義另見 JPA Entity 欄位型別規範。