Vì sao nên dùng soft delete?
- Tránh “bay màu” dữ liệu ngoài ý muốn: Hard delete lỡ tay là đi luôn. Soft delete cho bạn một lớp an toàn.
- Compliance và nghiệp vụ: Nhiều ngành yêu cầu lưu vết giao dịch, lịch sử. Soft delete giúp “xóa với người dùng” nhưng vẫn giữ dữ liệu để kiểm toán.
- Không làm vỡ quan hệ dữ liệu: Bản ghi bị xóa mềm vẫn tồn tại nên foreign key, báo cáo lịch sử… không bị gãy.
- Dễ khôi phục: Un-delete nhanh gọn khi cần.
Khi nào KHÔNG nên dùng?
- Dữ liệu thật sự nhạy cảm cần xóa vĩnh viễn ngay (privacy by design).
- Bảng log quá lớn, giá trị khôi phục thấp, chi phí lưu trữ cao.
- Trường hợp có yêu cầu pháp lý “phải xóa hoàn toàn” trong thời hạn.
Các kỹ thuật triển khai phổ biến
1) Cột đánh dấu
deleted_at(datetime) – phổ biến nhất. Null = còn sống. Có timestamp = đã xóa.is_deleted(boolean) – true/false. Đơn giản nhưng kém giàu thông tin hơndeleted_at.
Ví dụ SQL:
-- Dùng deleted_at
SELECT *
FROM users
WHERE deleted_at IS NULL; -- Dùng is_deleted
SELECT *
FROM users
WHERE is_deleted = FALSE;
2) Global query filter / scope
Thiết lập một lớp filter mặc định để mọi truy vấn đều loại bản ghi đã xóa. Có thể thêm method để “bỏ qua” filter khi cần audit hoặc khôi phục.
3) Interceptor/Repository/Service layer
Đóng gói logic xóa mềm tại tầng repository/service để code đồng nhất, dễ test, dễ thay đổi chiến lược sau này.
4) Archiving (lưu trữ)
Sau một thời gian, chuyển dữ liệu đã xóa mềm sang bảng archive để gọn bảng chính và tối ưu chỉ mục.
Triển khai trong các framework phổ biến
Laravel
- Bật soft delete với trait
SoftDeletestrong Eloquent Model. - Migration thêm cột
deleted_atbằngsoftDeletes().
// migration
Schema::table('posts', function (Blueprint $table) { $table->softDeletes(); // tạo cột deleted_at
}); // model
use Illuminate\\Database\\Eloquent\\SoftDeletes; class Post extends Model
{ use SoftDeletes; // tự động filter deleted_at IS NULL
} // xóa mềm
Post::find($id)->delete(); // truy vấn bao gồm cả bản ghi đã xóa
Post::withTrashed()->get(); // chỉ các bản ghi đã xóa
Post::onlyTrashed()->get(); // khôi phục
Post::withTrashed()->find($id)->restore(); // xóa vĩnh viễn (bỏ qua soft delete)
Post::withTrashed()->find($id)->forceDelete();
Tài liệu: Laravel Docs – Eloquent: Soft Deleting.
Django
- Sử dụng package như
django-safedeleteđể có manager mặc định lọc record đã xóa, hỗ trợ restore và hard delete.
Repo: django-safedelete trên GitHub.
Ruby on Rails
- Dùng gem
discardhoặcparanoiađể códiscarded_atvà scope mặc định.
Gems: discard trên RubyGems • paranoia trên GitHub.
Entity Framework Core (.NET)
- Tạo cột
IsDeletedhoặcDeletedAt, sau đó cấu hình Global Query Filter trongOnModelCreating.
modelBuilder.Entity<Post>().HasQueryFilter(p => p.DeletedAt == null);
Tài liệu: Entity Framework Core Docs.
Ưu và nhược điểm
Ưu điểm
- Khôi phục dữ liệu dễ dàng.
- Không phá vỡ quan hệ khóa ngoại.
- Phục vụ audit, báo cáo lịch sử, compliance.
- Trải nghiệm người dùng “xóa nhưng còn cứu”.
Nhược điểm
- Tăng dung lượng bảng.
- Truy vấn phức tạp hơn nếu quên filter.
- Hiệu năng giảm nếu chỉ mục chưa tối ưu cho cột xóa mềm.
- Uniqueness constraint có thể rắc rối khi khôi phục bản ghi cũ.
Cách khắc phục
- Lập lịch purge xóa vĩnh viễn sau N ngày hoặc chuyển sang bảng archive.
- Tạo index cho
deleted_athoặcis_deleted. - Dùng Global Scope/Filter để tránh quên điều kiện.
- Với uniqueness, cân nhắc unique có điều kiện, thêm suffix khi khôi phục, hoặc kiểm tra va chạm trước khi restore.
Best practices “đỡ đau đầu”
- Nhất quán: đã chọn soft delete thì áp dụng xuyên suốt domain liên quan.
- Minh bạch: giải thích rõ với người dùng về cơ chế xóa.
- Phân quyền: chỉ vai trò phù hợp mới được xem và khôi phục bản ghi đã xóa.
- Audit log: ghi lại ai xóa, lúc nào, lý do.
- Observability: metric số lượng bản ghi đã xóa, thời gian khôi phục, kích thước bảng.
- Test kỹ: xóa, restore, hard delete, cascade, uniqueness…
Một số pattern thường gặp
- Soft delete + Versioning: theo dõi lịch sử thay đổi và cả trạng thái xóa.
- Soft delete + Audit logging: full trace cho compliance.
- Soft delete + Archiving: giữ bảng chính gọn nhẹ, nhanh hơn.
FAQ
Soft delete có an toàn không?
An toàn hơn hard delete trong đa số nghiệp vụ vì có thể khôi phục. Nhưng đừng coi là “bất tử” – vẫn cần backup, audit, và quy trình purge.
Có nên dùng cả is_deleted và deleted_at?
Không bắt buộc. deleted_at đã đủ linh hoạt. Chỉ dùng boolean nếu yêu cầu cực đơn giản.
Làm sao để không quên filter?
Thiết lập global scope/filter hoặc repository mặc định lọc. Chỉ “bỏ qua” filter khi có chủ đích.
Checklist triển khai nhanh
- [ ] Thêm cột
deleted_atvà index. - [ ] Bật global scope/filter.
- [ ] Viết service
delete,restore,forceDelete. - [ ] Lên lịch purge, hoặc chuyển archive.
- [ ] Thêm audit log.
- [ ] Viết test cho xóa, restore, uniqueness, cascade.
Tài liệu tham khảo
- Laravel Docs – Eloquent: Soft Deleting: Laravel Docs
- Django Safe Delete: GitHub – django-safedelete
- Rails gem discard: RubyGems – discard
- Rails gem paranoia: GitHub – paranoia
- Entity Framework Core Docs: Learn Microsoft
Schema và model Laravel chuẩn
Soft Delete không phải là một quyền hạn hay một cơ chế backup. Đây là cách đánh dấu bản ghi bằng thời điểm deleted_at để truy vấn mặc định ẩn bản ghi đã xóa. Laravel cung cấp helper migration và trait Eloquent; tên cột phải thống nhất với model.
// database/migrations/xxxx_xx_xx_add_soft_deletes_to_projects.php
Schema::table('projects', function (Blueprint $table) {
$table->softDeletes();
});
// app/Models/Project.php
use Illuminate\Database\Eloquent\SoftDeletes;
class Project extends Model
{
use SoftDeletes;
}Migration softDeletes() tạo cột timestamp nullable deleted_at. Nếu hệ thống cần timezone-aware timestamp, đối chiếu helper softDeletesTz(). Sau migration, kiểm tra schema trên database thật và viết test cho cả trạng thái chưa xóa, đã xóa và khôi phục.
Truy vấn, khôi phục và xóa vĩnh viễn
// Mặc định: chỉ bản ghi chưa bị xóa mềm
$active = Project::query()->latest()->get();
// Bao gồm bản ghi đã xóa
$withDeleted = Project::withTrashed()->findOrFail($id);
// Chỉ lấy bản ghi đã xóa
$deleted = Project::onlyTrashed()->where('team_id', $teamId)->get();
// Khôi phục có kiểm tra quyền
$project = Project::onlyTrashed()->findOrFail($id);
$project->restore();
// Xóa vĩnh viễn: cần policy, audit và quy trình retention
$project->forceDelete();Global scope của trait có thể khiến báo cáo hoặc job nền “thiếu” dữ liệu nếu người viết quên withTrashed(). Ngược lại, thêm withTrashed() tùy tiện có thể làm lộ dữ liệu đã xóa. Mọi endpoint restore/force delete nên có policy riêng, log actor, lý do và request id.
Unique constraint và dữ liệu liên quan
Soft Delete không tự giải quyết unique constraint. Nếu email, slug hoặc mã dự án phải dùng lại sau khi xóa, cần chọn chính sách rõ ràng: giữ unique mãi, đổi/giải phóng giá trị khi xóa, hoặc dùng unique index phù hợp với database. Không tự thêm điều kiện deleted_at vào migration mà chưa kiểm tra engine và query planner.
Với quan hệ cha-con, quyết định rõ bản ghi con có bị xóa mềm theo cha hay không. Nếu có, dùng service/action nhất quán thay vì trông chờ cascade database như hard delete. Khi restore, khôi phục theo thứ tự và kiểm tra foreign key, quyền truy cập, search index, cache, queue và file liên quan.
Retention, privacy và test cases
“Có thể khôi phục” không đồng nghĩa “giữ vô thời hạn”. Đặt retention policy: dữ liệu nào được restore, ai được phép, sau bao lâu phải force delete, backup giữ bao lâu và yêu cầu xóa dữ liệu cá nhân xử lý thế nào. Nếu bản ghi chứa PII, hãy tham khảo yêu cầu pháp lý và chính sách tổ chức thay vì dùng Soft Delete để trì hoãn quyết định.
it('hides and restores a project', function () {
$project = Project::factory()->create();
$project->delete();
expect(Project::find($project->id))->toBeNull();
expect(Project::withTrashed()->find($project->id))->not->toBeNull();
$project->restore();
expect(Project::find($project->id))->not->toBeNull();
});
it('requires explicit authorization for force delete', function () {
// test policy, audit event and retention rule before forceDelete()
});- Test default scope,
withTrashedvàonlyTrashed. - Test restore khi bản ghi liên quan đã thay đổi hoặc bị xóa.
- Test unique key sau xóa và khôi phục đồng thời.
- Test policy cho restore/force delete và audit log.
- Test job, notification, cache và search index không vô tình lấy bản ghi đã xóa.