Borrado lógico con Entity Framework

  • net-core
  • ef-core

Hay muchas ocasiones en las que queremos dar al usuario la opción de borrar información de la base de datos, pero no nos interesa eliminar el registro de la tabla, por mantener un histórico, por evitar problemas con foreign keys… En estos casos, la mejor solución es la implementación de un borrado lógico. De esta forma, a ojos del usuario los datos han quedado eliminado, pero realmente siguen existiendo en la base de datos.

El borrado lógico en Entity Framework es una técnica utilizada para conseguir lo anteriormente expuesto, que no es otra cosa que marcar un registro como eliminado en lugar de borrarlo físicamente de la base de datos. Esto permite recuperar los datos en caso de que sean necesarios en un momento posterior.

Para implementar dicho borrado lógico, podemos seguir los siguientes pasos:

  • En el modelo de entidad, agregar una propiedad de tipo bool que indique si el registro está eliminado o no.
    Algo que me gusta hacer es tener una clase base, de las que heredan todas las entidades del modelo que van a implementar un borrado lógico. De esta manera, no tenemos un montón de clases con una propiedad booleana para indicar si el registro está borrado.
public class BaseEntity
{
    public bool IsDeleted { get; set; }
}
  • Con la propiedad anterior, todavía no estamos evitando que un registro se elimine de la base de datos, simplemente tenemos una propiedad booleana más en nuestra entidad.
    Para evitar ese borrado es necesario sobreescribir el método SaveChanges de nuestro DbContext para incluir la lógica de borrado lógico. En este método, se deben recorrer todas las entidades que se van a guardar y, si una de ellas está en un estado Deleted, se debe establecer su estado como Modified y establecer el valor de su propiedad IsDeleted a true en lugar de eliminarla físicamente de la base de datos.
    Por otro lado, puede ser una buena práctica establecer el valor IsDeleted a false si el estado de la entidad es Added, así nos aseguramos que, al añadir un elemento, no estará marcado como eliminado.
public override int SaveChanges()
{
    UpdateSoftDeleteStatuses();
    return base.SaveChanges();
}

public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
    UpdateSoftDeleteStatuses();
    return base.SaveChanges(acceptAllChangesOnSuccess);
}

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

public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    UpdateSoftDeleteStatuses();
    return base.SaveChangesAsync(cancellationToken);
}

private void UpdateSoftDeleteStatuses()
{
    foreach (EntityEntry entry in ChangeTracker.Entries())
    {
        BaseEntity baseEntity = entry.Entity as BaseEntity;
        if (baseEntity == null)
            return;

        switch (entry.State)
        {
            case EntityState.Added:
                baseEntity.IsDeleted = false;
                break;
            case EntityState.Deleted:
                entry.State = EntityState.Modified;
                baseEntity.IsDeleted = true;
                break;
        }
    }
}
  • Con todo lo anterior, ya hemos conseguido que no se eliminen los registros físicamente de la base de datos, pero el trabajo no está terminado. En este punto, si obtenemos registros, se estarían devolviendo los que está marcados como eliminados, algo que no es deseable ya que, a ojos del usuario, el registro está borrado. Por tanto, es necesario modificar las consultas para que solo devuelvan registros que no han sido marcados como borrados. Esto se puede hacer utilizando el método Where y añadiendo un filtro para retornar los registros cuyo valor IsDeleted es false, pero sería un poco tedioso ir añadiendo ese filtro en toda la consulta que hagamos. Para simplificarlo, podemos sobreescribir el método OnModelCreating de nuestro DbContext e indicar que todas las entidades con borrado lógico tienen un query filter.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    ...
    var entityBuilder = modelBuilder.Entity<PersonEntity>();
    entityBuilder.HasQueryFilter(x => !x.IsDeleted);
    ...
}

Con estos pasos, ya habríamos implementado correctamente el borrado lógico en Entity Framework. Con lo que los registros eliminados seguirán existiendo en la base de datos, pero no se mostrarán en las diferentes consultas.