Ir al contenido principal
Laravel, shipping fast.
Capítulo 17 · Gestión de datos

La retención pertenece al modelo

Julian Beaujardin

El primer error que cometen los equipos con la retención es escribir un comando que borra filas viejas y programarlo. Eso funciona hasta que el comando es el único lugar donde vive la regla, y más adelante nadie recuerda qué protege.

// Mal: la regla vive solo en un comando
class PruneApiRequests extends Command
{
    public function handle(): void
    {
        DB::table('api_requests')
            ->where('created_at', '<', now()->subDays(90))
            ->delete();
    }
}

Nada en el modelo dice que tenga un tiempo de vida. Un desarrollador que consulta api_requests no tiene ninguna señal de que las filas viejas deberían haber desaparecido.

Laravel pone la regla en el modelo. El log de peticiones del Capítulo 6 dice por sí mismo cuánto vive:

// app/Models/ApiRequest.php
#[Fillable([
    'consumer_id', 'token_id', 'ip', 'method',
    'path', 'status', 'duration_ms', 'api_version',
])]
class ApiRequest extends Model
{
    use HasFactory, MassPrunable;

    public const UPDATED_AT = null;

    public function consumer(): BelongsTo
    {
        return $this->belongsTo(Consumer::class);
    }

    /** @return Builder<ApiRequest> */
    public function prunable(): Builder
    {
        $days = config('retention.api_requests_days');

        return static::query()
            ->where('created_at', '<', now()->subDays($days));
    }
}
// config/retention.php
return [
    'api_requests_days' => 90,
    'deleted_consumers_days' => 30,
];

Fillable lista lo que escribe el middleware del Capítulo 6, y UPDATED_AT es null porque una fila de log se escribe una vez y no se cambia nunca.

El Capítulo 11 ya programó model:prune para que se ejecute a diario, y no hace falta nada más. model:prune encuentra todos los modelos purgables de la aplicación y borra lo que coincide con su consulta prunable(). No escribes un comando por modelo ni programas uno por modelo. Una línea cubre todos los modelos que se apuntan. (Documentación de Laravel: Eloquent: Getting Started › Pruning Models.)

Hay dos traits. MassPrunable borra con una sola consulta por lote, que es lo correcto para una tabla de log con millones de filas. Prunable carga cada modelo y dispara sus eventos, que es lo correcto cuando borrar una fila tiene que limpiar algo más.

La regla y el modelo viven en un solo archivo. Quien lea ApiRequest ve el trait y sabe que esta tabla no es permanente.

El Capítulo 5 dejó aquí una promesa. Un consumidor eliminado se borra de forma lógica para que un error pueda deshacerse, y una fila borrada lógicamente sigue siendo una fila que conservas. El mismo trait le pone un final:

// app/Models/Consumer.php
public function prunable(): Builder
{
    $days = config('retention.deleted_consumers_days');

    return static::onlyTrashed()
        ->where('deleted_at', '<', now()->subDays($days));
}

Consumer usa Prunable, no MassPrunable, y purgar un modelo borrado lógicamente lo borra de forma definitiva. Treinta días después de eliminar un consumidor, la fila ya no está.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.