Relaciones uno a uno en Eloquent
En un post anterior hemos visto qué es eloquent y un ejemplo muy sencillo de su funcionamiento con laravel tinker. El día de hoy veremos uno de los conceptos más importantes de Eloquent y que sientan las bases de todo su funcionamiento: las relaciones, en particular las relaciones uno a uno.
Laravel soporta diferentes tipos de relaciones tales como uno a uno, uno a muchos y 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 siguiente problema:
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 creado un modelo User ya que laravel por defecto trae este modelo al crear un nuevo proyecto.
Relaciones uno a uno
Una relación uno a uno entre dos tablas A y B se presenta cuando un registro de A está relacionado con un solo elemento de B. De manera análoga, un registro de B está relacionado con un solo registro en A. Esta es una relación especial que suele presentarse solo en ciertos casos ya que por lo general al normalizar, dicha realación desaparece.
Para el ejemplo que nos acontece, existe una relación uno a uno entre la entidad File y la entidad FileDownload. Como cada archivo es único, existe solo un registro asociado en la tabla de descargas que indica el número total de descargas. Esto es posible porque en el enunciado hemos colocado el texto "con la última fecha en la que fue descargado", de lo contrario, si hubiésemos querido cada fecha de descarga ya no sería una relación uno a uno. el diagrama que modela esta relación es el siguiente:
Nota que como se dijo antes esta relación puede ser normalizada generando una sola tabla como resultado. Para efectos de este tutorial vamos a suponer que la relación es necesaria. Hemos omitido también los timestamps en el modelo anterior a fin de simplificarlo. Dicho esto vamos a modificar las migraciones respectivas para que queden como el 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 CreateFileDownloadsTable
Schema::create('file_downloads', function (Blueprint $table) {
$table->bigIncrements('file_id');
$table->integer('total_downloads');
$table->timestamps();
});
Ahora viene lo interesante. Cómo le comunico a eloquent que existe una relación entre estos dos modelos ?. La respuesta es el método hasOne
. Ejecutemos las migraciones con el comando php artisan migrate y agreguemos el siguiente contenido al 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';
public function fileDownloads()
{
// laravel assumes id as foreign and local key.
//return $this->hasOne('App\FileDownload');
return $this->hasOne('App\FileDownload', 'file_id', 'file_id');
}
}
Con esto le hemos dicho a laravel que cada objeto File tiene relación uno a uno con el objeto FileDownload. 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 File. Observa que si nuestras llaves primarias tanto en la tabla de archivos como de descargas hubiera sido id, no hubiese sido necesario definir los parámetros 2 y 3 del método hasOne
que son respectivamente la llave foránea y local. Vamos con la definición de llave primaria en nuestro modelo FileDownload.
namespace App;
use Illuminate\Database\Eloquent\Model;
class FileDownload extends Model
{
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'file_id';
}
Antes de terminar con lo que laravel denomina la inversa de la relación, vamos a probar nuestra nueva relación con laravel tinker. Creemos entonces un par de archivos en la tabla files y unas cuantas descargas acumuladas.
$file = new \App\File();
$file->file_name = 'Ebook- Linux Basics';
$file->file_type_id = 4;
$file->created_by = 1;
$file->created_at = date('Y-m-d');
$file->save();
$file = new \App\File();
$file->file_name = 'Video - How to do a developer ?';
$file->file_type_id = 2;
$file->created_by = 1;
$file->created_at = date('Y-m-d');
$file->save();
$download = new \App\FileDownload();
$download->file_id = 1;
$download->total_downloads = 10;
$download->save();
$download = new \App\FileDownload();
$download->file_id = 2;
$download->total_downloads = 800;
$download->save();
Lo primero que debemos notar aquí, es que si tú no defines el id para el registro eloquent automáticamente lo calculará y colocará el siguiente consecutivo. Para nuestro caso, creará los registros 1 y 2. Hemos sido un poco más explícitos en la creación de las descargas relacionando el file_id
respectivo a cada registro. En la misma sesión de tinker vamos a realizar la consulta directa a la base de datos para traer el primer archivo y utilizando las propiedades dinámicas de Eloquent vamos a traer el total de descargas.
(new \App\File)->find(1)->fileDownloads;
El resultado de esto sería muy similar al siguiente:
=> App\FileDownload {#2969
file_id: 1,
total_downloads: 10,
created_at: "2019-07-18 22:36:30",
updated_at: "2019-07-18 22:36:30",
}
Observa que el resultado es un objeto FileDownload, con lo cuál podemos acceder directamente a la propiedad y ver el resultado de tinker así:
>>> (new \App\File)->find(1)->fileDownloads->total_downloads;
=> 10
No olvides que los caracteres >>> se muestran en el prompt de tinker y no hacen parte del comando. Ahora bien, vamos a definir ahora si la inversa de la relación. Esto no es estrictamente necesario, pero veremos que facilita enormemente las consultas cuando tenemos un resultado de la tabla descargas y queremos ver su registro relacionado en la tabla de archivos. Para lograr esto, debemos definir el método file()
en la clase FileDownload.
public function file()
{
//return $this->belongsTo('App\File');
return $this->belongsTo('App\File', 'file_id', 'file_id');
}
No hace falta hacer nada más, vamos al tinker y consultamos el segundo registro de descargas y desde este tratemos de traernos el nombre del archivo asociado.
>>> (new \App\FileDownload)->find(2)->file->file_name;
=> "Video - How to do a developer ?"
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 FileDownload.