A menudo llega un momento en la vida de todo desarrollador en el que tienes que interactuar con una base de datos. Aquí es donde Eloquent, el mapeador objeto-relacional (ORM) de Laravel, hace que el proceso de interactuar con las tablas de tu base de datos sea intuitivo y natural.

Es vital que, como profesional, reconozcas y comprendas los seis tipos de relaciones clave que vamos a repasar.

¿Qué Son las Relaciones en Eloquent?

Cuando trabajamos con tablas en una base de datos relacional, podemos caracterizar las relaciones como conexiones entre tablas. Esto te ayuda a organizar y estructurar los datos sin esfuerzo, lo que permite una mayor legibilidad y manejo de los datos. En la práctica, existen tres tipos de relaciones en las bases de datos:

  • uno a uno – Un registro de una tabla se asocia con uno, y sólo uno, de otra tabla. Por ejemplo, una persona y un número de la Seguridad Social.
  • uno a varios – Un registro está asociado a varios registros de otra tabla. Por ejemplo, un escritor y sus blogs.
  • varios-a-varios – Varios registros de una tabla están asociados a varios registros de otra tabla. Por ejemplo, los alumnos y los cursos en los que están matriculados.

Laravel facilita la interacción y la gestión de las relaciones entre bases de datos utilizando la sintaxis orientada a objetos de Eloquent.

Junto con estas definiciones, Laravel introduce más relaciones, a saber:

  • Has Many Through
  • Relaciones Polimórficas
  • Polimórficas de Varios a Varios

Tomemos, por ejemplo, una tienda cuyo inventario contiene diversos artículos, cada uno en su propia categoría. Por tanto, dividir la base de datos en varias tablas tiene sentido desde el punto de vista empresarial. Esto conlleva sus propios problemas, ya que no quieres consultar todas y cada una de las tablas.

Podemos crear fácilmente una relación simple de uno a muchos en Laravel para ayudarnos, por ejemplo, cuando necesitemos consultar los productos, podemos hacerlo utilizando el modelo Producto.

Esquema de base de datos con tres tablas y una tabla conjunta que representa una relación polimórfica
Esquema de base de datos con tres tablas y una tabla conjunta que representa una relación polimórfica

Relación Uno a Uno

Al ser la primera relación básica que ofrece Laravel, asocian dos tablas de forma que una fila de la primera tabla está correlacionada con una sola fila de la otra tabla.

Para ver esto en acción, tenemos que crear dos modelos con su propia migración:

php artisan make:model Tenant 
Php artisan make:model Rent

En este punto, tenemos dos modelos, uno es el «Tenant» (Inquilino) y el otro es su » Rent» (Alquiler).

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * Get the rent of a Tenant
    */
    public function rent() 
    {
        return $this->hasOne(Rent::class);
    }
}

Como eloquent determina la relación de clave ajena basándose en el nombre del modelo padre (Inquilino en este caso), el modelo Alquiler asume que existe una clave ajena tenant_id.

Podemos sobrescribirla fácilmente como con un argumento adicional al método hasOne:

return $this- >hasOne(Rent::class, "custom_key");

Eloquent también asume que existe una coincidencia entre la clave ajena definida y la clave primaria del padre (modelo Inquilino). Por defecto, buscará la coincidencia de tenant_id con la clave id del registro Tenant. Podemos sobrescribir esto con un tercer argumento en el método hasOne, de forma que coincida con otra clave:

return $this->hasOne(Rent::class, "custom_key", "other_key"); 

Ahora que hemos definido la relación uno a uno entre los modelos, podemos utilizarla fácilmente, así:

$rent = Tenant::find(10)->rent;

Con esta línea de código, obtenemos el alquiler del inquilino con el id 10 si existe.

Relación Uno a Varios

Al igual que la relación anterior, ésta definirá relaciones entre un modelo monoparental y varios modelos hijos. Es poco probable que nuestro Inquilino tenga sólo una factura de Alquiler porque es un pago recurrente, por lo tanto, tendrá múltiples pagos.

En este caso, nuestra relación anterior tiene defectos, y podemos solucionarlos:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * Get the rents of a Tenant
    */
    public function rent() 
    {
        return $this->hasMany(Rent::class);
    }
}

Antes de llamar al método para obtener los alquileres, conviene saber que las relaciones sirven como constructores de consultas, por lo que podemos añadir más restricciones (como alquiler entre fechas, pago mínimo, etc.) y encadenarlas para obtener nuestro resultado deseado:

$rents = Tenant::find(10)->rent()->where('payment', '>', 500)->first();

Y como en la relación anterior, podemos sobrescribir las claves foráneas y locales pasando argumentos adicionales:

return $this->hasMany(Rent::class, "foreign_key");
return $this->hasMany(Rent::class, "foreign_key", "local_key");

Ahora tenemos todos los alquileres de un inquilino, pero ¿qué hacemos cuando conocemos el alquiler y queremos averiguar a quién pertenece? Podemos hacer uso de la propiedad belongsTo:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * Return the tenant for the rent
    */
    public function tenant() 
    {
        return $this->belongsTo(Tenant::class);
    }
}

Y ahora podemos obtener el inquilino fácilmente:

$tenant = Rent::find(1)->tenant;

Para el método belongsTo, también podemos sobrescribir las claves foráneas y locales como hicimos antes.

Relación Has-One-Of-Many

Como nuestro modelo de Inquilino puede estar asociado a muchos modelos de Alquiler, queremos recuperar fácilmente el modelo relacionado más reciente o más antiguo de las relaciones.

Una forma cómoda de hacerlo es combinar los métodos hasOne y ofMany:

public function latestRent() {
    return $this->hasOne(Rent::class)->latestOfMany();
}

public function oldestRent() {
    return $this->hasOne(Rent::class)->oldestOfMany();
}

Por defecto, obtenemos los datos basándonos en la clave primaria, que es ordenable, pero podemos crear nuestros propios filtros para el método ofMany:

return $this->hasOne(Rent::class)->ofMany('price', 'min');

Relaciones HasOneThrough y HasManyThrough

Los métodos -Through sugieren que nuestros modelos tendrán que pasar por otro modelo para establecer una relación con el modelo deseado. Por ejemplo, podemos asociar el Alquiler con el Arrendador, pero el Alquiler debe pasar primero por el Arrendatario para llegar al Arrendador.

Las claves de las tablas necesarias para ello tendrían el siguiente aspecto:

rent
    id - integer
    name - string
    value - double

tenants
    id - integer
    name - string
    rent_id - integer

landlord
    id - integer
    name - string
    tenant_id - integer

Después de visualizar el aspecto de nuestras tablas, podemos hacer los modelos:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * Return the rents' landlord
    */
    public function rentLandlord() 
    {
        return $this->hasOneThrough(Landlord::class, Tenant::class);
    }
}

El primer argumento del método hasOneThrough es el modelo al que quieres acceder, y el segundo argumento es el modelo por el que pasarás.

Y al igual que antes, puedes sobrescribir las claves foráneas y locales. Ahora que tenemos dos modelos, tenemos dos de cada para sobrescribir en este orden:

public function rentLandlord() 
{
    return $this->hasOneThrough(
        Landlord::class,
        Tenant::class,
        "rent_id",    // Foreign key on the tenant table
        "tenant_id",  // Foreign key on the landlord table
        "id",         // Local key on the tenant class
        "id"          // Local key on the tenant table
    );
}

Del mismo modo, la relación «Has Many Through» de Laravel Eloquent es útil cuando quieres acceder a registros de una tabla lejana a través de una tabla intermedia. Consideremos un ejemplo con tres tablas:

  • país
  • usuarios
  • juegos

Cada País tiene muchos Usuarios, y cada Usuario tiene muchos Juegos. Queremos recuperar todos los Juegos pertenecientes a un País a través de la tabla Usuario.

Definirías las tablas así

country
    id - integer
    name - string

user
    id - integer
    country_id - integer
    name - string

games
    id - integer
    user_id - integer
    title - string

Ahora debes definir el modelo Eloquent para cada una de las tablas:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    protected $fillable = ['name'];

    public function users()
    {
        return $this->hasMany(User::class);
    }

    public function games()
    {
        return $this->hasManyThrough(Games::class, User::class);
    }
}
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = [article_id, 'name'];

    public function country()
    {
        return $this->belongsTo(Country::class);
    }

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Game extends Model
{
    protected $fillable = ['user_id', 'title'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Ahora podemos llamar al método games() del modelo País para obtener todos los juegos porque hemos establecido la relación «Has Many Through» entre País y Juego a través del modelo Usuario.

<?php

$country = Country::find(159);
            
// Retrieve all games for the country
$games = $country->games;

Relación Varios-a-Varios

La relación varios-a-varios es más complicada. Un buen ejemplo es un empleado que tiene varios roles. Un rol también puede asignarse a varios empleados. Ésta es la base de la relación de muchos a muchos.

Para ello, debemos tener las tablas employeesrolesrole_employees.

La estructura de las tablas de nuestra base de datos será la siguiente

employees
    id - integer
    name - string

roles 
    id - integer
    name - string

role_employees
    user_id - integer
    role_id - integer

Conociendo la estructura de tablas de la relación, podemos definir fácilmente nuestro modelo de Employee para belongsToMany Role model.

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Employee extends Model
{
    public function roles() 
    {
        return $this- >belongsToMany(Role::class);
    }
}

Una vez definido esto, podemos acceder a todos los roles de un empleado e incluso filtrarlos:

$employee = Employee::find(1);
$employee->roles->forEach(function($role) { // });

// OR 

$employee = Employee::find(1)->roles()->orderBy('name')->where('name', 'admin')->get();

Como todos los demás métodos, podemos sobrescribir las claves foráneas y locales del método belongsToMany.

Para definir la relación inversa del belongsToMany simplemente utilizamos el mismo método pero sobre el método hijo ahora, con el padre como argumento.

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    public function employees() 
    {
        return $this->belongsToMany(Employee::class);
    }
}

Usos de la tabla intermedia

Como habrás notado, cuando utilizamos la relación varios-a-varios, siempre se supone que tenemos una tabla intermedia. En este caso, estamos utilizando la tabla role_employees.

Por defecto, nuestra tabla pivotante contendrá sólo los atributos id. Si queremos otros atributos, tenemos que especificarlos así:

return $this->belongsToMany(Employee::class)->withPivot("active", "created_at");

Si queremos abreviar el pivote para las marcas de tiempo, podemos hacerlo:

return $this->belongsToMany(Employee::class)->withTimestamps();

Un truco que hay que saber es que podemos personalizar el nombre «pivote» en cualquier cosa que se adapte mejor a nuestra aplicación:

return $this->belongsToMany(Employee::class)->as('subscription')->withPivot("active", "created_by");

Filtrar los resultados de una consulta elocuente es un conocimiento imprescindible para cualquier desarrollador que quiera dar un paso adelante y optimizar sus aplicaciones Laravel.

Por lo tanto, Laravel proporciona una fantástica característica de pivotes que se puede utilizar para filtrar los datos que queremos recopilar. Así, en lugar de utilizar otras funciones como las transacciones de base de datos para obtener nuestros datos a trozos, podemos filtrarlos con métodos útiles como wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNull, wherePivotNotNull ¡y podemos utilizarlos al definir relaciones entre tablas!

return $this->belongsToMany(Employee::class)->wherePivot('promoted', 1);
return $this->belongsToMany(Employee::class)->wherePivotIn('level', [1, 2]);
return $this->belongsToMany(Employee::class)->wherePivotNotIn('level', [2, 3]);
return $this->belongsToMany(Employee::class)->wherePivotBetween('posted_at', ['2023-01-01 00:00:00', '2023-01-02 00:00:00']);
return $this->belongsToMany(Employee::class)->wherePivotNull('expired_at');
return $this->belongsToMany(Employee::class)->wherePivotNotNull('posted_at');

Una última característica sorprendente es que podemos ordenar por pivotes:

return $this->belongsToMany(Employee::class)
        ->where('promoted', true)
        ->orderByPivot('hired_at', 'desc');

Relaciones Polimórficas

La palabra Polimórfico viene del griego, y significa «muchas formas» Así, un modelo de nuestra aplicación puede adoptar muchas formas, es decir, puede tener más de una asociación. Imagina que estamos construyendo una aplicación con blogs, vídeos, encuestas, etc. Un usuario puede crear un comentario para cualquiera de ellos. Por tanto, un modelo Comentario podría pertenecer a los modelos Blogs, Vídeos y Encuestas.

Polimórfico Uno a Uno

Este tipo de relación es similar a una relación estándar uno a uno. La única diferencia es que el modelo hijo puede pertenecer a más de un tipo de modelo con una única asociación.

Tomemos, por ejemplo, un modelo de Tenant y Landlord, puede compartir una relación polimórfica con un modelo de WaterBill.

La estructura de la tabla puede ser la siguiente:

tenants
    id – integer
    name – string

landlords
    id – integer
    name – string

waterbills
    id – integer
    amount – double
    waterbillable_id
    waterbillable_type

Utilizamos waterbillable_id para el id del landlord o tenant, mientras que waterbillable_type contiene el nombre de la clase del modelo padre. La columna tipo la utiliza eloquent para averiguar qué modelo padre debe devolver.

La definición del modelo para una relación de este tipo tendrá el siguiente aspecto:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class WaterBill extends Model
{
    public function billable()
    {
        return $this->morphTo();
    }
}

class Tenant extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

class Landlord extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

Una vez que tengamos todo esto en su sitio, podremos acceder a los datos tanto del modelo Propietario como del modelo Inquilino:

<?php

$tenant = Tenant::find(1)->waterBill;
$landlord = Landlord::find(1)->waterBill;

Polimórfico Uno a Varios

Es similar a una relación normal de uno a varios, la única diferencia clave es que el modelo hijo puede pertenecer a más de un tipo de modelo, utilizando una única asociación.

En una aplicación como Facebook, los usuarios pueden comentar publicaciones, vídeos, encuestas, directos, etc. Con un uno a muchos polimórfico, podemos utilizar una única tabla de comentarios para almacenar los comentarios de todas las categorías que tengamos. La estructura de nuestra tabla sería algo así

posts 
    id – integer
    title – string
    body – text

videos
    id – integer
    title – string
    url – string

polls
    id – integer
    title – string

comments 
    id – integer
    body – text
    commentable_id – integer
    commentable_type – string

El commentable_id siendo el id del registro, y el commentable_type siendo el tipo de clase, para que eloquent sepa qué buscar. En cuanto a la estructura del modelo, es muy similar al polimórfico uno a muchos:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model 
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Poll extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Live extends Model
{
    public function comments()
    {
        return $this->morphMany(Comments::class, 'commentable');
    }
}

Ahora, para recuperar los comentarios de un Vivo, basta con llamar al método encontrar con el id, y ya tenemos acceso a la clase iterable comentarios:

<?php

use App\Models\Live;

$live = Live::find(1);

foreach ($live->comments as $comment) { }

// OR

Live::find(1)->comments()->each(function($comment) { // });
Live::find(1)->comments()->map(function($comment) { // });
Live::find(1)->comments()->filter(function($comment) { // });

// etc.

Y si tenemos el comentario y queremos averiguar a quién pertenece, accedemos al método commentable:

<?php

use App\Models\Comment;

$comment = Comment::find(10);
$commentable = $comment->commentable;

// commentable – type of Post, Video, Poll, Live

Polimórfico Uno a Varios

En muchas aplicaciones que escalan, queremos una forma fácil de interactuar con los modelos y entre ellos. Podemos querer el primer o el último comentario de un usuario, lo que puede hacerse con una combinación de métodos morphOne y ofMany:

<?php

public function latestPost()
{
    return $this->morphOne(Post::class, 'postable')->latestOfMany();
}

public function oldestPost()
{
    return $this->morphOne(Post::class, 'postable')->oldestOfMany();
}

Los métodos latestOfMany y oldestOfMany recuperan el modelo más reciente o más antiguo basándose en la clave primaria del modelo, que era la condición de que fuera ordenable.

En algunos casos, no queremos ordenar por el ID, tal vez cambiamos la fecha de publicación de algunas entradas y las queremos en ese orden, no por su id.

Para ello podemos pasar 2 parámetros al método ofMany. El primer parámetro es la clave por la que queremos filtrar, y el segundo es el método de ordenación:

<?php

public function latestPublishedPost()
{
    return $this->morphOne(Post::class, "postable")->ofMany("published_at", "max");
}

Teniendo esto en cuenta, ¡es posible construir relaciones más avanzadas para esto! Imagina que tenemos este escenario. Se nos pide que generemos una lista de todas las entradas actuales en el orden en que han sido publicadas. El problema surge cuando tenemos 2 entradas con el mismo valor de published_at y cuando las entradas están programadas para publicarse en el futuro.

Para ello, podemos pasar al método ofMany el orden en el que queremos que se apliquen los filtros. De esta forma ordenamos por published_at, y si son iguales, ordenamos por id. En segundo lugar, podemos aplicar una función de consulta al método ofMany para excluir todas las entradas cuya publicación esté programada

<?php

public function currentPosts()
{
    return $this->hasOne(Post::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

Polimórfico Varios a Varios

El varios-a-varios polimórfico es ligeramente más complejo que el normal. Una situación común es tener etiquetas que se aplican a más activos en tu aplicación. Por ejemplo, en TikTok, tenemos etiquetas que se pueden aplicar a Vídeos, Cortos, Historias, etc.

El polimórfico varios-a-varios nos permite tener una única tabla de etiquetas asociadas a los Vídeos, Cortos e Historias.

La estructura de la tabla es sencilla:

videos
    id – integer
    description – string

stories 
    id – integer
    description – string

taggables 
    tag_id – integer
    taggable_id – integer
    taggable_type – string

Con las tablas listas, podemos hacer el modelo y utilizar el método morphToMany. Este método acepta el nombre de la clase del modelo y el «nombre de la relación»:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Video extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

Y con esto, podemos definir fácilmente la relación inversa. Sabemos que para cada modelo hijo queremos llamar al método morphedByMany:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    public function stories()
    {
        return $this->morphedByMany(Story::class, 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    } 
}

Y ahora, cuando obtengamos una etiqueta, ¡podremos recuperar todos los vídeos e historias relacionados con esa etiqueta!

<?php
use App\Model\Tag;

$tag = Tag::find(10);
$posts = $tag->stories;
$videos = $tag->stories;

Optimiza la velocidad de Eloquent

Cuando trabajes con el ORM Eloquent de Laravel, es esencial que sepas cómo optimizar las consultas a la base de datos y minimizar la cantidad de tiempo y memoria que requiere la obtención de datos. Una forma de hacerlo es implementando el almacenamiento en caché en tu aplicación.

Laravel proporciona un sistema de almacenamiento en caché flexible que admite varios backends, como Redis, Memcached y almacenamiento en caché basado en archivos. Al almacenar en caché los resultados de las consultas de Eloquent, puedes reducir el número de consultas a la base de datos, haciendo que tu aplicación sea más rápida y valiosa.

Además, puedes utilizar el constructor de consultas de Laravel para crear consultas complejas adicionales, optimizando aún más el rendimiento de tu aplicación.

Resumen

En conclusión, las relaciones Eloquent son una potente característica de Laravel que permite a los desarrolladores trabajar fácilmente con datos relacionados. Desde relaciones uno a uno hasta relaciones varios a varios, Eloquent proporciona una sintaxis sencilla e intuitiva para definir y consultar estas relaciones.

Como desarrollador de Laravel, dominar las relaciones Eloquent puede mejorar enormemente tu flujo de trabajo de desarrollo y hacer que tu código sea más eficiente y legible. Si estás interesado en aprender más sobre Laravel, Kinsta tiene varios recursos disponibles, incluido un tutorial sobre cómo empezar con Laravel y un artículo sobre los salarios de los desarrolladores de Laravel.

Kinsta ofrece soluciones de alojamiento gestionado que facilitan el despliegue y la gestión de aplicaciones Laravel.

Coman Cosmin

Cosmin Coman es un escritor y desarrollador tecnológico con más de 3 años de experiencia. Además de escribir para Kinsta, ha colaborado en investigaciones en instalaciones de física nuclear y universidades. Experto en tecnología e integrado en la comunidad, siempre aporta soluciones innovadoras.