Uno de los pasos más importantes en el diseño de una base de datos es la creación de relaciones entre las tablas, ya que nos permiten vincular y recuperar datos almacenados en múltiples tablas de una manera eficiente. Para crear este vínculo se debe especificar una clave foránea en una tabla que haga referencia a una columna en otra tabla. Sin embargo, debemos asegurarnos que la relación sea consistente y es a través de la integridad referencial de datos que lo hacemos posible. A continuación veremos algunos aspectos a tomar en cuenta al definir claves foráneas en una base de datos en Laravel para cumplir con la integridad referencial.

La integridad referencial de datos se refiere a las restricciones o reglas que se le aplican a una base de datos a fin de garantizar que los registros entre tablas relacionadas sean válidos y consistentes, esto se hace a través del uso de restricciones de claves foráneas para así prevenir cambios accidentales y su propagación entre tablas relacionadas.

En Laravel podemos implementar la integridad referencial de datos cuando creamos las migraciones de la base de datos y definimos las claves foráneas de las tablas que se encuentran relacionadas.

Definición de migraciones y claves foráneas

Antes de comenzar debemos tener creada una base de datos en MySQL y configuradas las credenciales en el archivo .env de la aplicación.

Supongamos que en una aplicación en Laravel tenemos que trabajar con productos y categorías, entonces creamos las migraciones para definir las tablas ejecutando:

php artisan make:migration create_categories_table --create=categories
php artisan make:migration create_products_table --create=products

Los archivos de migraciones creados se encuentran en la carpeta database/migrations.

Agregamos algunos campos a las tablas, para categories:

<?php

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

class CreateCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->engine = 'InnoDB';
            $table->increments('id');
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('categories');
    }
}

Por otro lado, para la tabla products agregamos una clave foránea para restringir que un producto deba pertenecer a una categoría válida de la siguiente forma:

$table->integer('category_id')->unsigned();            
$table->foreign('category_id')->references('id')->on('categories');

A partir de Laravel 5.8 se utiliza por defecto bigIncrements en vez de increments para las llaves primarias. Si estás usando bigIncrements para tu llave primaria debes declarar el campo category_id como bigInteger en vez de integer. De esta manera el campo de la llave primaria y de la llave foránea compartirán el mismo tipo de dato, de lo contrario obtendrás un error.

Con esto creamos un campo tipo entero y sin signo (no serán números negativos) category_id ya que lo asignaremos como clave foránea que hace referencia al campo id de la tabla categories y estos deben coincidir en tipo (un campo definido con el método «increments» en Laravel, resulta en un entero sin signo).

Por tanto, el archivo de migración para la tabla products podría quedar de la siguiente manera:

<?php

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

class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->engine = 'InnoDB';
            $table->increments('id');
            $table->string('name');

            $table->integer('category_id')->unsigned();
            $table->foreign('category_id')->references('id')->on('categories');

            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('products');
    }
}

Hay dos cosas importantes que debemos tomar en cuenta antes de ejecutar las migraciones:

Cuando trabajamos con claves foráneas debemos especificar que el motor de almacenamiento sea ‘InnoDB’ si es que no está especificado como default, ya que es el que soporta trabajar con ellas y la integridad referencial. Lo hacemos agregando a la definición de las tablas:

$table->engine = 'InnoDB';

Adicionalmente, cuando trabajamos con claves foráneas el orden de las migraciones importa y éste viene dado por el orden en que ejecutemos php artisan make:migration en cada archivo de migración, por tanto, se debe crear primero las migraciones para las tablas que serán referenciadas y luego las migraciones para las tablas que contendrán claves foráneas. Para este ejemplo NO podríamos hacer:

php artisan make:migration create_products_table
php artisan make:migration create_categories_table

ya que se crearían los siguientes archivos:

2016_06_20_183844_create_products_table.php
2016_06_20_183848_create_categories_table.php

y al ejecutar php artisan migrate  con las migraciones en ese orden, obtendríamos el siguiente error:

SQLSTATE[HY000]: General error: 1215 Cannot add foreign key constraint

indicándonos que tenemos un problema de integridad referencial porque la clave foránea no puede ser creada, debido a que la tabla categories, que se está referenciando en la tabla products a través de category_id, aún no existe.

Otra forma diferente de hacerlo es crear las migraciones de cada una de las tablas sin importar el orden pero sin incluir las claves foráneas que apartaremos para ejecutarlas en la última migración de esta manera:

php artisan make:migration add_foreign_key_products_table --table=products

Luego agregamos lo siguiente al archivo creado:

<?php

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

class AddForeignKeyProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('products', function (Blueprint $table) {
            $table->integer('category_id')->unsigned();
            $table->foreign('category_id')
                 ->references('id')->on('categories');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('products', function (Blueprint $table) {
            $table->dropForeign('products_category_id_foreign');
        });
    }
}

De esta manera, creamos todas las tablas sin claves foráneas para evitar errores de integridad referencial y luego añadimos las claves foráneas a cada tabla.

Inserción de registros con claves foráneas

Cuando agregamos un nuevo registro a una tabla que tiene una clave foránea definida usando la integridad referencial anteriormente explicada, podemos evitar que se relacione o se referencie a un registro que no existe en la otra tabla. Si esto sucede, nos aparecerá el siguiente error:

SQLSTATE[23000]: Integrity constraint violation: 
1452 Cannot add or update a child row: a foreign key constraint fails ...

Lo que quiere decir que el valor que le estamos asignando al campo de la clave foránea no coincide con ningún registro de la tabla referenciada.

Un caso donde nos puede ocurrir esto es cuando estamos cargando datos por medio de seeders pues debemos generar datos para las tablas referenciadas antes de insertar registros a las tablas con claves foráneas. Veamos esto con un ejemplo.

Creamos los modelos Product y Category en el directorio app, ejecutando:

php artisan make:model Product
php artisan make:model Category

Ahora, creamos los seeder para cada tabla, que se ubicaran en la carpeta database/seeds:

php artisan make:seed ProductsTableSeeder
php artisan make:seed CategoriesTableSeeder

Para CategoriesTableSeeder agregamos algunas categorías en el método run:

public function run()
{
    App\Category::create(['name' =>'Fruta']);
    App\Category::create(['name' =>'Bebida']);
}

y para ProductsTableSeeder:

public function run()
{
    App\Product::create(['name' => 'Leche', 'category_id' => 2]);
    App\Product::create(['name' => 'Agua', 'category_id' => 2]);
}

En el método run del archivo /database/seeds/DatabaseSeeder.php debemos asegurarnos que el orden sea el siguiente para que los registros se creen correctamente al ejecutar php artisan db:seed :

public function run()
{
    $this->call(CategoriesTableSeeder::class);
    $this->call(ProductsTableSeeder::class);
}

Si intercambias el orden de los llamados a los seeder te aparecerá el error antes mencionado. Prueba cambiándolo y ejecuta: php artisan migrate:refresh –seed

Actualización y eliminación de registros

Las restricciones que aplicamos por medio de claves foráneas nos permiten mantener la integridad de los datos de nuestra aplicación y esto también abarca, por ejemplo, si quisiéramos eliminar una categoría solo podría ser posible si ésta no está asignada a ningún producto.

Por ejemplo, usando los registros creados con los seeder podríamos eliminar satisfactoriamente la categoría con id=1, usemos tinker para probarlo:

php artisan tinker

escribimos:

$cat = App\Category::find(1);
$cat->delete();

y se elimina la categoría sin embargo, hacer el mismo procedimiento para la categoría con id=2 no será posible y nos devolverá este error:

Illuminate\Database\QueryException with message 'SQLSTATE[23000]: 
Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint 
fails (`laravel`.`products`, CONSTRAINT `products_category_id_foreign` FOREIGN KEY (`category_id`) 
REFERENCES `categories` (`id`)) (SQL: delete from `categories` where `id` = 2)'

Esto es debido a la integridad referencial que definimos con la clave foránea, puesto que dicha categoría está siendo usada en algunos productos y su eliminación causaría una inconsistencia en la base de datos.

Sin embargo, sí podemos eliminar un registro con clave foránea, estableciendo en la definición de la tabla una de las dos acciones referenciales: eliminación en cascada o eliminación con set null.

  • Eliminación en cascada o ON DELETE CASCADE: Cuando se elimina un registro de la tabla padre que está siendo referenciado,  automáticamente se eliminan todos los registros que coincidan en la clave foránea de la tabla relacionada.
  • Eliminación con set null o ON DELETE SET NULL: Cuando se elimina el registro de la tabla padre, se establece el campo de la clave foránea en la tabla relacionada como NULL. Si se define una acción como SET NULL, debemos estar pendiente de no declarar el campo en la tabla como NOT NULL.

Esto también es aplicable para la actualización de registros, es decir, que podemos usar las acciones ON UPDATE CASCADE y ON UPDATE SET NULL.

En las migraciones de Laravel tenemos disponibles los métodos onDelete() y onUpdate() con los que podemos aplicar las acciones referenciales a una clave foránea.

Por ejemplo si queremos que cuando se elimine una categoría se eliminen también todos los productos relacionados a ella se define en la migración de la tabla products lo siguiente:

$table->foreign('category_id')
      ->references('id')->on('categories')
      ->onDelete('cascade');

En cambio, si queremos que al eliminar una categoría no se eliminen los productos relacionados sino que el campo se defina como NULL en migración de la tabla products colocamos:

$table->integer('category_id')->unsigned()->nullable();

$table->foreign('category_id')
    ->references('id')
    ->on('categories')
    ->onDelete('set null');

¡Bien! esto es todo acerca de la integridad referencial de datos en Laravel, espero que este tutorial sea de utilidad para diseñar bases de datos más consistentes y por favor ayúdanos compartiéndolo en las redes sociales.

Material relacionado

Únete a nuestra comunidad en Discord y comparte con los usuarios y autores de Styde, 100% gratis.

Únete hoy

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.

Lección anterior Traits para el desarrollo de pruebas de integración en Laravel