Phiên bản & khôi phục dữ liệu
Dựng danh sách phiên bản, xem snapshot và khôi phục dữ liệu dựa trên nhật ký audit. Dùng khi nghiệp vụ cần xem lại lịch sử phiên bản hoặc rollback dữ liệu theo yêu cầu.
Module hoạt động trên dữ liệu của govex-cloud-data-audit: mọi phiên bản được dựng lại từ bảng audit_log, không ghi nhật ký riêng và không lưu snapshot dựng sẵn. Vì vậy cần bật audit trước khi dùng.
Khi nào sử dụng
- Cần hiển thị danh sách phiên bản của một đối tượng nghiệp vụ theo thời gian.
- Cần xem snapshot các trường của đối tượng tại một phiên bản cụ thể.
- Cần khôi phục đối tượng về phiên bản trước đó kèm lý do khôi phục.
- Cần tận dụng nhật ký audit sẵn có thay vì tự dựng bảng versioning riêng.
Cài đặt
<dependency>
<groupId>vn.govex.cloud</groupId>
<artifactId>govex-cloud-data-revision</artifactId>
</dependency>
Version quản lý qua BOM — xem Cài đặt.
Cấu hình
Module không có property riêng. Bean DataRevisionService được tạo tự động khi ứng dụng có bean AuditLogService — nghĩa là khi govex.audit.enabled=true ở govex-cloud-data-audit và bảng audit_log đã tồn tại.
DataRevisionService cung cấp các method:
| Method | Ý nghĩa |
|---|---|
getVersions(root, rootId) | Danh sách phiên bản của đối tượng gốc, tối đa 1000 bản ghi. |
getVersions(root, rootId, maxRevisions) | Giới hạn số phiên bản trả về; maxRevisions phải lớn hơn 0. |
getSnapshot(root, rootId, revisionNo) | Snapshot tại một phiên bản, tối đa 1000 bản ghi xử lý. |
getSnapshot(root, rootId, revisionNo, maxRevisions) | Snapshot với giới hạn bản ghi xử lý; revisionNo không được vượt quá maxRevisions. |
restore(root, rootId, revisionNo, reason) | Khôi phục entity gốc về phiên bản, kèm lý do ghi vào nhật ký audit. |
Điều kiện để phiên bản có dữ liệu
getVersions/getSnapshot đọc thẳng auditLogService.findByTarget(root.getSimpleName(), rootId), vì vậy audit phải ghi được bản ghi cho đối tượng gốc:
- Method nghiệp vụ gắn
@AuditvớitargetEntity = Document.classvà xác định đượctargetId. - Entity gốc gắn
@AuditedEntity(kèm cột ID/target) để thay đổi được capture vàochanges.
Chi tiết cấu hình audit xem Lịch sử thao tác dữ liệu.
@Audit(code = "CAP_NHAT_HO_SO", name = "Cập nhật hồ sơ",
targetEntity = Document.class, targetId = "#id")
public void update(String id, DocumentUpdateRequest request) {
// thay đổi dữ liệu trên Document / DOCUMENT_DETAIL
}
Sử dụng
Bước 1 — Liệt kê phiên bản
// Toàn bộ phiên bản (tối đa 1000 bản ghi mới nhất)
List<RevisionInfo> versions = dataRevisionService.getVersions(Document.class, documentId);
// Giới hạn số phiên bản trả về
List<RevisionInfo> latest = dataRevisionService.getVersions(Document.class, documentId, 50);
Danh sách trả về theo thứ tự từ cũ đến mới; revisionNo đánh số từ 1 theo đúng lịch sử chứ không phải theo vị trí trong cửa sổ giới hạn. maxRevisions phải lớn hơn 0; không truyền thì dùng mặc định 1000.
RevisionInfo gồm: id, revisionNo, actionCode, actionName, actorId, actorName, actionTime.
Bước 2 — Dựng snapshot tại một phiên bản
Map<String, Object> snapshot = dataRevisionService.getSnapshot(Document.class, documentId, revisionNo);
// giới hạn số bản ghi audit xử lý
Map<String, Object> limited = dataRevisionService.getSnapshot(Document.class, documentId, revisionNo, 200);
Quy tắc:
revisionNotính từ 1, phải lớn hơn 0 và không vượt quámaxRevisions.- Snapshot là
Map<String, Object>khóa theo tên field Java của entity gốc, giá trị là giá trị tại phiên bản đó. - Cách dựng: đọc lịch sử audit của target, đảo về thứ tự cũ → mới, áp dần
changescủa các bản ghi thuộc chính entity gốc cho tớirevisionNo. Thay đổi của bảng chi tiết (entityNamekhác entity gốc) không được đưa vào snapshot. - Bản ghi audit không làm thay đổi giá trị trường nào sẽ không ảnh hưởng tới snapshot.
Ví dụ snapshot sau hai phiên bản:
{
"documentCode": "HS-2026-001",
"status": "ACTIVE",
"receivedDate": "2026-01-15"
}
Bước 3 — Khôi phục về một phiên bản
dataRevisionService.restore(Document.class, documentId, revisionNo, "Khôi phục theo yêu cầu nghiệp vụ");
Cách hoạt động:
- Chạy trong transaction qua
TransactionTemplate. - Tìm entity gốc bằng
EntityManager.find(...); không tìm thấy dữ liệu →IllegalStateException. - Dựng snapshot của phiên bản đích; snapshot rỗng →
IllegalStateException. - Ghi từng giá trị snapshot vào field tương ứng của entity (đã chuyển về đúng kiểu field); field không có trong snapshot được giữ nguyên.
- Sau khi transaction commit, ghi thêm một bản ghi audit mã
KHOI_PHUC_PHIEN_BAN, tên "Khôi phục phiên bản N",contextlà lý do khôi phục kèm danh sách thay đổi cũ/mới của lần khôi phục.
Lần khôi phục sinh thêm một bản ghi audit nên getVersions sau đó sẽ thấy nó là phiên bản mới nhất.
Bước 4 — Gắn transaction nghiệp vụ với revision (tuỳ chọn)
@RevisionTransactional khai báo entity gốc cho một method nghiệp vụ:
@RevisionTransactional(rootAggregateEntity = Document.class)
public void approveDocument(String documentId) {
// xử lý nghiệp vụ
}
Hình dạng dữ liệu phiên bản
| Thành phần | Ý nghĩa |
|---|---|
RevisionInfo | Tóm tắt một phiên bản: id, revisionNo, actionCode, actionName, actorId, actorName, actionTime. |
getVersions(...) | Danh sách RevisionInfo từ cũ đến mới, tối đa maxRevisions phần tử. |
getSnapshot(...) | Map<String, Object> tên field → giá trị tại phiên bản. |
| Bản ghi khôi phục | AuditEntry mã KHOI_PHUC_PHIEN_BAN (hằng số RevisionConstants.ActionCodes.RESTORE_REVISION). |
Annotation metadata phiên bản
Module có lớp annotation metadata dùng để mô tả entity/trường tham gia phiên bản. Hiện trạng (cần kiểm tra lại khi nâng version):
@DomainRevisiontrên entity:aggregateColumn(cột liên kết với đối tượng gốc, bắt buộc),rootAggregateEntity(entity gốc, bắt buộc),ignoreTrackingColumns(cột bỏ qua).@Revisiontrên field:description,hidden,ignore,deletedColumn,values(@Revision.Value(value, description)).@NotRevision: tương đương@Revision(ignore = true).@RevisionTransactional: khai báo entity gốc cho method nghiệp vụ.
Lớp metadata này được đọc bởi TableRevisionMetadata/RevisionEntityMetadata (kiểm tra thiếu @Id mã RV500110, xung đột @SQLDelete với @DomainRevision mã RV500101, cột cấu hình không tồn tại mã RV500007...) nhưng chưa được đăng ký trong auto-configuration: auto-config hiện chỉ tạo DefaultDataRevisionService dựng phiên bản từ audit_log. Vì vậy khi triển khai thực tế hãy dùng API DataRevisionService + cấu hình audit (@Audit, @AuditedEntity) như các mục trên; chỉ dùng nhóm annotation @Revision khi đã xác nhận với đơn vị phát triển.
Ví dụ đầu-cuối
Hồ sơ HS-2026-001 là entity gốc Document; mọi thay đổi được ghi vào audit_log qua cấu hình audit:
@Entity
@Table(name = "DOCUMENT")
@AuditedEntity(idColumn = "ID", targetEntity = Document.class)
public class Document {
@Id
@Column(name = "ID")
private String id;
@AuditedField(label = "Mã hồ sơ")
@Column(name = "DOCUMENT_CODE")
private String documentCode;
@AuditedField(label = "Trạng thái")
@Column(name = "STATUS")
private String status;
}
Sau hai thao tác tạo hồ sơ (TAO_HO_SO) và duyệt hồ sơ (DUYET_HO_SO), getVersions(Document.class, "HS-2026-001") trả về:
revisionNo | actionCode | actionName | actorName | actionTime |
|---|---|---|---|---|
| 1 | TAO_HO_SO | Tạo hồ sơ | Nguyễn Văn A | 2026-01-15 09:00:00 |
| 2 | DUYET_HO_SO | Duyệt hồ sơ | Trần Thị B | 2026-01-16 10:30:00 |
getSnapshot(Document.class, "HS-2026-001", 1) trả dữ liệu tại thời điểm tạo:
{
"documentCode": "HS-2026-001",
"status": "DRAFT"
}
restore(Document.class, "HS-2026-001", 1, "Khôi phục về bản nháp để chỉnh sửa") đưa status về DRAFT, sau đó ghi thêm phiên bản 3 mã KHOI_PHUC_PHIEN_BAN.
Luồng hoạt động
Sơ đồ dưới đây mô tả luồng phiên bản: audit_log được dựng thành snapshot và dùng để restore đối tượng.
Lưu ý
- Điều kiện tiên quyết:
govex-cloud-data-auditđang bật (govex.audit.enabled=true) và đã tạo bảng audit; module dựng phiên bản từ nhật ký audit, không ghi nhật ký riêng. DataRevisionServicekhông kiểm tra quyền truy cập — tầng gọi phải phân quyền trước khi đọc hoặc khôi phục dữ liệu.restore(...)chạy trong transaction và ghi thêm bản ghi audit mãKHOI_PHUC_PHIEN_BANsau khi commit; lỗi ghi nhật ký khôi phục được log và không làm hỏng transaction.- Giới hạn cửa sổ dữ liệu:
DefaultAuditLogService.findByTargetmặc định chỉ trả tối đa 100 bản ghi mới nhất (DEFAULT_FIND_LIMIT). VìgetVersions/getSnapshotđọc qua API này, lịch sử phiên bản mặc định chỉ thấy tối đa 100 bản ghi audit gần nhất vàrevisionNođược đánh số theo cửa sổ đó, không phải toàn bộ lịch sử. Muốn xem sâu hơn phải cung cấpAuditLogServiceghi đèfindByTarget(targetType, targetId, limit)trả đủ bản ghi. - Số phiên bản phải lớn hơn 0 và không vượt quá
maxRevisions(mặc định 1000 nếu không truyền). - Entity gốc cần được cấu hình audit (
@AuditedEntity), nếu khônggetSnapshot/restorenémIllegalStateException("Entity chưa được cấu hình audit root"). - Snapshot chỉ chứa thay đổi thuộc chính entity gốc, không gom dữ liệu bảng chi tiết.
- Trong lớp metadata
@DomainRevision: không dùng đồng thời với@SQLDeletetrên cùng entity (nên dùng@SoftDelete), và entity cần có field@Id. - Parser cho kiểu thời gian Oracle có trong dependency nhưng chưa được auto-config đăng ký — cần kiểm tra lại khi chạy Oracle.
restore(...)không gọisave/flush thủ công: entity gốc là managed entity trong transaction nên thay đổi field được ghi khi commit.getVersions/getSnapshotkhông cache — mỗi lần gọi đọc lạiaudit_log, vì vậy chi phí tăng theo số bản ghi trong cửa sổ dữ liệu.- Giá trị snapshot được chuyển về kiểu field bằng Jackson
convertValuetrước khi ghi; giá trị không chuyển được sẽ ném ngoại lệ khi khôi phục.