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.
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.
