Relaciones muchos a muchos en Eloquent

Author
By Darío Rivera
Posted on 2019-07-21 in Laravel
Tags   Laravel 5.8

En un post anterior hemos visto las relaciones uno a muchos en eloquent y un ejemplo muy sencillo de su funcionamiento con laravel tinker. El día de hoy veremos el siguiente tipo de relación el cuál es la relación muchos a muchos. El objetivo de este post es que aprendas a realacionar modelos de laravel utilizando las herramientas de Eloquent mediante un ejemplo práctico. Para lograr esto vamos a tratar de imaginarnos un ejemplo de la vida real y a modelarlo con eloquent. Veamos así el problema del post anterior:

Nuestro cliente el Sr. Jobs, quiere crear una página web en la que sus usuarios puedan subir y descargar todo tipo de contenido compartido, algo muy similar a Dropbox o GDrive. Para esto, el Sr. Jobs desea que los usuarios puedan subir a su aplicación archivos de distinto tipo. Es obligatorio que los usuarios estén registrados para subir contenido y que quede el registro de quién subió un archivo. También se debe llevar registro de las descargas de cada archivo con la última fecha en la que fue descargado. Finalmente, al momento de descargar un archivo este puede descargarse en distintos formatos dependiendo del archivo. Por ejemplo, una imagen PNG podría ser descagada en JPEG o comprimida en ZIP/TAR/7ZIP.

Como puedes observar he resaltado las palabras en donde intervienen las entidades que tendrémos que modelar las cuáles son las siguientes:

- Usuarios
- Archivos
- Tipos de archivo
- Descargas
- Tipos de exporte de archivo

Para crear estas entidades basta ejecutar los siguientes comandos en la raíz de un proyecto limpio de laravel:

php artisan make:model File -m
php artisan make:model FileType -m
php artisan make:model FileDownload -m
php artisan make:model FileTypeExport -m

Nótese que no hemos creadao un modelo User ya que laravel por defecto trae este modelo al crear un nuevo proyecto.

Relaciones muchos a muchos

Una relación muchos a muchos entre dos tablas A y B se presenta cuando un registro de A está relacionado con uno o más elementos de B. De manera análoga, un registro de B puede estar relacionado con uno o más elementos en A. Según la teoría de bases de datos relacionales, este tipo de relación produce una tabla intermedia que relaciona las llaves primarias de ambas tablas y si es necesario se crea una o más columnas con información adicional.

Para el ejemplo que nos acontece existe una relación muchos a muchos entre la entidad FileType y la entidad File ya que cada archivo puede ser descargado en uno o más formatos y cada tipo de archivo puede tener cientos de archivos subidos. Esta relación se expresa mediante el modelo FileTypeExport. No se debe confundir esta relación con la planteada en el anterior post la cuál es una relación uno a muchos (un archivo es de un tipo, un tipo tiene varios archivos). Esto muestra que puede definirse más de una relación distinta entre dos tablas.

11_1.png

Hemos omitido los timestamps para simplificar el modelo. Dicho esto vamos a realizar las migraciones respectivas para que se asemejen al esquema anterior. No olvides revisar nuestras entradas qué es eloquent y migraciones de base de datos en laravel si no estás seguro de cómo se crea una migración, cómo funciona eloquent o incluso cómo utilizar laravel tinker.

# implementación del método up() en la clase CreateFilesTable
Schema::create('files', function (Blueprint $table) {
    $table->bigIncrements('file_id');
    $table->integer('file_type_id');
    $table->string('file_name');
    $table->integer('created_by');
    $table->timestamps();
});

# implementación del método up() en la clase CreateFileTypesTable
Schema::create('file_types', function (Blueprint $table) {
    $table->bigIncrements('file_type_id');
    $table->string('mime_type', 150);
    $table->string('extensions', 100);
    $table->timestamps();
});

# implementación del método up() en la clase CreateFileTypeExportsTable
Schema::create('file_type_exports', function (Blueprint $table) {
    $table->integer('file_type_id');
    $table->integer('file_id');
});

Si ya revisaste el tutorial de relaciones uno a muchos solo tienes que agregar la implementación en CreateFileTypeExportsTable. Nuevamente viene lo interesante, cómo le comunico a eloquent que existe una relación entre estos  modelos ?. La respuesta el es método belongsToMany. Ejecutemos las migraciones con php artisan migrate:fresh y agreguemos el siguiente contenido al modelo File.

public function fileTypesItCanBeExported()
{
    // laravel assumes table1_table2 as name of the intermediate table (ordered alphabetically)
    // laravel assumes the intermediate table has the same primary key names of them parents
    // return $this->belongsToMany('App\FileType', 'middle_table', 'local_key', 'foreign_key');
    return $this->belongsToMany('App\FileType', 'FileTypeExport', 'file_id', 'file_type_id');
}

Con esto le hemos dicho a laravel que cada objeto File tiene relación muchos a muchos con el objeto FileType. El segundo parámetro de belongsToMany()  indica el nombre de la tabla intermedia. Además de esto no olvides que laravel asume que las llaves primarias de una tabla intermedia son siempre el nombre de la tabla en singular más el nombre de la columna de llave primaria de cada tabla. Dado esto hemos hemos tenido que utilizar los parámetros tres y cuatro para indicar de manera explícita los nombres de los campos que tiene nuestra tabla intermedia. Dado que nuestro modelo FileTypeExport no necesita timestamps, debemos indicarlo para que no intente realizar inserciones a la base de datos con estos campos.

namespace App;

use Illuminate\Database\Eloquent\Model;

class FileTypeExport extends Model
{
    /**
     * Indicates if the model should be timestamped.
     *
     * @var bool
     */
    public $timestamps = false;
}

Antes de ver la relación inversa vamos a probar nuestra relación con laravel tinker. Creemos un par de tipos de archivos, algunos usuarios y algunos archivos.

# creating the file types
$fileType = new \App\FileType();
$fileType->mime_type = 'text/plain';
$fileType->extensions = '.txt';
$fileType->save();
$fileType = new \App\FileType();
$fileType->mime_type = 'image/png';
$fileType->extensions = '.png';
$fileType->save();
$fileType = new \App\FileType();
$fileType->mime_type = 'application/zip';
$fileType->extensions = '.zip';
$fileType->save();

# creating the users
$user = new \App\User;
$user->name = 'Mitnick';
$user->email = 'kevin.mitnick@mitnicksecurity.com';
$user->password = '$2y$10$8pthlW3eg9lHOhHOfS7PTeZ5JRQEXdle8ATy9.4FHHcSg9MV8/tjO';
$user->save();

# creating the files
$file = new \App\File();
$file->file_name = 'google_passwords.txt';
$file->file_type_id = 1;
$file->created_by = 1;
$file->created_at = date('Y-m-d');
$file->save();
$file = new \App\File();
$file->file_name = 'my_girlfriend.png';
$file->file_type_id = 2;
$file->created_by = 1;
$file->created_at = date('Y-m-d');
$file->save();

# creating the file type exportings
$fte = new \App\FileTypeExport;
$fte->file_id = 2;
$fte->file_type_id = 2;
$fte->save();
$fte = new \App\FileTypeExport;
$fte->file_id = 2;
$fte->file_type_id = 3;
$fte->save();

Si ya realizaste el tutorial de relaciones uno a muchos solo debes ejecutar la parte de creación de file type exportings. No olvides que eloquent automáticamente calcula los consecutivos incrementales para cada tabla al realizar el registro de la información excepto para la tabla intermedia file_type_exports. Vamos entonces en la misma sesión de eloquent a realizar la consulta directa a la base de datos del segundo archivo y a traer los tipos de formato a los que puede ser exportado.

(new \App\File)->find(2)->fileTypesItCanBeExported;

El resultado de esto sería muy similar al siguiente:

=> Illuminate\Database\Eloquent\Collection {#2967
     all: [
       App\FileType {#2953
         file_type_id: 2,
         mime_type: "image/png",
         extensions: ".png",
         created_at: "2019-07-21 15:58:36",
         updated_at: "2019-07-21 15:58:36",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#2969
           file_id: 2,
           file_type_id: 2,
         },
       },
       App\FileType {#2973
         file_type_id: 3,
         mime_type: "application/zip",
         extensions: ".zip",
         created_at: "2019-07-21 15:58:36",
         updated_at: "2019-07-21 15:58:36",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#2954
           file_id: 2,
           file_type_id: 3,
         },
       },
     ],
   }

Observa que el resultado de la propiedad dinámica fileTypesItCanBeExported es una colección de objetos FileType y para cada objeto existe una propiedad llamada pivot. Esto indica que la relación ha quedado definida de manera correcta. No debes confundir este resultado con el de la propiedad dinámica fileType el cuál es la relación uno a muchos entre File y FileType que indica el tipo de archivo. Veamos el resultado de esta propiedad en tinker.

>>> (new \App\File)->find(2)->fileType
=> App\FileType {#2965
     file_type_id: 2,
     mime_type: "image/png",
     extensions: ".png",
     created_at: "2019-07-21 15:58:36",
     updated_at: "2019-07-21 15:58:36",
   }

No olvides que los caracteres >>> se muestran en el prompt de tinker y no hacen parte del comando. Como ves el resultado de la propiedad anterior no muestra una colección, simplemente muestra el tipo de archivo en un objeto FileType. Ahora bien, vamos a definir la inversa de la relación muchos a muchos. Para lograr esto debemos definir el método filesFromItCanBeExported en el modelo FileType.

public function filesFromItCanBeExported()
{
    // laravel assumes table1_table2 as name of the intermediate table (ordered alphabetically)
    // laravel assumes the intermediate table has the same primary key names of them parents
    // return $this->belongsToMany('App\File', 'middle_table', 'local_key', 'foreign_key');
    return $this->belongsToMany('App\File', 'file_type_exports', 'file_type_id', 'file_id');
}

No hace falta hacer nada más, vamos al tinker y nos traemos los objetos File que pueden ser exportados a ZIP.

>>> (new \App\FileType)->find(3)->filesFromItCanBeExported;
=> Illuminate\Database\Eloquent\Collection {#2966
     all: [
       App\File {#2965
         file_id: 2,
         file_type_id: 2,
         file_name: "my_girlfriend.png",
         created_by: 1,
         created_at: "2019-07-21 00:00:00",
         updated_at: "2019-07-21 15:58:37",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#2952
           file_type_id: 3,
           file_id: 2,
         },
       },
     ],
   }

Voila!. Como puedes observar nuevamente es una colección de objetos, esta vez File con el atributo  pivot. Ten en cuenta que para que esto funcione debes cerrar la sesión anterior del tinker para que tome los cambios en el modelo File.

Para finalizar, ten cuenta que para acceder a las columnas de la tabla intermedia lo puedes hacer a través de la propiedad dinámica  pivot. Para este ejemplo nos hemos salido un poco del estándar al nombrar las propiedades dinámicas ya que normalmente el nombre de los métodos es el mismo nombre de la tabla con la cuál está relacionada. Es decir, una relación muchos a muchos entre roles y usuarios debería tener un método roles() en el modelo User y un método users() en el modelo Roles. Sin embargo, ya existína una relación uno a muchos entre tipos de archivos y archivos, por lo que utilizar este estándar podría causar solapamiento y un poco de confusión con las otras dos propiedades dinámicas. Hasta la próxima!.


Si te ha gustado este artículo puedes invitarme a tomar una taza de café

Acerca de Darío Rivera

Author

Ingeniero de desarrollo en PlacetoPay , Medellín. Darío ha trabajado por más de 6 años en lenguajes de programación web especialmente en PHP. Creador del microframework DronePHP basado en Zend y Laravel.

Sólo aquellos que han alcanzado el éxito saben que siempre estuvo a un paso del momento en que pensaron renunciar.