Relaciones uno a muchos en Eloquent

2019-07-19 Laravel
Tags   Laravel 5.8

En un post anterior hemos visto las relaciones uno a uno 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 uno 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 uno a muchos

Una relación uno 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. Sin embargo, un registro en B solo puede estar relacionado con un registro en A. Este tipo de relación suele ser el más natural a modelar en una base de datos. Podemos tener un ejemplo de esta relación en la creación de un blog en las entidades Post y Comments. Un Post tiene muchos comentarios, y a su vez un comentario pertenece a un solo post.

Para el ejemplo que nos acontece existe una relación uno a muchos entre la entidad FileType y la entidad File ya que cada archivo puede ser de un tipo pero un tipo puede tener varios archivos. Además, cada usuario es propietario de uno o más archivos, con lo cuál el diagrama que modela esta relación es el siguiente:

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', 100);
    $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();
});

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

namespace App;

use Illuminate\Database\Eloquent\Model;

class FileType extends Model
{
    /**
     * The primary key associated with the table.
     *
     * @var string
     */
    protected $primaryKey = 'file_type_id';

    public function files()
    {
        // laravel assumes user_id as foreign and local key.
        //return $this->hasMany('App\File');
        return $this->hasMany('App\File', 'file_type_id', 'file_type_id');
    }
}

Con esto le hemos dicho a laravel que cada objeto FileType tiene relación uno a muchos con el objeto File. Además de esto no olvides que laravel asume que las llaves primarias de las tablas son siempre id, dado esto hemos modificado el nombre de la llave primaria en nuestro modelo FileType. Observa que si nuestras llaves primarias tanto en la tabla de tipos de archivo como de archivos hubiera sido id, no hubiese sido necesario definir los parámetros 2 y 3 del método hasMany que son respectivamente la llave foránea y local. Vamos con la definición de llave primaria en nuestro modelo File.

namespace App;

use Illuminate\Database\Eloquent\Model;

class File extends Model
{
    /**
     * The primary key associated with the table.
     *
     * @var string
     */
    protected $primaryKey = 'file_id';
}

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();

No olvides que eloquent automáticamente calcula los consecutivos incrementales para cada tabla al realizar el registro de la información. Vamos entonces en la misma sesión de eloquent a realizar la consulta directa a la base de datos del primer tipo de archivo y a traer sus archivos asociados.

(new \App\FileType)->find(1)->files;

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

=> Illuminate\Database\Eloquent\Collection {#2969
     all: [
       App\File {#2970
         file_id: 1,
         file_type_id: 1,
         file_name: "google_passwords.txt",
         created_by: 1,
         created_at: "2019-07-20 00:00:00",
         updated_at: "2019-07-20 04:12:23",
       },
     ],
   }

Observa que el resultado de la propiedad dinámica file es una colección de objetos File. Esto indica que la relación ha quedado definida de manera correcta. Si cambiaramos el parámetro de find() para obtener los archivos de tipo 2 tendríamos el siguietne resultado.

>>> (new \App\FileType)->find(2)->files
=> Illuminate\Database\Eloquent\Collection {#2972
     all: [
       App\File {#2954
         file_id: 2,
         file_type_id: 2,
         file_name: "my_girlfriend.png",
         created_by: 1,
         created_at: "2019-07-20 00:00:00",
         updated_at: "2019-07-20 04:12:24",
       },
     ],
   }

No olvides que los caracteres >>> se muestran en el prompt de tinker y no hacen parte del comando. Ahora bien, vamos a definir la inversa de la relación. Para lograr esto debemos definir el método fileType() en el modelo File.

public function fileType()
{
    // laravel assumes user_id as foreign and local key.
    //return $this->belongsTo('App\FileType');
    return $this->belongsTo('App\FileType', 'file_type_id', 'file_type_id');
}

No hace falta hacer nada más, vamos al tinker y nos traemos el tipo de archivo desde el archivo dos.

>>> (new \App\File)->find(2)->fileType->mime_type;
=> "image/png"

Voila!. 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.

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.