Lập trình · 29/09/2026

Soft Delete trong Production: Xóa mềm không đơn giản là thêm deleted_at

Soft delete giúp một bản ghi biến mất khỏi luồng sử dụng thông thường nhưng vẫn còn trong cơ sở dữ liệu để khôi phục, điều tra hoặc tuân thủ quy trình lưu giữ. Cách làm phổ biến là thêm cột deleted_at rồi mặc định lọc các hàng có giá trị khác NULL. Cơ chế này dễ triển khai, nhưng nếu chỉ dừng ở một cột thời gian, hệ thống có thể trả nhầm dữ liệu đã xóa, khóa mất email của người dùng mới, làm sai báo cáo, khôi phục quan hệ không nhất quán hoặc giữ dữ liệu cá nhân lâu hơn mức được phép.

Soft Delete trong Production: Xóa mềm không đơn giản là thêm deleted_at

Soft delete giúp một bản ghi biến mất khỏi luồng sử dụng thông thường nhưng vẫn còn trong cơ sở dữ liệu để khôi phục, điều tra hoặc tuân thủ quy trình lưu giữ. Cách làm phổ biến là thêm cột deleted_at rồi mặc định lọc các hàng có giá trị khác NULL. Cơ chế này dễ triển khai, nhưng nếu chỉ dừng ở một cột thời gian, hệ thống có thể trả nhầm dữ liệu đã xóa, khóa mất email của người dùng mới, làm sai báo cáo, khôi phục quan hệ không nhất quán hoặc giữ dữ liệu cá nhân lâu hơn mức được phép.

Một thiết kế production cần xem soft delete như một phần của vòng đời dữ liệu. Nhóm phát triển phải xác định rõ trạng thái, phạm vi truy vấn, quy tắc duy nhất, tác động lên quan hệ, quyền khôi phục, thời hạn lưu giữ và đường dẫn xóa vĩnh viễn. Bài viết này trình bày cách biến một tiện ích ORM thành hợp đồng có thể kiểm thử và vận hành an toàn.

1. Xác định lý do trước khi chọn soft delete

Không phải bảng nào cũng cần xóa mềm. Hãy bắt đầu bằng câu hỏi: bản ghi cần được giữ lại để làm gì? Một đơn hàng đã ghi nhận doanh thu thường phải được bảo toàn như lịch sử nghiệp vụ; tài khoản bị người dùng xóa có thể cần khoảng chờ để hoàn tác; cấu hình nháp có thể chỉ cần xóa cứng. Nếu không có mục tiêu khôi phục, audit hay retention cụ thể, soft delete chỉ làm tăng diện tích lỗi và chi phí lưu trữ.

Nhu cầuCách phù hợpLưu ý
Hoàn tác thao tác người dùng trong thời gian ngắnSoft delete và cửa sổ khôi phụcCần ngày hết hạn và quyền restore rõ ràng
Lưu lịch sử giao dịch bất biếnTrạng thái nghiệp vụ hoặc sổ cáiKhông nên giả vờ rằng giao dịch chỉ bị “xóa”
Điều tra ai đã thay đổi dữ liệuAudit log riêngdeleted_at không ghi đủ tác nhân và lý do
Loại bỏ dữ liệu nhạy cảm theo chính sáchPurge hoặc ẩn danh hóa có kiểm soátSoft delete không đáp ứng yêu cầu xóa vĩnh viễn
Soft delete là trạng thái nghiệp vụ có thời hạn, không phải bản sao lưu và cũng không thay thế audit log.

2. Thiết kế schema thể hiện đủ ngữ nghĩa

Cột deleted_at là điểm khởi đầu tốt vì cho biết thời gian chuyển trạng thái và hỗ trợ retention. Với hệ thống cần truy vết, có thể bổ sung deleted_by, deletion_reason hoặc một bảng sự kiện audit. Không nên nhồi thông tin nhạy cảm hay nội dung tự do không giới hạn vào lý do xóa; các mã lý do chuẩn hóa thường dễ tìm kiếm và báo cáo hơn.

ALTER TABLE customers
  ADD COLUMN deleted_at TIMESTAMP NULL,
  ADD COLUMN deleted_by BIGINT NULL,
  ADD COLUMN deletion_reason VARCHAR(64) NULL;

CREATE INDEX idx_customers_active
  ON customers (deleted_at, id);

Thứ tự cột trong index phải dựa trên truy vấn và bộ máy cơ sở dữ liệu thực tế, không sao chép máy móc ví dụ trên. Nếu hầu hết truy vấn đều lấy bản ghi đang hoạt động theo tenant và trạng thái, một index bắt đầu bằng tenant_id có thể phù hợp hơn. Với hệ quản trị hỗ trợ partial index, index chỉ chứa hàng chưa xóa có thể nhỏ và hiệu quả; với hệ khác, cần đo bằng execution plan và dữ liệu gần production.

Đừng dùng riêng một cờ Boolean nếu retention cần biết bản ghi bị xóa khi nào. Ngược lại, đừng suy đoán người thực hiện từ phiên đăng nhập hiện tại khi job nền hoặc quản trị viên có thể khởi tạo thao tác. Thông tin tác nhân phải được truyền tường minh qua lớp ứng dụng hoặc ghi vào audit event cùng transaction.

3. Biến phạm vi truy vấn thành mặc định an toàn

Lỗi nguy hiểm nhất là quên thêm WHERE deleted_at IS NULL. ORM thường cung cấp global scope, nhưng raw SQL, báo cáo, job nền, truy vấn join và kho dữ liệu có thể đi vòng qua scope đó. Vì vậy, quy ước phải áp dụng từ repository đến dashboard vận hành: truy vấn thông thường chỉ thấy dữ liệu hoạt động; truy vấn có dữ liệu đã xóa phải mang tên rõ như withDeleted và yêu cầu quyền riêng.

SELECT id, email, display_name
FROM customers
WHERE tenant_id = :tenant_id
  AND deleted_at IS NULL
ORDER BY id DESC
LIMIT :limit;

API lấy chi tiết cũng cần thống nhất semantics. Với người dùng thông thường, ID đã xóa nên trả về kết quả giống tài nguyên không tồn tại để tránh lộ thông tin. Giao diện quản trị có thể hiển thị trạng thái đã xóa nhưng phải qua authorization. Cache key, search index và materialized view phải được vô hiệu hóa hoặc cập nhật khi bản ghi chuyển trạng thái; chỉ sửa database rồi giữ cache cũ sẽ khiến dữ liệu “đã xóa” tiếp tục xuất hiện.

4. Giải bài toán unique constraint

Giả sử email là duy nhất. Khi tài khoản a@example.com bị xóa mềm, sản phẩm có cho phép đăng ký tài khoản mới bằng email đó không? Câu trả lời là quyết định nghiệp vụ, không phải chi tiết ORM. Nếu không cho phép tái sử dụng, unique constraint toàn bảng là đúng và khôi phục đơn giản. Nếu cho phép, constraint phải chỉ áp dụng cho bản ghi hoạt động hoặc khóa duy nhất phải được biến đổi khi xóa.

  • Partial unique index: rõ ràng khi database hỗ trợ điều kiện WHERE deleted_at IS NULL.
  • Khóa tổng hợp: có thể dùng cột trạng thái hoặc khóa dẫn xuất, nhưng semantics của NULL khác nhau giữa các hệ quản trị.
  • Đổi giá trị khi xóa: thêm hậu tố vào email hoặc slug giúp giải phóng khóa nhưng làm mất giá trị gốc nếu không lưu riêng, đồng thời dễ hỏng khi retry.

Luồng khôi phục phải xử lý xung đột. Nếu một tài khoản mới đã chiếm email cũ, không được chỉ đặt deleted_at = NULL rồi hy vọng transaction thành công. Hệ thống cần trả lỗi nghiệp vụ dễ hiểu, cho phép chọn định danh khác hoặc chuyển sang quy trình hợp nhất được kiểm soát.

5. Quan hệ và “cascade mềm” cần chính sách tường minh

Xóa mềm một project không tự động trả lời số phận của task, attachment và comment. Có ba lựa chọn chính: giữ con hoạt động nhưng ẩn theo trạng thái cha; xóa mềm cả cây; hoặc chặn xóa khi còn phụ thuộc. Mỗi lựa chọn có trade-off về khôi phục, hiệu năng và tính dễ hiểu. Cascade của khóa ngoại thường chỉ chạy khi xóa cứng, nên không thể dựa vào ON DELETE CASCADE cho một lệnh update deleted_at.

Nếu xóa cả cây, thao tác phải idempotent và có chiến lược cho tập dữ liệu lớn. Một transaction cập nhật hàng triệu hàng có thể giữ lock lâu và làm phình transaction log. Có thể chuyển cha sang trạng thái “đang xóa”, xử lý con theo batch với checkpoint rồi hoàn tất. Trong thời gian đó, mọi đường đọc phải hiểu trạng thái trung gian. Nếu chỉ ẩn con theo cha, truy vấn trực tiếp task vẫn phải join hoặc kiểm tra project để không làm lộ bản ghi mồ côi về mặt nghiệp vụ.

Khôi phục cũng không nhất thiết đối xứng. Một attachment đã qua hạn retention có thể bị purge dù project vẫn còn khả năng restore. Hệ thống phải thông báo phần nào khôi phục được, phần nào mất vĩnh viễn, thay vì trả thành công chung chung.

6. Xóa và khôi phục là các transition có điều kiện

Hai request đồng thời có thể cùng xóa, một request restore trong khi job purge bắt đầu, hoặc quản trị viên sửa dữ liệu giữa lúc người dùng xác nhận. Dùng update có điều kiện giúp phát hiện trạng thái đã thay đổi và làm retry an toàn hơn.

UPDATE customers
SET deleted_at = :now,
    deleted_by = :actor_id,
    deletion_reason = :reason
WHERE id = :id
  AND deleted_at IS NULL;

Nếu số hàng cập nhật bằng 0, ứng dụng phải đọc lại trạng thái hoặc trả conflict phù hợp; không nên báo “đã xóa” khi ID không tồn tại. Restore dùng điều kiện ngược lại và phải kiểm tra unique constraint trước hoặc xử lý lỗi constraint trong transaction. Khi thao tác còn phát sự kiện, ghi outbox intent trong cùng transaction để tránh database đã đổi nhưng message không được phát. Consumer vẫn phải idempotent vì broker có thể giao lại.

7. Retention và purge là phần bắt buộc của thiết kế

Nếu không có purge, soft delete trở thành lưu trữ vô thời hạn. Hãy định nghĩa retention theo loại dữ liệu: ví dụ nội dung người dùng có cửa sổ hoàn tác, log kỹ thuật có thời hạn khác, còn chứng từ có yêu cầu riêng. Chính sách phải xét nghĩa vụ pháp lý, hợp đồng và nhu cầu nghiệp vụ thực tế; không lấy một con số chung cho mọi bảng.

  1. Job chọn bản ghi đã xóa trước cutoff, theo khóa ổn định và batch giới hạn.
  2. Kiểm tra legal hold, trạng thái phụ thuộc và điều kiện khôi phục đang mở.
  3. Xóa hoặc ẩn danh dữ liệu con theo đúng thứ tự khóa ngoại.
  4. Xóa file object storage và search document bằng thao tác retry được.
  5. Ghi metric, số lượng thành công/thất bại và checkpoint, không log dữ liệu nhạy cảm.

Database và object storage không tạo một transaction chung. Nếu xóa file trước rồi transaction database rollback, metadata còn nhưng file mất; nếu xóa database trước rồi xóa file thất bại, file trở thành orphan. Một workflow có trạng thái, retry và reconciler định kỳ thường thực tế hơn lời hứa “xóa nguyên tử”. Bản sao lưu cũng cần chính sách hết hạn: purge ở database chính không làm dữ liệu biến mất ngay khỏi backup còn hiệu lực.

8. Audit, bảo mật và quyền khôi phục

Audit event nên ghi loại tài nguyên, ID nội bộ, tác nhân, thời điểm, lý do chuẩn hóa, nguồn request và kết quả. Không ghi toàn bộ payload nếu trong đó có mật khẩu, token, tài liệu hoặc dữ liệu cá nhân không cần thiết. Log ứng dụng phục vụ chẩn đoán; audit log phục vụ truy cứu hành động. Hai loại có thời hạn, quyền truy cập và yêu cầu toàn vẹn khác nhau.

Quyền delete và restore nên tách biệt. Nhân viên hỗ trợ có thể được khôi phục trong 24 giờ nhưng không được xem trường nhạy cảm; quản trị viên có thể purge nhưng cần phê duyệt hai bước với tài nguyên quan trọng. Endpoint quản trị cần CSRF hoặc token phù hợp, rate limit, xác nhận lại danh tính và nhật ký không thể sửa tùy ý. Soft-deleted data vẫn là dữ liệu thật, vì vậy vẫn phải mã hóa, phân quyền và bảo vệ khỏi truy vấn chéo tenant.

9. Kiểm thử theo trạng thái và điểm lỗi

Unit test một model chưa đủ. Bộ test cần chạy qua HTTP, transaction, queue và storage để chứng minh hợp đồng ở ranh giới hệ thống.

  • Danh sách, tìm kiếm và detail không trả bản ghi đã xóa cho người dùng thường.
  • Quản trị viên chỉ xem/restore đúng tenant và đúng quyền.
  • Xóa lặp lại không phát nhiều sự kiện hoặc tạo side effect trùng.
  • Restore phát hiện xung đột email, slug hoặc khóa nghiệp vụ.
  • Cha bị xóa không làm lộ con qua endpoint trực tiếp.
  • Purge dừng giữa batch có thể chạy tiếp từ checkpoint.
  • Lỗi xóa file được retry và reconciler tìm được orphan.
  • Metric và audit có đủ ngữ cảnh nhưng không chứa bí mật.

Hãy thêm test concurrency trên database production-like cho transition quan trọng; SQLite tuần tự không chứng minh hành vi lock của MySQL hoặc PostgreSQL. Với index và query scope, đo execution plan trên phân bố dữ liệu gần thực tế. Một truy vấn đúng trên mười hàng vẫn có thể trở thành full scan khi bảng chứa hàng trăm triệu bản ghi đã xóa.

10. Checklist đưa soft delete lên production

  • Mục tiêu khôi phục, audit và retention đã được viết thành chính sách.
  • Mọi đường đọc mặc định loại dữ liệu đã xóa, kể cả search và cache.
  • Unique constraint phản ánh đúng việc có cho tái sử dụng định danh hay không.
  • Quan hệ cha–con có quy tắc delete, restore và purge tường minh.
  • Transition dùng update có điều kiện, transaction và sự kiện retry an toàn.
  • Job purge có batch, checkpoint, legal hold, metric và reconciler.
  • Quyền xem, xóa, khôi phục và purge được tách theo vai trò.
  • Backup, bản sao phân tích và dữ liệu dẫn xuất có vòng đời tương ứng.

Kết luận

Soft delete hữu ích khi sản phẩm thật sự cần cửa sổ khôi phục hoặc trạng thái lưu giữ có kiểm soát. Giá trị của nó không nằm ở cột deleted_at, mà ở hợp đồng bao quanh cột đó: truy vấn mặc định an toàn, uniqueness rõ ràng, quan hệ nhất quán, transition chống race condition, audit tối thiểu, retention có điểm kết thúc và purge quan sát được. Khi những quyết định này được kiểm thử từ database đến API và storage, “xóa mềm” mới trở thành một năng lực production thay vì khoản nợ dữ liệu bị trì hoãn.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.