La retencion como politica, no como tarea programada
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 sitio donde vive la regla, y seis meses después nadie recuerda que existe ni qué protege.
// Bad: retention logic lives only in a scheduled command,
// invisible to anything that isn't reading routes/console.php
class PruneExpiredTokens extends Command
{
public function handle(): void
{
DB::table('site_tokens')
->where('expires_at', '<', now())
->delete();
}
}
Nada en Token dice que este modelo tenga una vida útil. Quien consulta la tabla no tiene ninguna señal de que las filas expiradas deberían haber desaparecido; la regla vive en un fichero de comando que casi nadie abre.
El patrón real de la flota pone la regla en el modelo, usando el contrato Prunable de Eloquent:
// app/Models/Token.php
class Token extends Model
{
use HasFactory;
use HasUuids;
use Prunable;
protected $connection = 'sites';
protected $table = 'site_tokens';
protected function casts(): array
{
return [
'metadata' => 'array',
'expires_at' => 'datetime',
];
}
/**
* @return Builder<Token>
*/
public function prunable()
{
return static::where('expires_at', '<', now());
}
}
// routes/console.php
Schedule::command('model:prune')->daily();
Ese es todo el mecanismo. model:prune es un comando integrado de Laravel que encuentra cada modelo Prunable de la aplicación y llama a su ámbito prunable(). No escribes un comando por modelo, ni programas nada por modelo. Una línea programada cubre la retención de todos los modelos que se apunten.
¿Por qué importa esto más de lo que parece? Porque la regla y el modelo viven en el mismo fichero. Quien lea Token ve use Prunable; y sabe de inmediato que esta tabla no es permanente. La retención que vive en el modelo sobrevive a que su autor se marche.
Migraciones sobre una tabla viva
Una migración que funciona perfectamente en tu SQLite local puede bloquear una tabla de producción durante minutos. La diferencia es el número de filas, y más aún cuando no sólo cambias un esquema sino que reescribes datos bajo tráfico en vivo.
// Bad: DB::table()->chunk() paginates by numeric offset.
// On a UUID-keyed table, concurrent writes shift the offset
// window mid-run and rows silently get skipped.
DB::table('sites')
->select('id', 'category', 'metadata')
->chunk(500, function ($rows): void {
foreach ($rows as $row) {
// ... backfill logic
}
});
// Good: cursor() streams every row in a single query, so no
// row can be missed no matter what else is writing to the
// table while this runs. Idempotent: a row already matching
// the target shape is skipped, not re-written.
DB::table('sites')
->select('id', 'category', 'metadata')
->cursor()
->each(function ($row): void {
$metadata = is_string($row->metadata)
? (json_decode($row->metadata, true) ?: [])
: ((array) ($row->metadata ?? []));
if (($metadata['category'] ?? null) === $row->category) {
return;
}
$metadata['category'] = $row->category;
DB::table('sites')
->where('id', $row->id)
->update(['metadata' => json_encode($metadata, JSON_THROW_ON_ERROR)]);
});
chunk() pagina una tabla con LIMIT/OFFSET. Bien para una tabla estática, pero en una que recibe escrituras las filas se desplazan entre páginas y la paginación por desplazamiento las salta o las repite en silencio. cursor() abre una única consulta en streaming, así que la deriva de paginación no puede ocurrir por muchas escrituras que aterricen durante la ejecución. La idempotencia es la otra mitad del arreglo: la actualización sólo se dispara cuando el JSON está realmente desincronizado, así que ejecutar la migración dos veces, o reanudarla después de que un despliegue la interrumpa, produce el mismo estado final.
Los cambios de esquema aditivos reciben la misma disciplina en miniatura: una migración que añade una columna comprueba Schema::hasColumn() antes de añadir nada, así que el mismo fichero puede correr de forma segura una segunda vez contra un entorno donde una ejecución parcial anterior ya llegó hasta ahí.
Una migración que puedes ejecutar dos veces sin consecuencias es una migración en la que puedes confiar de verdad en producción. Una que sólo puedes ejecutar una vez, con éxito, a la primera, es una migración con la que estás apostando.