Ir al contenido

El borrado suave no borra, y ese es el problema

El trait es una línea y cambia lo que significan las restricciones de unicidad, las claves foráneas y las solicitudes de supresión. Casi nadie decide nada de eso al adoptarlo.

5 min de lectura

Añadir SoftDeletes a un modelo cuesta una línea, y todos los tutoriales lo presentan como deshacer gratis. No es gratis. Cambia el significado del borrado en toda la aplicación, y las consecuencias aterrizan en sitios que no tienen nada que ver con el modelo al que se lo añadió.

Qué ocurre en realidad

delete() deja de borrar. Pone deleted_at y la fila se queda. Un scope global la esconde de las consultas. Ese es todo el mecanismo, y todos los problemas de abajo se siguen de que la fila siga ahí.

Las restricciones de unicidad dejan de funcionar

Una persona se registra con una dirección, borra su cuenta, e intenta registrarse otra vez con la misma dirección. La fila sigue en la tabla, el índice único sigue en la columna, y el registro falla con un error de base de datos sobre un registro que el usuario no puede ver y que soporte no encuentra.

La aplicación dice que la cuenta no existe. La base de datos dice que sí. Las dos dicen la verdad sobre cosas distintas.

Dos salidas, y son elecciones más que arreglos:

// una fila viva, cualquier número de borradas
$table->unique(['email', 'deleted_at']);

O libere el valor al borrar, reescribiéndolo:

public function delete(): bool
{
    $this->email = "{$this->email}#deleted-{$this->id}";
    $this->save();
 
    return parent::delete();
}

La primera conserva el valor original y no puede imponer unicidad entre filas vivas y borradas a la vez. La segunda libera la dirección y pierde el original, lo cual importa si alguna vez necesita saber de quién era esa cuenta. Ninguna de las dos está mal; no elegir ninguna, sí.

Las relaciones vuelven con mal aspecto

Un padre con borrado suave sigue satisfaciendo sus claves foráneas, porque la fila existe. Así que una fila hija no queda huérfana en la base de datos, y un join que no sepa del borrado suave devolverá tan tranquilo el padre borrado.

El scope de Eloquent lo esconde cuando consulta el modelo. Las consultas en crudo, los informes escritos en SQL y las consultas agregadas construidas a mano no, y ahí es donde los números dejan de coincidir entre dos pantallas que enseñan lo mismo.

Peor aún, un withTrashed() en un lado de la relación y no en el otro produce un resultado que no es correcto en ninguna dirección.

Suprimir no es borrar

Una persona ejerce su derecho a que se supriman sus datos. La aplicación llama a delete(). La fila sigue ahí, sigue siendo legible por cualquiera con acceso a la base de datos, sigue en todas las copias de seguridad.

No se ha suprimido nada. La obligación legal no se cumple y el sistema informa de que sí, que es peor que fallar a gritos.

Un camino de supresión tiene que esquivar el mecanismo a propósito:

$user->forceDelete();

O, cuando los registros tienen que sobrevivir por motivos fiscales o de auditoría, anonimice en lugar de eliminar: sustituya los campos personales, conserve la fila, y deje constancia de que ocurrió. Esa es una decisión con un abogado dentro, y el código tiene que implementar la respuesta que dé, no la que venga por defecto.

La tabla solo crece

Nada poda las filas con borrado suave salvo que alguien escriba el trabajo. Años después la tabla es en buena parte filas que nadie puede ver, los índices son más grandes de lo necesario, y todas las consultas arrastran un deleted_at is null que el planificador tiene que tener en cuenta.

Si el trait está en un modelo, debería haber una política de retención y un trabajo que la aplique. "Lo guardamos todo para siempre" es una política válida; no tener ninguna es como una tabla llega a cuarenta millones de filas de las que importan seis.

Cuándo es realmente lo correcto

Donde deshacer es una funcionalidad que los usuarios esperan: un archivo, una papelera, un borrado por error que soporte debería poder revertir.

Donde el registro está referenciado por un histórico que tiene que seguir siendo legible: una factura que nombra a un cliente que después se eliminó debería seguir nombrándolo.

Donde borrar es un flujo de trabajo y no un evento, con un paso de aprobación entre marcar y eliminar.

Eso es un conjunto más estrecho que "todos los modelos", que es donde suele acabar el trait. Por defecto debería haber un borrado real, con el trait añadido donde alguien sepa decir qué significa deshacer para esa tabla, y donde alguien haya contestado la pregunta de la unicidad, la de los informes y la de la retención antes de que se borre la primera fila y no después del primer ticket de soporte.

Las restricciones únicas y los informes son preguntas de esquema. Añadir cualquiera de las dos a una tabla que lleva dos años borrando en blando es trabajo de datos, y lleva más tiempo del que la gente espera. La pregunta del borrado tiene un plazo legal encima, así que va con las demás decisiones que no se pueden deshacer.

Preguntas relacionadas

¿Deberíamos usar borrado suave por defecto?
No: por defecto borre de verdad y añada el trait donde haya un motivo. El motivo suele ser que alguien necesita deshacer, o que el registro está referenciado por un histórico que tiene que seguir siendo legible. Aplicado en todas partes por costumbre, convierte cada tabla en un archivo que nadie poda y cada restricción en una media verdad.
¿Cómo tratamos las columnas únicas?
O incluye la marca de borrado en el índice único, lo que permite una fila viva más cualquier número de borradas, o deja de usar el valor natural como clave única y lo libera al borrar reescribiéndolo. Las dos son decisiones deliberadas; lo que no funciona es un índice único simple en una tabla con borrado suave.
¿Un borrado suave satisface una solicitud de supresión?
No, y esta es la que trae una consecuencia legal y no de ingeniería. El dato sigue ahí y sigue siendo legible, así que no se ha suprimido nada. Una solicitud de supresión necesita una eliminación real o una anonimización real, con el mecanismo de borrado suave esquivado de forma explícita.
¿Y las filas que apuntan a la borrada?
Siguen apuntando a ella, porque la fila sigue ahí y la clave foránea sigue satisfecha. A menudo es lo que quería - una factura antigua sigue nombrando a su cliente - y es una decisión que se toma por relación, no una propiedad del trait.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto