¿Qué son las migraciones y como usarlas en Laravel para crear tablas en Base de Datos?

Ya sabemos cómo trabajar con los controladores de manera básica, pero, no hemos podido enlazar con los modelos ya que, el modelo es la entrada a la base de datos, específicamente una tabla; es decir, en nuestro modelo llamado Post (en singular) necesitamos una tabla llamada posts (en plural), lo de colocar los nombres en plural en singular son simplemente buenas prácticas.

Inicialmente en el libro configuramos la base de datos para nuestro proyecto y ahora, es momento de usarla, nosotros no nos conectamos directamente a las tablas de la  base de datos, la creación de tablas de manera directa no es recomendada al trabajar con el framework y prácticamente cualquier framework web moderno del lado del servidor en PHP, Node, Python y vale contar, siguen este mismo principio de evitar que los desarrolladores interactúen con la base de datos, y esto es genial por dos puntos:

  1. Nos permite trabajar con la base de datos como un proceso más.
  2. Nos permite un mejor desempeño al momento de trabajar en equipo.

Antes de explicar los dos puntos en detalle, vamos a mencionar cuál es la herramienta empleada para lograr tal hazaña... Se conocen como migraciones, que no son más que un sistema de control para las tablas.

En definitiva, para empezar a trabajar con los modelos, necesitamos las tablas en la base de datos, y para trabajar con las tablas, necesitamos las migraciones; comencemos por las migraciones.

¿Por qué son tan importantes?

  1. Abstracción del SQL: Laravel soporta múltiples motores como MySQL, MariaDB, PostgreSQL, SQLite y SQL Server. Las migraciones son "transparentes": tú escribes el código en PHP y Laravel lo traduce al SQL específico del motor que estés usando.
  2. Control de Versiones: La base de datos forma parte del proyecto. Si pierdes la base de datos, no quieres hacer "ingeniería inversa" revisando controladores para reconstruirla. Con las migraciones, cualquier desarrollador puede recrear toda la estructura con un solo comando.
  3. Documentación Viva: Al leer una migración, entiendes perfectamente qué campos tiene una tabla (si es un varchar, si es nullable, si es único, etc.).

Migraciones

Las migraciones son un sistema que nos permiten generar tablas en base a archivos PHPs (clases) una clase por cada tabla en nuestra base de datos que nosotros definimos; mediante las migraciones (entiéndase clases) podemos, o crear nuevas tablas, o modificar las existentes; una migración luce como la siguiente:

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};

Explicación del código anterior

Lo primero que podemos notar es que, tenemos un new class y heredamos de una clase Migration, que es interna al framework y permite usar esta clase como migración; en cuando al new class, es una característica de PHP que nos permite crear clases anónimas que es particularmente útil para evitar conflictos entre clases; no necesitamos un nombre para esta clase ya que, no la vamos a referenciar en ninguna parte, estas clases son empleadas de manera interna para manejar las migraciones y más nada.

Las migraciones constan de dos partes, por una parte tenemos el método de up(), en donde aplicamos las operaciones que queremos realizar sobre la base de datos:

  1. Crear una tabla.
  2. Modificar una tabla existente, agregando/removiendo columnas y/o índices.

También existe un método llamada down(), la cual es usada para revertir los cambios realizados en el método up(); es decir, si en el método de up() creamos una tabla, en down() la removemos, si en up() creamos una o varias columnas, en down() removemos esa misma columna o columnas. Esto se debe a que el sistema de migraciones nos permite tanto ejecutarlas, como revertir las operaciones anteriores.

Puedes ver que en el método de up(), definimos el esquema, el cual, dado el nombre de la tabla, podemos agregar/remover columnas; por defecto, ya Laravel nos define una estructura básica, en la cual:

  • Tenemos una columna para la PK $table->id()
  • Tenemos las columnas para las fechas de creación y actualización $table->timestamps()

Algunas operaciones para crear columnas son:

  1. id() para generar una columna llamada id, autoincremental, bigInteger y con la relación de clave primaria o primary key.
  2. string() para indicar una columna de tipo varchar; recibe dos atributos, el nombre y la longitud.
  3. timestamps() crea dos columnas de tipo timestamps, una para la fecha de creación del registro, y la otra para la fecha de actualización.
  4. foreignId() este método nos permite crear la clave de tipo foránea; recibe un parámetro de manera obligatoria, con la cual indicamos el nombre del campo; este método puede recibir más parámetros para indicar las relaciones pertinentes, pero, si respetamos las convenciones de nombres de Laravel, no sería necesario.
  5. text() permite crear una columna de tipo text, recibe un parámetro, con el cual indicamos el nombre de la columna.
  6. enum() permite crear una columna de tipo enum (seleccionable) y recibe dos parámetros, el nombre de la columna, y los valores seleccionables presentados mediante un array.
  7. onDelete() indica el comportamiento que van a tener los registros al ser eliminados en una relación foránea.

A las cuales, puedes personalizar mediante modificadores; algunos de los más comunes:

  1. unsigned() para indicar que va a ser de tipo UNSIGNED.
  2. nullable() para indicar que pueden ser nulos.
  3. constrained() para crear el constrained/referencia a la tabla, usualmente se usa en conjunto con la columna de tipo foreignId().

Por aqui tienes acceso a la documentación oficial, para más detalles:

https://laravel.com/docs/master/migrations#creating-columns

Al final, las migraciones no son más que archivos que definen una estructura, que con un comando, reflejamos su estructura en la base de datos en una tabla; por lo tanto, una (o varias) migración, define una tabla en la base de datos.

Por ejemplo, una migración luce como la siguiente:

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('title', 500);
            $table->string('slug', 500);
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};

En la cual, indicamos una columna para el id, título, slug y fechas.

Crear una migración

Para crear una migración, tenemos el siguiente comando:

$ php ​​artisan make:migration <NombreMigracion>

Al cual, le podemos definir una estructura mediante un conjunto de funciones como explicamos anteriormente.

Las migraciones se ubican en la carpeta de:

  • database/migrations

Y es importante señalar patrones como:

create_<tabla>_table

O para modificar una tabla existente:

add_<operacion>_<tabla>_table

Ya que, si respetas estos patrones al momento de crear la migración; Laravel auto completará parte del código, como el nombre de la tabla y las columnas de id y fechas; es decir, si no indicamos estos patrones:

// *** php artisan make:migration tablita
<?php
return new class extends Migration
{
    public function up(): void
    {
        //
    }
    public function down(): void
    {
        //
    }
};

Y si indicamos un patrón:

// *** php artisan make:migration createCategoriesTable
<?php
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('categories', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }
    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};

Dentro del archivo encontrarás los métodos que mencionamos antes:

  • up(): Donde definiremos los campos de nuestra tabla.
  • down(): Donde definiremos cómo deshacer esos cambios.

Ejecutar la migración

Para tener nuestras tablas, necesitamos ejecutar nuestras migraciones, así que, vamos a empezar a trabajar con las mismas.

Para ejecutar las migraciones:

$ php ​​artisan migrate

Caso práctico: Creación de la tabla de Posts y Categorías

Vamos a crear ahora, una migración para las categorías:

$ php artisan make:migration createCategoriesTable
Con la siguiente estructura:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('title', 500);
            $table->string('slug', 500);
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};

Y para los posts:

$ php ​​artisan make:migration createPostsTable

Con la siguiente estructura:

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title', 500);
            $table->string('slug', 500);
            $table->string('description',150)->nullable();
            $table->text('content')->nullable();
            $table->string('image')->nullable();
            $table->enum('posted', ['yes', 'not'])->default('not');
            $table->timestamps();
            $table->foreignId('category_id')->constrained()
                ->onDelete('cascade');
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Explicación del código anterior

  • description: Un extracto corto (usamos string).
  • content: El cuerpo del post. Usamos el tipo text() ya que un VARCHAR se quedaría pequeño para artículos largos.
  • image: Usamos string para la ruta de la imagen y le añadimos el atributo nullable(), indicando que este campo puede quedar vacío.
  • posted: Usamos un tipo enum con los valores ['yes', 'no']. Además, usamos el decorador default('no') para que, si no especificamos nada, el post nazca como "no publicado".

En la migración de los posts, creamos una columna llamada category_id, que como puedes suponer, es empleada para manejar la relación de tipo foránea con las categorías; ya Laravel sabe de manera automática como relacionar la columna de category_id con una tabla llamada categorías (en plural), ya que, estamos empleando la convención de nombres de Laravel; pero, en caso de que hiciera falta, también puedes indicar la tabla:

$table->foreignId('category_id')->constrained('categories');

Por lo demás, definimos una serie de columnas de tipo texto y una con un enum para manejar el estado.

Importante notar que, el orden en el cual definimos las migraciones es fundamental, ya que, en el orden en el cual se encuentran:

Migraciones iniciales

Es el orden en el cual se van a ejecutar. Al ejecutar las migraciones:

$ php ​​artisan migrate

Veremos en nuestra base de datos las tablas creadas.

Tips para tus migraciones

Va a ser el orden en el cual se encuentran definidas; por lo tanto, es común que las migraciones tengan relaciones entre ellas; y si, intentas ejecutar una migración que tiene una relación foránea con otra relación que aún no ha sido ejecutada, tendrás un error. Para ejemplificar esto; supón que, tenemos en nuestra carpeta de migraciones del proyecto:

Migraciones del proyecto

Que es el orden inverso que tenemos originalmente presentando en el apartado anterior (la migración de post y categoría); suponiendo que, no tengamos tablas en la base de datos del proyecto y si intentamos ejecutar las migraciones con:

$ php ​​artisan migrate

Verás que ocurre el error justamente cuando Laravel intenta crear la migración de post:

***
Migrating: 2024_04_17_100827_create_posts_table
  SQLSTATE[HY000]: General error: 1215 Cannot add foreign key constraint (SQL: alter table `posts` add constraint `posts_category_id_foreign` foreign key (`category_id`) references `categories` (`id`) on delete cascade)
  at C:\laragon\www\larafirststeps\vendor\laravel\framework\src\Illuminate\Database\Connection.php:712
    711catch (Exception $e) {
  ➜ 712▕             throw new QueryException(
    713▕                 $query, $this->prepareBindings($bindings), $e
    714▕             );
    715▕         }
    716▕     }

\vendor\laravel\framework\src\Illuminate\Database\Connection.php

PDOException::("SQLSTATE[HY000]: General error: 1215 Cannot add foreign key constraint")

\vendor\laravel\framework\src\Illuminate\Database\Connection.php

PDOStatement::execute()

Ya que, al estar antes definida que la de categorías, no puedes crear la relación foránea.

¿Cómo sabe Laravel qué falta por migrar?

Si intentas ejecutar php artisan migrate de nuevo, verás un mensaje indicando que "no hay nada que migrar" (Nothing to migrate). ¿Cómo lo sabe el sistema?

Laravel crea automáticamente una tabla interna llamada migrations (que no verás en tus archivos de migración porque es del sistema). En esta tabla se guarda un registro de cada archivo ejecutado:

  • Batch (Lote): Indica en qué grupo se ejecutó la migración. Por ejemplo, las tablas iniciales del proyecto suelen tener el lote 1, mientras que nuestra tabla de categorías tendrá el lote 2.
  • Registro: Si el nombre del archivo ya está en esa tabla, Laravel simplemente lo salta.

Flujo de las migraciones

En este apartado vamos a ver otros comandos y opciones útiles sobre las migraciones:

Revertir las migraciones (rollback)

Muchas veces nos damos cuenta de que faltó agregar una columna, o necesitamos cambiar la definición de la misma, en estos casos tenemos dos escenarios posibles:

  1. Crear otra migración para agregar estas columnas o cambiar las existentes.
  2. Hacer un rollback de las migraciones y hacer los cambios en la migración que define dicha tabla que queremos cambiar y ejecutar nuevamente las migraciones.

En definitiva, hacemos un rollback cuando no va bien y tenemos que hacer correcciones; el comando es:

migrate:rollback

Y si lo ejecutas, verás que se revierten justamente las migraciones que hayas ejecutado; es decir:

  • Si cuando ejecutamos el comando de migrate se ejecutaron las migraciones para las categorías y posts, entonces, al ejecutar el rollback, se revirtieron ambas migraciones.
  • Si cuando ejecutamos el comando de migrate se ejecutó solamente una de las migraciones, ya sea la de categorías o posts, entonces, al ejecutar el rollback, se habrá revertido justamente la migración que se ejecutó en el último migrate.

¿Cómo funciona el Rollback?

Básicamente, este comando busca el último número de lote (batch) en la tabla de migraciones y revierte todos los archivos que pertenezcan a ese grupo. Al ejecutarlo:

  1. Se dispara el método down() de la migración.
  2. Se elimina la tabla de la base de datos (en nuestro caso, la tabla de categorías desaparece).
  3. Se borra el registro correspondiente en la tabla interna de control de Laravel.

Ten mucho cuidado con este comando. En entornos de desarrollo no hay problema porque trabajamos con datos de prueba, pero si ejecutas un rollback en producción, eliminarás la tabla completa con todos los datos reales de los usuarios. ¡Úsalo con precaución!

Todo esto depende del número de lote que se crea al momento de ejecutar las migraciones:

Migraciones ejecutadas y número de batch

Este número es de carácter incremental, como ocurre cuando vas insertando nuevos registros en una tabla y se van generando los IDs/PKs de manera incremental:

Número de batch

Esta tabla es tomada en cuenta por el framework para saber que migraciones ha ejecutado, y tener el control del flujo en base al número de lote.

Importante notar que, si tenemos muchas migraciones y las mismas ya fueron ejecutadas, muchas veces no es posible hacer rollback hasta llegar a la migración que quieres editar; y esto es, para evitar revertir todas las migraciones que anteceden a esta; por lo tanto, una solución común es la de generar una nueva migración para indicar estos cambios.

Por ejemplo, suponte que tenemos las siguientes migraciones:

Migraciones de ejemplo

Y deseamos cambiar la migración de posts, que es una de las primeras como puedes evidencias en la imagen anterior; en estos casos, es muy complicado revertir todas tus migraciones hasta llegar a la de posts, para agregar la columna o columnas para luego ejecutar todas las migraciones nuevamente; lo que podemos hacer para evitar esto, es crear otra migración como:

// add_extra_campo_to_posts_table
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->string('extra_campo', 255)->nullable();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropColumn('extra_campo');
        });
    }
}

Que aplique los cambios sobre posts y la ejecutamos, para reflejar los cambios en la base de datos.

Ya que, recuerda que las migraciones las empleamos para dos propósitos, crear tablas, y alterar las tablas ya existentes con nuevas columnas, índices o removiendo estos.

Refrescar la base de datos

Este comando es una combinación entre el rollback y el migrate, ya que, Laravel lo que hace es hacer un rollback de todas las migraciones, para volverlas a ejecutar:

$ php artisan migrate:refresh

Si intentas ejecutar un:

$ php artisan migrate

Verás que, en este punto, Laravel no va ejecutar nuevamente las migraciones (devuelve "Nothing to migrate."), ya que, ya fueron ejecutadas; pero, si quieres hacer una ejecución fresca, tenemos el comando de:

$ php artisan migrate:refresh

Y verás que, primero hace un rollback y luego un migrate:

  2026_03_18_155116_create_categories_table  11.84ms DONE
  0001_01_01_000002_create_jobs_table  29.79ms DONE
  0001_01_01_000001_create_cache_table  19.75ms DONE
  0001_01_01_000000_create_users_table  29.64ms DONE
  0001_01_01_000002_create_jobs_table  55.35ms DONE  
  2026_03_18_155116_create_categories_table  11.54ms DONE
  2026_03_18_160307_create_posts_table  14.34ms DONE

Opcional: Sincronización con GitHub: Control y Respaldo

Lo siguiente que haremos por buenas prácticas es sincronizar nuestro proyecto con GitHub. Aunque trabajes solo, esto te ofrece ventajas increíbles: te permite llevar un rastreo de cada cambio (saber qué hiciste y cuándo) y compartir el código entre distintas máquinas. Como estaré alternando entre Mac y Windows durante el curso, esto me asegura tener exactamente el mismo proyecto en ambos ambientes.

GitHub es el servicio más popular, aunque existen otros como GitLab. Es una herramienta que se integra con Git, el sistema de control de versiones que debes tener instalado en tu equipo.

Si usas Mac, Git ya suele venir instalado. En Windows, puedes descargarlo desde la página oficial. Una vez instalado, no olvides configurar tu usuario y correo globalmente.

Configuración del Repositorio

Para empezar, debes crear un nuevo repositorio en tu cuenta de GitHub. Yo le pondré un nombre descriptivo, como course-book-laravel-base-13. Una vez creado, seguiremos estos pasos en la terminal:

  • git init: Inicializa el repositorio local en la carpeta de tu proyecto.
  • git add .: Agrega todos los archivos del proyecto al área de preparación (staging) para que Git empiece a rastrearlos.
  • git commit -m "Primeros pasos": Crea una "foto" o estado de tu proyecto con un mensaje descriptivo.

Subiendo el código a la nube

Una vez que tengas tus cambios locales, debemos conectarlos con el repositorio remoto en GitHub. Usaremos estos comandos (que GitHub te proporciona al crear el repo):

  • git remote add origin [URL-DE-TU-REPOSIROTIO]: Establece el vínculo con el servidor.
  • git branch -M main: Renombra la rama principal a main (una convención moderna).
  • git push -u origin main: Sube tus archivos locales a la nube. En este punto, si es tu primera vez, se abrirá una ventana para que te autentiques.

Ahora, si recargas tu página de GitHub, verás todo tu código fuente, incluyendo la carpeta database/migrations con los cambios que hicimos.

Uso de Etiquetas (Tags) para el seguimiento del libro

Para que tú puedas tener el código exactamente igual a como yo lo dejo al final de cada sección, utilizaré Etiquetas (Tags) en lugar de ramas, ya que funcionan como un "espejo" o punto de control fijo.

Para crear y subir una etiqueta, los comandos son:

  • git tag -a v0.1 -m "Migraciones completadas": Crea la etiqueta localmente.
  • git push origin --tags: Sincroniza todas las etiquetas con el repositorio remoto.

Este proceso solo lo explicare en este apartado, aunque hare estos mismos pasos al final de cada capítulo.

Si necesitas ayuda para poder configurar Git en tu equipo:

https://www.desarrollolibre.net/blog/programacion-basica/la-guia-de-git-que-nunca-tuve

FOREIGN_KEY_CHECKS para el roolback de las migraciones de tipo foreign key en Laravel

Veremos cómo podemos resolver un error bastante molesto que puede ocurrir cuando haces el rollback de las migraciones que tengan relaciones de tipo foráneas en Laravel: el error en cuestión, luce como el siguiente:

SQLSTATE[HY000]: General error: 3730 Cannot drop table 'tutorials' referenced by a foreign key constraint 'inscribed_tutorial_id_foreign' on table 'inscribed'. (SQL: drop table if exists `tutorials`)
 at vendor/laravel/framework/src/Illuminate/Database/Connection.php:703
   699        // If an exception occurs when attempting to run a query, we'll format the error
   700    // message to include the bindings with SQL, which will make this exception a
   701      // lot more helpful to the developer instead of just the database's errors.
   702   catch (Exception $e) {
 703             throw new QueryException(
   704                $query, $this->prepareBindings($bindings), $e
   705            );
   706        }
   707    }

Esto es, a primeros análisis, un error de las claves foráneas o foreign key que aplicamos a nuestras tablas, todo es muy bueno cuando hacemos este tipo de relaciones, en la cual, por ejemplo, la tabla tutorials existe antes que la tabla inscribed_tutorial_id_foreign, y a esta última, aplicamos una relación de tipo FK o foránea:

<?php
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class CreateTutorialsTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('tutorials', function (Blueprint $table) {
           ***
           $table->foreignId('user_id')->constrained()
           ->onDelete('cascade');
           ***
       });
   }
   public function down()
   {
       DB::statement('SET FOREIGN_KEY_CHECKS = 0');
       Schema::dropIfExists('tutorials');
       DB::statement('SET FOREIGN_KEY_CHECKS = 1');
   }
}

Como puedes ver, la novedad está en que en el momento de borrar una tabla que guarda relaciones con otra tabla de tipo FK, desactivamos el check a las claves foráneas.

Migraciones lista
Migraciones lista

Resolución

Para evitar esto, podemos desactivar el check para las claves foráneas en aquellas tablas que es empleada por otras para guardar las referencias (FKs)

Opcional

En SQL es algo así:

SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS table1;
SET FOREIGN_KEY_CHECKS = 1;

Siempre recuerda, luego de hacer el drop de la tabla, volver a habilitar el check

FOREIGN_KEY_CHECKS para el roolback de las migraciones de tipo foreign key en Laravel

Veremos cómo podemos resolver un error bastante molesto que puede ocurrir cuando haces el rollback de las migraciones que tengan relaciones de tipo foráneas en Laravel: el error en cuestión, luce como el siguiente:

SQLSTATE[HY000]: General error: 3730 Cannot drop table 'tutorials' referenced by a foreign key constraint 'inscribed_tutorial_id_foreign' on table 'inscribed'. (SQL: drop table if exists `tutorials`)
 at vendor/laravel/framework/src/Illuminate/Database/Connection.php:703
   699        // If an exception occurs when attempting to run a query, we'll format the error
   700    // message to include the bindings with SQL, which will make this exception a
   701      // lot more helpful to the developer instead of just the database's errors.
   702   catch (Exception $e) {
 703             throw new QueryException(
   704                $query, $this->prepareBindings($bindings), $e
   705            );
   706        }
   707    }

Esto es, a primeros análisis, un error de las claves foráneas o foreign key que aplicamos a nuestras tablas, todo es muy bueno cuando hacemos este tipo de relaciones, en la cual, por ejemplo, la tabla tutorials existe antes que la tabla inscribed_tutorial_id_foreign, y a esta última, aplicamos una relación de tipo FK o foránea:

<?php
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class CreateTutorialsTable extends Migration
{
   /**
    * Run the migrations.
    *
    * @return void
    */
   public function up()
   {
       Schema::create('tutorials', function (Blueprint $table) {
           ***
           $table->foreignId('user_id')->constrained()
           ->onDelete('cascade');
           ***
       });
   }
   public function down()
   {
       DB::statement('SET FOREIGN_KEY_CHECKS = 0');
       Schema::dropIfExists('tutorials');
       DB::statement('SET FOREIGN_KEY_CHECKS = 1');
   }
}

Como puedes ver, la novedad está en que en el momento de borrar una tabla que guarda relaciones con otra tabla de tipo FK, desactivamos el check a las claves foráneas.

Migraciones lista
Migraciones lista

Resolución

Para evitar esto, podemos desactivar el check para las claves foráneas en aquellas tablas que es empleada por otras para guardar las referencias (FKs)

Opcional:

En SQL es algo así:

SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS table1;
SET FOREIGN_KEY_CHECKS = 1;

Siempre recuerda, luego de hacer el drop de la tabla, volver a habilitar el check

Porque NO uso las migraciones para agregar columnas

Porque NO uso las migraciones para agregar columnas
Video thumbnail

Por aquí quiero hablar de un tema que creo que es interesante: las migraciones en Laravel.

Tipos de migraciones en Laravel

Recordemos que existen dos tipos de migraciones para entrar un poquito en contexto. Está la típica:

$ php artisan migrate

Siempre comienza así. Luego, create, indicas qué es lo que quieres crear (por ejemplo, el nombre de la tabla: payments, libros, etc. — users ya la tenemos). Y con eso ya se entiende que vas a crear una tabla. Esto lo puedes ver aquí; por ejemplo, fue lo que hicimos para la parte de payments. Si la encuentro… aquí está. Entonces, se va a crear una estructura llamada payments.

Pero ¿qué pasa cuando quieres, por ejemplo, agregar una columna y demás? Ahí tenemos una sintaxis ligeramente distinta.

$ php artisan make:migration add_nombre_columna_to_nombre_tabla_table

Esto es un poquito complicado. Yo lo hablo desde la perspectiva de que soy un desarrollador independiente. Todos los proyectos que hago los hago yo mismo, ya sea para terceros o directamente para mí.

Por lo tanto, cuando quiero hablar con alguien, hablo conmigo mismo. No es un proyecto en equipo, así que tengo ciertas limitantes… y también adapto mi forma de programar a eso.

Por ejemplo, Git lo uso siempre. A mí me pueden explotar la máquina —lo lamentaría, claro— pero mi trabajo no lo pierdo porque siempre lo tengo sincronizado. Lo sincronizo, como quien dice, conmigo mismo.

Uso Git, luego voy a otra máquina, bajo los cambios, sigo desarrollando… y así. Esto es muy diferente cuando estás desarrollando en un equipo de 10 personas en un mismo proyecto. En esos casos puede que en algún punto se solapen archivos, haya conflictos, y se tengan que resolver con merge. Yo no tengo esos problemas, porque trabajo solo.

Tampoco es que vaya a prender dos máquinas para trabajar en paralelo. Trabajo en una, sincronizo, voy a la otra, hago pull, sigo trabajando, y ya.

La mala práctica que me funciona

Esto también se aplica un poco a las migraciones. Y aquí es donde reconozco que puedo estar siguiendo una mala práctica.

Yo odio tener tablas “regadas” en múltiples migraciones. Sobre todo con entidades tan genéricas y flexibles como los pagos. Por ejemplo, al principio tenía un producto llamado “curso”. Luego quise manejar también “libros”. Como al inicio mi organización no fue la mejor, tengo cosas duplicadas.

Eso luego afecta a otras tablas, como la de usuarios. Al principio no vendía nada, pero cuando empecé a vender, tuve que agregar más campos a users. Suponte que users no fue generada por Laravel, sino por mí. Esto aplica con cualquier entidad.

Lo que quiero concluir es que seguramente en algún punto vas a querer modificar las tablas. Sobre todo para agregar columnas o usar tipos enumerados. Siempre vas a querer agregar cositas.

La forma “Laravel” es crear una nueva migración y hacer los cambios ahí. Esa sería la solución correcta... o al menos la más recomendada. Pero ¿qué pasa? Cuando busques payments, por ejemplo, te van a aparecer 20 migraciones para esa tabla: una donde agregaste una columna, otra donde quitaste un enum, otra donde cambiaste el nombre…

Eso me parece horroroso. No me gusta.

Mi alternativa

En mi caso, prefiero tener toda la estructura definida en una sola migración y no en múltiples. Pero esto trae un problema: si haces un cambio, tienes que eliminar toda la base de datos para que la migración vuelva a aplicarse. Eso no es lo ideal porque perderías todos los datos de prueba en desarrollo (y en producción ni hablar: no se puede hacer).

Además, trabajo en un servidor compartido. No tengo acceso a la terminal para ejecutar estos comandos. Tengo que dar directamente el SQL. Que yo sepa, Laravel no genera ese SQL de forma directa, pero tú ya entiendes el problema.

¿Cómo lo resuelvo?
Lo que yo hago es no generar migraciones intermedias. Y es por eso que no recuerdo bien los comandos. Cuando quiero agregar un campo nuevo —por ejemplo, retornado— lo agrego directamente en la migración original. Esto pasa mucho: a veces desarrollas módulos después, como uno de cancelaciones o disputas, y te das cuenta de que necesitas nuevas columnas.

Por ejemplo, al principio tienes cancelado, y después te das cuenta de que necesitas también un campo disputa. Luego integras MercadoPago. Y todo eso serían cinco migraciones diferentes... ¡para una sola tabla!

Es una pesadilla leerlo así. Y si un desarrollador nuevo entra al equipo, tiene que armar el rompecabezas en su cabeza para entender cómo quedó la tabla.

Entonces, lo que yo hago es modificar directamente la migración original. Pero claro, ¿cómo replico eso en la base de datos ya creada? Lo que hago es usar herramientas como DBgin, TablePlus, phpMyAdmin o el que trae Laragon.

Migraciones manuales con SQL

Estos sistemas gestores de base de datos generan el comando SQL por detrás. Yo hago el cambio desde la interfaz gráfica (por ejemplo, agregar un campo enum) y luego copio ese SQL.

Después lo ejecuto manualmente en mis otras máquinas… y también en el servidor de producción (donde no tengo acceso a la consola). En total, uso unas 4 máquinas, más producción:

ALTER TABLE `file_payments`
CHANGE `payments` `payments` enum('paypal','stripe','free','other', 'google', 'apple') NOT NULL DEFAULT 'paypal';

De esta forma, tengo todas mis migraciones organizadas. Las lees, las abres, y no tienes que estar armando rompecabezas. Además, evito tener tantas migraciones.

Lo malo es que sí es una mala práctica, porque Laravel está pensado para que manejes la base de datos desde el framework. Pero como te digo, a mí me ha servido.

Aunque reconozco que trae problemas. Por ejemplo, una vez definí un enum con un espacio al final del valor… y eso causó problemas. Aquí en la migración estaba bien definido, pero en el SQL que se generó manualmente, no. Entonces, cuando comparas valores, no coinciden (null vs. vacío, por ejemplo).

Ese tipo de errores son sutiles y difíciles de detectar. Pero bueno, son cosas que uno aprende con el tiempo.

Te hablo de como hago para cuando quiero modificar una tabla en Laravel evitar crear una migración adicional para MODIFICAR la tabla.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english