Hoy vamos a explorar algo que mencioné en el post sobre eliminar datos, que es cuando queremos eliminar dichos datos, pero sin borrarlos de la base de datos, para que se quede el registro
Índice
Como siempre, este post es parte de un curso, para el cual tienes el código disponible en GitHub.
1 - Qué es soft delete en una base de datos?
Para entender el concepto, lo más sencillo es que entendamos que es que cuando eliminamos un registro de la base de datos lo estamos eliminando completamente, si lo volvemos a buscar en la base de datos, este ya no estará disponible.
Un soft delete es similar pero no igual, lo que hacemos es, en todas nuestras entidades, tener una propiedad llamada IsDeleted
esta propiedad es un simple booleano que indica si su estado es eliminada o no, pero claro, está eliminada “lógicamente” ya que el registro como tal, está disponible en la base de datos.
Esta acción es muy útil, ya que nos permite mantener el registro y bueno, si eliminamos algo por error o por un bug, no se elimina completamente. Además nos puede servir para tener un poco de visibilidad con ciertas acciones.
Algo que a mi me gusta mucho, además es incluir un timestamp con cuando se ha eliminado, o cuando fue la última vez que se modificó.
Esta información es puramente informativa por si algún día descubres un registro que está borrado y no debería tener información de cuando sucedió e ir a buscar información en los logs correspondientes.
- NOTA: Si quieres mantener una trazabilidad de todo lo que sucede en tu entidad, recomiendo que utilices event sourcing, o bitemporal data modeling.
2 - Implementar soft delete en entity framework core
Para implementar soft delete en EF Core lo que tenemos que hacer es añadir dichas propiedades a la entidad. Mi recomendación personal es que TODAS las entidades lo tengan implementado, y por lo tanto, sea parte de una clase base:
public abstract class CursoEFBaseEntity
{
public bool IsDeleted { get; set; }
public DateTime DeletedTimeUtc { get; set; }
}
Y luego cada una de tus entidades debe implementar dicha clase base.
public class User : CursoEFBaseEntity
{
...
}
public class Wokringexperience : CursoEFBaseEntity
{
...
}
Ahora lo que tenemos que hacer es, cuando hacemos queremos eliminar un registro, en vez de utilizar el método .Remove()
, lo que hacemos es un update, con la propiedad IsDeleted=true
.
public async Task<bool> Delete(int id)
{
User? user = await _context.Users
.FirstOrDefaultAsync(x => x.Id == id);
if (user == null)
return false;
user.IsDeleted = true;
user.DeletedTimeUtc = DateTime.UtcNow;
_context.Users.Update(user);
return true;
}
- Nota: que no se te olvide correr las migraciones.
Si probamos ahora, veremos que el registro de la base de datos a pasado a eliminado:
- Nota: Dependerá de tu arquitectura el saber sí tienes que marcar todos los registros que hacen referencia a tu entidad principal como
delete true
también o no, en algunos casos verás que es necesario, en otros no lo será tanto, como digo, depende de cada empresa/dominio.
Como te puedes imaginar esta propiedad es simplemente una propiedad, ni entity framework ni la base de datos van a hacer nada de forma mágica para no mostrar el registro si se lo pides. por lo que si vas ahora a tu api y pides el Id correspondiente, te lo devolverá con la propiedad IsDeleted
y el valor true
.
Para que Entity framework lo oculte tenemos dos opciones.
La primera, y más obvia, es, actualizar todas las consultas para poner un where
y excluir todos los registros donde el isDeleted
es true. Lo cual puede ser una verdadera pesadilla si tenemos muchas consultas, además tendrías que actualizar todos los includes, etc.
La segunda y más eficiente es configurar esta información en nuestro método OnModelCreating
del DbContext.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasQueryFilter(a => !a.IsDeleted);
}
Cuando utilizamos HasQueryFilter
, lo que estamos haciendo es añadir ese filtro a todas las consultas SQL que se vayan a ejecutar sobre dicha entidad.
Esta opción es mucho mejor, pero igual que vimos en el post de data seed; podemos delegar esta configuración a sus clases específicas (si las tienes creadas):
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasQueryFilter(a => !a.IsDeleted);
builder.HasData(
new User { Email = "[email protected]", Id = 1, UserName = "user1", IsDeleted = false},
new User { Email = "[email protected]", Id = 2, UserName = "user2", IsDeleted = false }
);
}
Que por supuesto también funciona.
Si ejecutamos el código y corremos el mismo endpoint que antes, vemos que no nos devuelve nada.
Además, si comprobamos la query SQL ejecutada, vemos que tiene el filtro especificado.
2.1 - Limitaciones de los filtros globales
Hay que tener en cuenta que los filtros pueden tener algunas limitaciones, por ejemplo, con Entity Framework podemos ejecutar consultas SQL directamente escribiendolas en el código, si hacemos eso, nos estaremos saltando el filtro.
O por ejemplo, hay otra función llamada IgnoreQueryFilters
que omite todos los filtros globales.
Espero que este post te sea de utilidad y si tienes cualquier pregunta no dudes en hacerla por twitter o por youtube.
Un saludo!