Una migración que se ejecuta al instante en tu portátil puede detener una tabla de producción durante minutos. La diferencia es el número de filas y el tráfico.
Añade, no cambies. Una columna nueva que admite null es segura. Renombrar o eliminar una rompe el código que sigue en ejecución durante el despliegue. El Capítulo 20 vuelve sobre el porqué.
Haz que sea seguro ejecutarla dos veces. Un despliegue que falla a medias deja las migraciones a medio aplicar. Protege las que podrían volver a ejecutarse:
if (! Schema::hasColumn('consumers', 'rate_limit')) {
Schema::table('consumers', function (Blueprint $table) {
$table->unsignedInteger('rate_limit')->nullable();
});
}
Nunca reescribas datos dentro de una migración de esquema. Cambiar la estructura es un despliegue. Rellenarla es un trabajo aparte y reiniciable, y es la sección siguiente.
Cuando sí recorres una tabla que está recibiendo escrituras, importa cómo la paginas:
// Mal: paginación por offset. Las filas se mueven entre
// páginas con otras escrituras; unas se saltan o se repiten.
Consumer::query()->chunk(500, $callback);
// Bien: páginas por clave primaria. Una fila no se mueve.
Consumer::query()->chunkById(500, $callback);
chunk() usa LIMIT y OFFSET. En cuanto el bucle actualiza la columna por la que filtra, u otra petición inserta una fila, las páginas dejan de cuadrar. chunkById() recuerda el último ID que vio y pide las filas que vienen después. Úsalo, o su gemelo perezoso lazyById(), siempre que modifiques filas mientras iteras. (Documentación de Laravel: Eloquent: Getting Started › Chunking Results.)
Backfills que puedes reiniciar
Un backfill cambia filas existentes para que cumplan una expectativa nueva. Se entrega como un comando de Artisan propio, porque necesita un ensayo en seco y la capacidad de detenerse y reanudarse sin dejar los datos migrados a medias.
// app/Console/Commands/BackfillConsumerSettings.php
#[Signature('consumers:backfill-settings {--dry-run}')]
#[Description('Reshape consumer settings in place')]
class BackfillConsumerSettings extends Command
{
public function handle(): int
{
Consumer::query()->chunkById(
100,
$this->backfill(...),
);
return self::SUCCESS;
}
// ... siguen backfill() y reshape()
}
private function backfill(Collection $consumers): void
{
foreach ($consumers as $consumer) {
$new = $this->reshape($consumer->settings);
if ($new === $consumer->settings) {
continue;
}
$this->line("Consumer {$consumer->id} changes");
if (! $this->option('dry-run')) {
$consumer->forceFill(['settings' => $new])
->save();
}
}
}
reshape() es la función pura que convierte la forma vieja en la nueva.
Dos detalles de ese bucle son fáciles de hacer mal. settings no es asignable en bloque, así que update() descartaría el cambio. En producción lo haría sin decir palabra, y el comando imprimiría «changes» sin cambiar nada. El shouldBeStrict() del Capítulo 9 convierte el mismo error en una excepción en todos los demás entornos, que es como te enteras primero. forceFill() es la manera honesta de escribir un atributo protegido desde código que controlas. Y cuando cambia la forma de settings, el providerToken() del Capítulo 4 tiene que cambiar en el mismo despliegue. Es el único lector de esa clave, y por eso es un cambio de una línea, y lanza una excepción cuando la clave falta, y por eso una equivocación aquí produce un error y no un cambio de cuenta.
Tres cosas hacen que esto sea seguro de ejecutar contra producción, y ninguna es opcional.
--dry-runmuestra lo que cambiaría sin escribir, para que puedas leer la salida antes de fiarte.chunkById()recorre la tabla en páginas ordenadas, así que una tabla con un millón de filas se comporta como una con cien.- Saltar lo que no cambia. Comparar la forma nueva con la vieja antes de escribir significa que una segunda ejecución no les hace nada a las filas ya migradas. Mata el proceso a medias, ejecútalo otra vez mañana, y retomará el trabajo donde se detuvo.
Un backfill que solo puede ejecutarse una vez es uno que te dará miedo ejecutar. Uno que puedes interrumpir y volver a ejecutar puede programarse en horario de oficina.