php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

Guía Práctica Completa: Diseño de Migraciones, Seeders y Factories en Laravel — Cambios Seguros de BD, Datos de Prueba, Datos Iniciales y Rollback

Lo que aprenderás en este artículo

  • Cómo diseñar migraciones de Laravel de forma segura
  • Patrones prácticos para reducir accidentes al añadir, modificar o eliminar columnas
  • Cómo crear datos iniciales y datos de prueba con Seeders y Factories
  • Qué tener en cuenta al desplegar cambios de base de datos en producción
  • Operación considerando rollback, backups, migración de datos y cero downtime
  • Diseño de datos claro conectado con paneles de administración y formularios
  • Puntos de datos iniciales, textos y estados considerando accesibilidad

Público objetivo

  • Ingenieros Laravel de nivel inicial a intermedio: personas que ya escriben migraciones, pero sienten inseguridad al cambiar BD en producción
  • Tech leads: quienes quieren ordenar reglas y criterios de revisión de cambios de BD dentro del equipo
  • QA / responsables de mantenimiento: quienes sienten que los datos de prueba o iniciales son inestables y dificultan la verificación
  • Diseñadores / CS / responsables de accesibilidad: quienes quieren ordenar nombres de estado, textos iniciales y datos usados en pantalla de forma comprensible

Nivel de accesibilidad: ★★★★☆
Aunque el tema principal es diseño de BD, la forma de almacenar datos está directamente conectada con la claridad de la UI. Por ejemplo, si los valores de estado, nombres de roles, plantillas de notificación o opciones de formularios no están ordenados, será difícil explicarlos en pantalla. En este artículo también tendremos en cuenta etiquetas legibles, datos iniciales fáciles de traducir y visualización de estados que no dependa solo del color.


1. Introducción: Las Migraciones Son el “Historial” de la Base de Datos

Las migraciones de Laravel son un mecanismo para gestionar la estructura de la base de datos como código. Permiten dejar como archivos cambios como crear tablas, añadir columnas, crear índices o definir claves foráneas. No son solo una función cómoda; cumplen un papel muy importante en desarrollo en equipo y operación en producción.

Cuando los cambios de BD se hacen manualmente, queda ambiguo “quién cambió qué, cuándo y cómo”. Surgen problemas como que en local funciona pero en staging falta una columna, que solo en producción falta un índice o que los datos iniciales del entorno de pruebas son distintos. Al usar migraciones, el historial de cambios de BD se gestiona con Git y todos los miembros del equipo pueden reproducir el mismo procedimiento.

Sin embargo, aunque las migraciones son útiles, hay que tratarlas con cuidado en producción. En especial, eliminar columnas, cambiar tipos, actualizar grandes volúmenes de datos, añadir claves foráneas o crear índices sobre tablas enormes puede causar paradas de servicio o locks largos. Este artículo organiza las migraciones de Laravel no solo como “forma de escribirlas”, sino como una forma de operarlas con seguridad.


2. Migración Básica: Empezar Creando una Tabla

Primero veamos la creación básica de una tabla. Por ejemplo, para crear una tabla de artículos, se ejecuta:

php artisan make:migration create_posts_table

En el archivo generado se escribe la estructura de la tabla.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('title', 120);
            $table->string('slug')->unique();
            $table->text('body');
            $table->string('status', 30)->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->index(['status', 'published_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Lo importante en este ejemplo es que los nombres de columnas y estados sean fáciles de usar en pantalla.
En status se asumen valores como draft, published y archived. En pantalla se pueden traducir y mostrar como “Borrador”, “Publicado” o “Archivado”. Resulta práctico guardar en la BD valores fáciles de manejar por la máquina y convertirlos en etiquetas legibles para personas en la UI.


3. Reglas de Nombres: Nombres Comprensibles Incluso al Leerlos Después

Los nombres de archivos de migración se leerán más adelante como historial.
Por eso conviene poner nombres que indiquen claramente qué hace el archivo.

Buenos ejemplos:

create_posts_table
add_status_to_orders_table
add_published_at_to_posts_table
create_notification_preferences_table

Ejemplos poco claros:

update_posts
fix_table
change_columns
add_fields

En desarrollo en equipo, es importante que solo mirando el nombre de la migración se entienda el resumen del cambio. En revisiones de código, un nombre concreto también facilita revisar el contenido.

También conviene evitar nombres ambiguos en tablas y columnas, porque se conectan con la UI y el negocio.
Por ejemplo, es más mantenible usar is_active en lugar de flag, notification_type en lugar de type, o display_order en lugar de value.


4. Añadir Columnas: Pensar con Cuidado en nullable y default

Cuando se añade una columna a una tabla existente, hay que asumir que ya existen datos en producción.
Por ejemplo, añadimos timezone a la tabla users.

php artisan make:migration add_timezone_to_users_table
return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('timezone', 64)->nullable()->after('email');
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('timezone');
        });
    }
};

Al principio, usar nullable() facilita añadir la columna con seguridad aunque existan datos previos. Después se añade un fallback en la aplicación si no está configurada.

$timezone = $user->timezone ?? config('app.timezone');

Cuando se confirme que todos los usuarios tienen valor, se puede considerar quitar nullable si es necesario.
Si se añade desde el inicio como not null, puede fallar porque los datos existentes no tienen valor. Los cambios de BD son más seguros si se separan en “añadir”, “migrar” y “reforzar restricciones”.


5. Eliminar Columnas: Avanzar con Mucho Cuidado en Producción

Eliminar columnas es una de las operaciones más peligrosas en cambios de BD. El motivo es que, una vez eliminada, cualquier código antiguo que la consulte causará error. Si se busca cero downtime, no se elimina de golpe; se avanza por etapas.

Flujo seguro:

  1. Añadir la nueva columna
  2. Adaptar la aplicación a la nueva columna
  3. Dejar de usar la columna antigua
  4. Confirmar mediante logs o monitoreo que ya no se referencia
  5. Eliminar la columna antigua en otro release

Es decir, eliminar una columna es “la limpieza final”.
Muchas veces es más seguro no incluir la migración de eliminación en el mismo release que el cambio funcional.

Schema::table('posts', function (Blueprint $table) {
    $table->dropColumn('old_summary');
});

En este tipo de operación, durante la revisión se debe confirmar si la aplicación realmente ya no la consulta, si existe backup y si no habrá problemas al hacer rollback.


6. Diseño de Índices: Sostener la Velocidad de Búsquedas y Listados

Muchas lentitudes de BD vienen de índices ausentes o inadecuados. En las migraciones de Laravel también se pueden gestionar índices como código.

$table->index('status');
$table->index(['status', 'published_at']);
$table->unique('slug');

El diseño debe considerar las condiciones que se usan con frecuencia en listados.
Por ejemplo, para mostrar artículos publicados ordenados por fecha de publicación, suele aparecer esta condición:

WHERE status = 'published'
ORDER BY published_at DESC

En ese caso, se puede considerar un índice compuesto sobre status y published_at.

$table->index(['status', 'published_at']);

Pero más índices no siempre significa mejor. También aumentan la carga de escritura y el uso de almacenamiento.
En la práctica, conviene pensar en este orden:

  • Qué condiciones de búsqueda se usan a menudo
  • Qué órdenes se usan a menudo
  • Si los listados tienen muchos registros
  • Si el plan de ejecución de la BD usa el índice
  • Si no han aumentado demasiado los índices innecesarios

Los índices afectan tanto la velocidad percibida en pantalla como el costo operativo.


7. Claves Foráneas: Proteger la Integridad También desde la BD

Laravel permite escribir claves foráneas de forma concisa.

$table->foreignId('user_id')
    ->constrained()
    ->cascadeOnDelete();

Esto hace que user_id referencie users.id y que los datos relacionados se eliminen al borrar el usuario.
Sin embargo, aunque cascadeOnDelete() es práctico, hay que usarlo con cuidado. Se debe pensar según el negocio si realmente está bien que publicaciones u órdenes desaparezcan cuando se elimina un usuario.

Por ejemplo, quizá se quiera conservar el historial de órdenes aunque el usuario sea eliminado. En ese caso, se cambia el comportamiento de borrado o se diseña el usuario como desactivado, no eliminado físicamente.

$table->foreignId('user_id')
    ->nullable()
    ->constrained()
    ->nullOnDelete();

Las restricciones de BD son una última línea de defensa distinta de la validación de aplicación.
Aunque haya bugs o acciones inesperadas en la aplicación, la BD puede proteger la integridad.


8. Diseño de Valores de Estado: Prepararlos para una UI Comprensible

Muchas aplicaciones usan una columna status.
Por ejemplo, una orden puede tener estados como:

  • pending
  • paid
  • shipped
  • cancelled

En la BD se guardan valores mecánicos en inglés, y en pantalla se traducen.

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return match ($this) {
            self::Pending => 'Pendiente de pago',
            self::Paid => 'Pagado',
            self::Shipped => 'Enviado',
            self::Cancelled => 'Cancelado',
        };
    }
}

Laravel permite usar Enum con casts.

protected function casts(): array
{
    return [
        'status' => OrderStatus::class,
    ];
}

En la visualización de estados, no se debe transmitir el significado solo con color.
Por ejemplo, además de una insignia roja, se debe mostrar siempre el texto “Cancelado”. Esto no solo mejora la accesibilidad, sino que también reduce errores de interpretación en la UI operativa.


9. Seeder: Reproducir Datos Iniciales con Código

Los Seeders son el mecanismo para insertar datos iniciales.
Se usan para roles, permisos, categorías, valores de configuración o datos de desarrollo.

php artisan make:seeder RoleSeeder
namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Role;

class RoleSeeder extends Seeder
{
    public function run(): void
    {
        foreach (['admin', 'editor', 'viewer'] as $role) {
            Role::firstOrCreate(['name' => $role]);
        }
    }
}

Se llaman desde DatabaseSeeder.

public function run(): void
{
    $this->call([
        RoleSeeder::class,
    ]);
}

Lo importante en Seeders es que no se rompan aunque se ejecuten varias veces.
Si solo se usa create(), puede haber duplicados al reejecutar. Para datos iniciales, firstOrCreate() o updateOrCreate() son más seguros.


10. Separar Seeders de Producción y Desarrollo

Hay que distinguir entre datos dummy de desarrollo y datos iniciales necesarios en producción.

Ejemplos de datos iniciales necesarios en producción:

  • roles
  • permisos
  • prefecturas / provincias / regiones
  • plantillas de notificación
  • configuración del sistema
  • categorías fijas

Ejemplos de datos de desarrollo:

  • usuarios de prueba
  • artículos dummy
  • órdenes de muestra
  • grandes volúmenes generados con Factory

Ejecutar Seeders de desarrollo en producción es peligroso.
Conviene separar nombres y forma de llamada, dejando claro qué se ejecuta por entorno.

public function run(): void
{
    $this->call([
        RoleSeeder::class,
        PermissionSeeder::class,
    ]);

    if ($this->app->environment('local')) {
        $this->call([
            DemoUserSeeder::class,
            DemoPostSeeder::class,
        ]);
    }
}

11. Factory: Crear Datos de Prueba Legibles

Factory es el mecanismo para crear datos en pruebas y desarrollo.

php artisan make:factory PostFactory --model=Post
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => User::factory(),
            'title' => fake()->sentence(4),
            'slug' => fake()->unique()->slug(),
            'body' => fake()->paragraphs(3, true),
            'status' => 'draft',
            'published_at' => null,
        ];
    }

    public function published(): static
    {
        return $this->state(fn () => [
            'status' => 'published',
            'published_at' => now()->subDay(),
        ]);
    }
}

Uso:

Post::factory()->published()->create();

Al preparar métodos de estado en Factory, las pruebas se vuelven más legibles.

public function test_published_posts_are_visible()
{
    Post::factory()->published()->create(['title' => 'Artículo publicado']);
    Post::factory()->create(['title' => 'Artículo en borrador']);

    $response = $this->get('/posts');

    $response->assertSee('Artículo publicado')
        ->assertDontSee('Artículo en borrador');
}

Los datos de prueba no deben limitarse a aumentar cantidad; deben crear “estados con intención clara”.


12. Factory y Relaciones: Crear Datos Cercanos a la Realidad

También se pueden crear datos de prueba con relaciones usando Factory.

$user = User::factory()
    ->has(Post::factory()->count(3)->published())
    ->create();

También se pueden crear relaciones muchos a muchos, como publicaciones con etiquetas.

$post = Post::factory()
    ->published()
    ->hasAttached(Tag::factory()->count(2))
    ->create();

Sin embargo, si se crean datos demasiado grandes sin necesidad, las pruebas se vuelven lentas.
Lo importante es crear una estructura cercana a la realidad, pero pequeña.
En lugar de pensar “necesito 100 registros”, conviene pensar “necesito 2 registros que cumplan esta condición”.


13. Migración de Datos: No Actualizar Demasiado Dentro de Migraciones

A veces, al añadir una columna con migración, se quiere actualizar al mismo tiempo los datos existentes.
En tablas pequeñas no suele ser problema, pero si se actualizan muchos datos de golpe dentro de una migración, el despliegue de producción puede quedar bloqueado mucho tiempo.

Ejemplo peligroso:

public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('timezone')->nullable();
    });

    User::query()->update(['timezone' => 'Asia/Tokyo']);
}

Para datos grandes, es más seguro separarlo así:

  1. Añadir la columna
  2. Añadir fallback en la aplicación cuando no haya valor
  3. Rellenar datos poco a poco con batch o job
  4. Confirmar que todos los registros tienen valor
  5. Quitar nullable si hace falta

No conviene meter demasiada actualización masiva de datos en la misma migración de estructura.


14. Rollback: Distinguir Cambios Reversibles y no Reversibles

Las migraciones de Laravel tienen down(). Pero en producción hay cambios que no pueden revertirse simplemente.

Cambios fáciles de revertir:

  • creación de una tabla nueva
  • añadir columna nullable
  • añadir índice

Cambios difíciles de revertir:

  • eliminación de columna
  • cambio de tipo
  • transformación de datos
  • sobrescritura de valores existentes

Especialmente los cambios que pierden datos pueden no recuperarse aunque exista down().
Por ejemplo, si se elimina una columna y luego el rollback la recrea, los datos originales no vuelven.

Por eso, no hay que depender demasiado del rollback y conviene preparar:

  • backup antes del despliegue
  • ejecución previa en staging
  • cambios pequeños
  • procedimiento de release compatible
  • procedimiento manual de recuperación si hace falta

down() es importante, pero “si realmente se puede volver atrás” debe pensarse como otro tema.


15. Cambios de BD en Producción: Pensar en Compatibilidad

En un despliegue con cambios de BD, código antiguo y código nuevo pueden coexistir temporalmente. Es especialmente importante en cero downtime o en configuraciones con múltiples servidores.

El principio seguro es:

  • primero añadir
  • dejar que código antiguo y nuevo funcionen
  • cambiar al código nuevo
  • eliminar la estructura antigua más adelante

Por ejemplo, si se divide name en first_name y last_name, eliminar name de inmediato es peligroso.
Primero se añaden las nuevas columnas, la aplicación maneja ambas, se migran datos y solo cuando el cambio está completo se elimina la columna antigua.

Así, los cambios de BD se tratan con más cuidado que los cambios de código.
La estructura de tablas es la base de toda la aplicación, por lo que es mejor evitar eliminaciones o modificaciones bruscas.


16. Accesibilidad y Diseño de BD: Ordenar Estados, Etiquetas y Opciones

El diseño de BD también afecta la claridad de las pantallas.
Por ejemplo, si los valores de estado son ambiguos, la UI también será ambigua.

Ejemplo a evitar:

status = 1
status = 2
status = 3

Estos estados solo numéricos son difíciles de leer después.
Si se usan, se debe aclarar el significado con Enum o constantes.

Ejemplo preferible:

draft
published
archived

En pantalla se puede mostrar:

  • Borrador
  • Publicado
  • Archivado

Aunque se añada color, existe una etiqueta textual, por lo que no se depende de la percepción del color.
Además, si las opciones y datos iniciales están estructurados para traducirse fácilmente, será más sencillo soportar múltiples idiomas.


17. Pruebas: Proteger la Calidad de Migraciones, Seeders y Factories

En Laravel, las pruebas pueden construir la BD usando migraciones.
Con RefreshDatabase, es fácil preparar el estado de BD para cada prueba.

use Illuminate\Foundation\Testing\RefreshDatabase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    public function test_published_post_is_visible(): void
    {
        Post::factory()->published()->create([
            'title' => 'Artículo publicado',
        ]);

        $response = $this->get('/posts');

        $response->assertSee('Artículo publicado');
    }
}

También son importantes las pruebas de Seeders.
Especialmente datos iniciales como roles o permisos pueden ser necesarios para que la aplicación funcione.

public function test_roles_are_seeded(): void
{
    $this->seed(RoleSeeder::class);

    $this->assertDatabaseHas('roles', [
        'name' => 'admin',
    ]);
}

Factory afecta directamente la legibilidad de las pruebas.
No hace falta aumentar demasiados métodos de estado, pero conviene agrupar en Factory los estados usados con frecuencia.


18. Errores Frecuentes y Cómo Evitarlos

18.1 Añadir de Golpe una Columna not null en Producción

Si ya existen datos, puede fallar.
Primero se añade como nullable y, tras migrar datos, se refuerza la restricción.

18.2 Eliminar Columnas en el Mismo Release que el Cambio Funcional

Si el código antiguo las referencia, habrá errores.
La eliminación debe hacerse más adelante, tras confirmar que no hay referencias.

18.3 Seeders que Crean Datos Duplicados

Si solo se usa create(), al reejecutar habrá duplicados.
Usa firstOrCreate() o updateOrCreate().

18.4 Factories Demasiado Alejadas de la Realidad

Si los datos solo funcionan en pruebas y no reflejan coherencia mínima, se pueden pasar por alto bugs.
Haz Factories con una integridad mínima.

18.5 Valores de Estado Solo Numéricos y Sin Significado

Dificultan la pantalla y el mantenimiento.
Aclara el significado con Enum o valores de texto.

18.6 Meter Actualizaciones Masivas de Datos en Migraciones

El despliegue puede bloquearse mucho tiempo.
Migra gradualmente con batches o jobs.


19. Checklist para Distribuir

Diseño de Migraciones

  • [ ] El nombre del archivo expresa el contenido del cambio
  • [ ] Los nombres de columnas tienen significado
  • [ ] Se piensa en nullable / default asumiendo datos existentes
  • [ ] La eliminación de columnas se separa con cuidado
  • [ ] Los índices coinciden con las condiciones de búsqueda
  • [ ] El comportamiento de borrado de claves foráneas coincide con el negocio

Operación en Producción

  • [ ] Se confirmó backup antes del cambio de BD
  • [ ] No se metieron actualizaciones masivas de datos en la migración
  • [ ] Se respeta el orden añadir → migrar → eliminar
  • [ ] Se confirmó si el rollback realmente puede restaurar

Seeder

  • [ ] Se separan datos iniciales de producción y datos de desarrollo
  • [ ] Se usa firstOrCreate() / updateOrCreate() para resistir reejecuciones
  • [ ] Existen Seeders para datos obligatorios como roles y permisos

Factory

  • [ ] Existen métodos de estado frecuentes
  • [ ] Se pueden crear datos con relaciones de forma natural
  • [ ] Se generan solo los registros mínimos necesarios para la prueba

Accesibilidad / Conexión con UI

  • [ ] Los valores de estado pueden mostrarse claramente en pantalla
  • [ ] Hay etiquetas de estado que no dependen solo del color
  • [ ] Las opciones y datos iniciales son fáciles de traducir
  • [ ] El panel de administración puede convertirlos en etiquetas con significado

20. Conclusión

Las migraciones de Laravel no solo gestionan la estructura de BD; son un mecanismo clave para estabilizar el desarrollo en equipo y la operación en producción. Al gestionar como código la creación de tablas, adición de columnas, índices y claves foráneas, se reducen diferencias entre entornos y se aclara el historial de cambios.

Por otro lado, los cambios de BD en producción deben tratarse con cuidado. En especial, añadir not null, eliminar columnas, cambiar tipos y migrar grandes volúmenes de datos es más seguro si se divide en etapas como añadir → migrar → eliminar. Al ordenar Seeders y Factories, también se vuelven más reproducibles los datos iniciales y los datos de prueba.

El diseño de BD no es solo un asunto backend. Valores de estado, nombres de roles, opciones y textos iniciales también se conectan con la claridad de pantalla y la accesibilidad. Al organizar los datos para que sean fáciles de manejar tanto por máquinas como por personas, mejora la calidad de toda la aplicación Laravel. Como primer paso, revisa migraciones y Factories existentes y ve ajustándolas poco a poco hacia nombres claros, cambios seguros y datos iniciales resistentes a reejecución.


Enlaces de Referencia

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)