Chuyển tới nội dung chính

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=truegovex-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 @Audit với targetEntity = Document.class và xác định được targetId.
  • Entity gốc gắn @AuditedEntity (kèm cột ID/target) để thay đổi được capture vào changes.

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:

  • revisionNo tí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 changes của các bản ghi thuộc chính entity gốc cho tới revisionNo. Thay đổi của bảng chi tiết (entityName khá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:

  1. Chạy trong transaction qua TransactionTemplate.
  2. Tìm entity gốc bằng EntityManager.find(...); không tìm thấy dữ liệu → IllegalStateException.
  3. Dựng snapshot của phiên bản đích; snapshot rỗng → IllegalStateException.
  4. 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.
  5. 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", context là 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
RevisionInfoTó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ụcAuditEntryKHOI_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):

  • @DomainRevision trê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).
  • @Revision trê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 @IdRV500110, xung đột @SQLDelete với @DomainRevisionRV500101, 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ề:

revisionNoactionCodeactionNameactorNameactionTime
1TAO_HO_SOTạo hồ sơNguyễn Văn A2026-01-15 09:00:00
2DUYET_HO_SODuyệt hồ sơTrần Thị B2026-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.
  • DataRevisionService khô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_BAN sau 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.findByTarget mặ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ấp AuditLogService ghi đè 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ông getSnapshot/restore ném IllegalStateException ("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 @SQLDelete trê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ọi save/flush thủ công: entity gốc là managed entity trong transaction nên thay đổi field được ghi khi commit.
  • getVersions/getSnapshot không cache — mỗi lần gọi đọc lại audit_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 convertValue trước khi ghi; giá trị không chuyển được sẽ ném ngoại lệ khi khôi phục.