DEV Community

Cover image for Cara Membuat Soft Delete di Entity Framework Core dengan Global Query Filter
Rahmat Al Hakam
Rahmat Al Hakam

Posted on

Cara Membuat Soft Delete di Entity Framework Core dengan Global Query Filter

Permasalahan dan Business Requirement

Proses pembuatan sistem wajib hukumnya mengikuti business requirement yang telah ditentukan. Terkadang, requirement tersebut mewajibkan untuk tidak menghapus data secara permanen di database. Berbagai alasan menjadi pertimbangan, seperti

  1. Tracing riwayat
  2. Backup data
  3. Lebih mudah debugging code (percaya ga percaya 🤣).

Pendekatan Pemecahan Permasalahan

Pendekatan yang dapat dilakukan adalah menggunakan metode soft delete. Metode ini sebenarnya hanya menyembunyikan data pada saat sistem berjalan dengan menambahkan flag true-false pada kolom IsDeleted di setiap table.

Metode Soft Delete dengan Global Query Filter

Step 1: buat base interface dan builder nya

Hal yang pertama dilakukan adalah membuat interface dan builder sebagai basis untuk model yang akan menerapkan fitur soft delete.
Buat file IEntityBase.cs dan IEntityBaseBuilder pada folder Models\EntityBases.

IEntityBase.cs

namespace SoftDeleteTutorial.Models.EntityBases;

public interface IEntityBase
{
    Guid Id { get; set; } //ditaruh di interface karena setiap model butuh Id
    bool IsDeleted { get; set; } //flagging utk soft delete
}
Enter fullscreen mode Exit fullscreen mode

IEntityBaseBuilder.cs

namespace SoftDeleteTutorial.Models.EntityBases;
public interface IEntityBaseBuilder<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : class
{
}
Enter fullscreen mode Exit fullscreen mode

Step 2: buat abstract model dan builder yang mengimplement interface

Lankah selanjutnya adalah membuat abstract basis model yang mengimplement interface pada step 1. Model yang akan dibuat adalah EntityBase.cs dan builder modelnya EntityBaseBuilder.cs.

EntityBase

namespace SoftDeleteTutorial.Models.EntityBases;

public abstract class EntityBase : IEntityBase //abstract class mengimplementasikan interface base
{
    public Guid Id { get; set; }
    public bool IsDeleted { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

EntityBaseBuilder

namespace SoftDeleteTutorial.Models.EntityBases;

public abstract class EntityBaseBuilder<TEntity> : IEntityBaseBuilder<TEntity> where TEntity : class, IEntityBase //implementasi base interface
{
    //konfigurasi global query filter
    public virtual void Configure(EntityTypeBuilder<TEntity> builder)
    {
        builder.HasQueryFilter(t => !t.IsDeleted); //filter data yang IsDeleted = 0 (false)
    }
}
Enter fullscreen mode Exit fullscreen mode

step 3: cara menggunakan EntityBase pada Model

Cara penggunaannya cukup simple yaitu dengan mengimplementasi interface EnitityBase dan EntityBaseBuilder pada model yang ingin menggunakan fitur soft delete. Contoh, kita membuat model Student.cs dan StudentBuilder.cs. Simpan file tersebut pada folder Models\Entities.

Student.cs

namespace SoftDeleteTutorial.Models.Entities;

public class Student : EntityBase //implement dari abstract class
{
    //tidak perlu proprty Id karena sudah ada di EntityBase
    public string Name { get; set; } = string.Empty;
    public string Nim { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

StudentBuilder.cs

namespace SoftDeleteTutorial.Models.Entities;

public class StudentBuilder : EntityBaseBuilder<Student> //implement EntityBaseBuilder utk class model yang diinginkan
{
    // override konfigurasi yang sebelumnya telah ada query filternya
    public override void Configure(EntityTypeBuilder<Student> builder)
    {
        base.Configure(builder);
        builder.Property(s => s.Name).HasMaxLength(100);
        builder.Property(s => s.Nim).HasMaxLength(10);
        builder.HasIndex(s => s.Nim).IsUnique();
    }
}
Enter fullscreen mode Exit fullscreen mode

step 4: buat Ekstensi untuk tracking hapus data

Hal yang diharapkan saat soft delete adalah otomatis set IsDelete = true ketika ada penghapusan data. Kita tidak ingin setiap ada penghapusan data harus set IsDeleted=true. Oleh karena itu, kita perlu membuat ekstensi yang akan mentracking setiap penghapusan data. Kita beri nama ChangeTrackerExtensions.cs yang ditaruh pada folder Extentions.

ChangeTrackerExtensions.cs

using SoftDeleteTutorial.Models.EntityBases;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace SoftDeleteTutorial.Extensions;

public static class ChangeTrackerExtensions
{
    public static void SetAuditProperties(this ChangeTracker changeTracker)
    {
        changeTracker.DetectChanges();
        IEnumerable<EntityEntry> entities = changeTracker.Entries().Where
            (
                t =>
                t.Entity is IEntityBase
                && (t.State == EntityState.Deleted)
            );

        foreach (EntityEntry entry in entities)
        {
            IEntityBase entity = (IEntityBase)entry.Entity;

            if (entry.State == EntityState.Deleted)
            {
                entity.IsDeleted = true; //jika ada penghapusan data maka IsDeleted=true
                entry.State = EntityState.Modified;
            }
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

step 5: override method savechange di ApplicationDbContext

Beberapa method yang harus dioverride ada 4 yaitu

  1. SaveChangesAsync(CancellationToken cancellationToken = default)
  2. SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
  3. SaveChanges()
  4. SaveChanges(bool acceptAllChangesOnSuccess)

Method tersebut akan ditambah ekstensi ChangeTracker yang sudah dibuat pada step 4.

AppDbContext.cs

namespace SoftDeleteTutorial.Models;
public class AppDbContext 
{
    public AppDbContext(DbContextOptions options) : base(options)
    {

    }
    public DbSet<Student> Students { get; set; } = default!;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        new StudentBuilder().Configure(modelBuilder.Entity<Student>());
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        ChangeTracker.SetAuditProperties();
        return await base.SaveChangesAsync(cancellationToken);
    }

    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
    {
        ChangeTracker.SetAuditProperties();
        return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

    public override int SaveChanges()
    {
        ChangeTracker.SetAuditProperties();
        return base.SaveChanges();
    }

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        ChangeTracker.SetAuditProperties();
        return base.SaveChanges(acceptAllChangesOnSuccess);
    }
}
Enter fullscreen mode Exit fullscreen mode

step 6: sudah selesai ❤️

Unique dan Index Coloumn

Jika menggunakan soft delete ada beberapa hal yang harus disesuaikan terutama untuk kolom yang unique atau berstatus index. Soft delete yang tidak menghapus secara fisik, artinya jika ada data baru yang ditambahkan dengan data yang sama bisa jadi kena error dulicate key. Contohnya, property Nim pada model Student adalah unique dengan data new Student {Name="Rahmat", Nim="123"}. Lalu dihapus tuh student atas nama Rahmat. Nah, jika ada data dengan Nim="123" maka akan error karena dianggap duplicate key. Oleh karena itu kita perlu mengecek ketika insert data Student dengan IgnoreQueryFilter(). Contoh kodingan seperti berikut.

private async Task<Student> InsertStudent(StudentInput input, CancellationToken cancellationToken)
    {
        //IgnoreQueryFilter() untuk mengambil semua data tanpa melihat flag IsDeleted.
        var student = await _dbContext.Students.IgnoreQueryFilters().SingleOrDefaultAsync(s => s.Nim == input.Nim, cancellationToken); 
        if (student != null && !student.IsDeleted)
            throw new BusinessLogicException($"NIM {input.Nim} sudah terdaftar. Silahkan daftar mahasiswa lainnya!");
        else if (student != null && student.IsDeleted)
        {
            student.IsDeleted = false; //*membangkitkan* yang sudah terhapus
            await _dbContext.SaveChangesAsync(cancellationToken);
            return student;
        }
        var newStudent = new Student { Name = input.Name, Nim = input.Nim };
        await _dbContext.Students.AddAsync(newStudent, cancellationToken);
        await _dbContext.SaveChangesAsync(cancellationToken);
        return newStudent;
    }
Enter fullscreen mode Exit fullscreen mode

Saran dan Masukan

Jika terdapat pertanyaan pada artikel ini bisa bertanya di kolom komentar. Saya juga sangat menerima jika ada masukan dan kritik, siapa tahu anda bisa kasih saran yang lebih baik atau mendapati kesalahan code. Terimakasih semuanya 😊👍

Top comments (1)

Collapse
 
makarimachmad profile image
achmad makarim widyanto

Interesting..