Relaciones en Laravel Eloquent: la guía completa 2022

Relaciones en Laravel Eloquent: la guía completa 2022

Artículo actualizado el 20 de febrero de 2022 con la nueva versión Laravel 9

Laravel Eloquent es una de las funcionalidades estrella del framework Laravel, en buena parte por lo bien que resuelve la definición, la creación y la gestión de las relaciones entre tablas. En este tutorial vas a ver cómo crear y usar las relaciones de Eloquent partiendo de cero, sin conocimientos previos, también te recomiendo el tutorial de Laravel 9 para principiantes, que propone otro enfoque para aprender Laravel con Eloquent.

¿Qué es una relación en una base de datos?

Empecemos por lo más básico. ¿Qué es exactamente una relación? Una relación significa que tienes dos tablas o más cuyos registros están vinculados entre sí. Imagina una tabla users en la que cada usuario puede tener varios posts. ¿Cómo se conectan las dos? Lo habitual es añadir una columna user_id a la tabla posts, para saber sin esfuerzo a qué usuario pertenece cada publicación. Esa «conexión», que te permite saber qué registros van juntos, es lo que se llama una relación.

¿Qué tipos de relaciones Eloquent existen?

Existen varios tipos de relaciones Eloquent. El tipo que necesitas depende de si tienes uno o (potencialmente) varios elementos en la primera y en la segunda tabla.

En el ejemplo anterior tenemos una tabla users y una tabla posts. Digamos que cada publicación solo puede pertenecer a un usuario. Para implementarlo basta con añadir una columna user_id al modelo Post, es decir a la tabla posts. En el escenario opuesto, mucho menos habitual, si cada publicación pudiera tener varios autores (users) y cada autor contribuir a una sola publicación, añadiríamos una columna post_id a la tabla users. Es un ejemplo de relación bastante simple, una relación One-To-Many.

¿Y qué pasa si cada usuario puede tener varias publicaciones y cada publicación varios autores? ¿Cómo lo resolvemos? Eso es lo que se llama una relación Many-To-Many. Para ese tipo de relación, más compleja, no basta con añadir una columna a una tabla y listo: hace falta una tabla pivote. No te preocupes si suena complicado, lo vemos enseguida.

En general se distinguen los siguientes tipos de relaciones entre datos:

  1. Relación «uno a uno» (One-To-One)
  2. Relación «uno a muchos» (One-To-Many)
  3. Relaciones Has-One-Of-Many (por ejemplo, el último de entre muchos)
  4. Relaciones HasOneThrough y HasManyThrough
  5. Relación Many-To-Many

Relaciones uno a uno con Eloquent

Empecemos por la relación más simple: la relación «Has One» o «One-To-One». Significa que un registro concreto está vinculado a otro registro, y solo a uno.

Sigamos con el ejemplo del blog, con usuarios y publicaciones: digamos que cada usuario puede tener un perfil. En algunos casos podrías guardar toda la información del perfil en el modelo User, pero no sería lo ideal. Aquí quiero guardarla en una tabla aparte. Si más adelante queremos transferir un perfil a otro usuario, lo agradeceremos.

Primero crea un modelo Profile, el modelo User ya viene generado por defecto. Las columnas exactas del modelo Profile dan bastante igual, pero lo cómodo es crear el modelo y su migración de una vez:

shell
php artisan make:model Profile -m

Como es una relación uno a uno, tenemos la rara oportunidad de elegir entre dos opciones:

  1. poner una columna user_id en el modelo Profile,
  2. o añadir una columna profile_id en el modelo User.

Puedes elegir la que prefieras, pero ten en cuenta que la relación describe un User que tiene un Profile. Y cuando algo tiene otra cosa, esa columna suele ir en el segundo modelo. Podríamos decir que el primer modelo «posee» al segundo.

Así que eso es lo que voy a hacer:

php
/** Añade esto a tu migración de la tabla de perfiles o crea una nueva migración */
$table->foreignId('user_id');

Lanza las migraciones y ya podemos ponernos a implementar la relación.

Abre el modelo User y añade el siguiente método público. Puedes llamarlo como quieras, pero la convención es el snake_case:

php
public function profile()
{
    return $this->hasOne(Profile::class);
}

A partir de ahí, hasOne espera que el método profile() se apoye en la columna user_id del modelo Profile. Si tu columna se llama de otra forma, pasa un segundo argumento a hasOne con ese nombre:

php
return $this->hasOne(Profile::class, 'author_id');

Dicho esto, salirse de la convención rara vez compensa.

Con esto ya has añadido tu primera relación Eloquent. Sigue bajando para aprender a usarla👌.

Añadir la relación inversa

En la mayoría de los casos también podemos definir la inversa de una relación. Suena complicado, pero no lo es: consiste en añadir en el modelo Profile un método que devuelva el modelo al que pertenece (User, en este caso). En tu modelo Profile:

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

Usar una relación Eloquent

Usar esta relación One-To-One de Eloquent es muy sencillo. Siempre que tengas una instancia de User puedes llamar a $user->profile o a $user->profile()->enchainerDautreMethodeIci().

Funciona porque $user es una instancia de la clase User y en ese modelo hemos añadido el método de relación profile().

Fíjate bien, porque esto es importante: puedes acceder tanto a ->profile, como si fuera una propiedad, como a ->profile(), como si fuera un método. La diferencia es que ->profile devuelve directamente una instancia de Eloquent, mientras que ->profile() te deja encadenar más métodos. Por ejemplo ->profile()->orderBy('xxx', 'ASC')->get().

En resumen, ->profile es un atajo de ->profile()->get().

php
$user = auth()->user();

$profile = $user->profile;
$name = $user->profile->display_name;

// Crear un perfil
$profile = $user->profile()->create([
   //
]);

$user = Profile::find(1)->user;

Relación One-To-Many con Eloquent

Otra relación importantísima, quizá la que más, es la relación uno a muchos. También se la conoce como relación hasMany, y describe que un elemento tiene muchos otros elementos. Se parece mucho a la anterior.

Siguiendo con el ejemplo del blog, digamos que un perfil tiene varias publicaciones. Abre el modelo Profile y añade este método:

php
public function posts()
{
    return $this->hasMany(Post::class);
    //O return $this->hasMany(Post::class, 'foreign_key');
}

Eso significa que cada perfil tiene muchas publicaciones. La inversa también existe:

php
public function profile()
{
    return $this->belongsTo(Profile::class);
    //O return $this->belongsTo(Profile::class, 'foreign_key');
}

Se usa igual que lo anterior, salvo que esta relación devuelve varios elementos, en forma de colección Eloquent.

Ahora que la relación está definida como método en el modelo, podemos volver a usarla como propiedad (->posts), que aquí devuelve una colección de Eloquent en lugar de un único modelo. Y también como método (->posts()->where('created_at', '>', now()->subDays(14))->get()).

php
$posts = Profile::find(1)->posts;
//O $posts = Profile::find(1)->posts()->get();

foreach ($posts as post) {
   // TODO
}

$lastPost = Profile::find(1)->posts()->latest()->first();

Recuperar el modelo más reciente o el más antiguo de una relación

A veces, al definir una relación hasMany, solo te interesa recuperar el modelo más reciente o el más antiguo. Para eso Laravel ofrece dos métodos muy cómodos, ->latestOfMany() y ->oldestOfMany():

php
public function latestPost()
{
    return $this->hasMany(Post::class)->latestOfMany();
}
php
public function oldestPost()
{
    return $this->hasMany(Post::class)->oldestOfMany();
}

Estos dos métodos devuelven un solo modelo, no una colección.

php
$latestPost = Profile::find(1)->latestPost;

Si te encaja pero necesitas filtros personalizados u otras cláusulas where() más avanzadas, echa un vistazo a este ejemplo de la documentación de Laravel:

php
/**
 * Obtén el precio actual del producto
 */
public function currentPricing()
{
    return $this->hasOne(Price::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

HasOneThrough y HasManyThrough

Vistos estos ejemplos, vamos un poco más lejos: ahora queremos definir una relación a través de otro modelo.

Volvamos al ejemplo anterior, donde cada User tiene un Profile y cada Profile tiene muchos Post. Un caso de relación a través de otro modelo es querer obtener todas las publicaciones de un usuario. No podemos añadir sin más un hasMany(Post::class) al modelo User, porque el modelo Post no tiene ninguna columna user_id: solo tiene profile_id. Es el ejemplo perfecto de una relación has many through.

Definir una relación así se parece mucho a lo que ya hemos visto. En nuestro ejemplo, añade esto al modelo User:

php
public function posts()
{
    return $this->hasManyThrough(Post::class, Profile::class);
}

El primer argumento es el modelo al que queremos llegar y el segundo es el modelo intermedio.

Definir una relación hasOneThough es casi idéntico a una hasMany(), salvo que aquí debes asegurarte de que solo hay un elemento final:

php
public function first_login()
{
    return $this->hasOneThrough(FirstLogin::class, Profile::class);
}

Relaciones muchos a muchos

Vistas las relaciones anteriores, pasamos a las relaciones más avanzadas. Una relación muchos a muchos es aquella en la que cada User puede tener varios Profile, por ejemplo cuando se colabora con otras personas, y cada Profile puede tener varios User.

¿Cómo se resuelve? No podemos añadir sin más una columna profile_id a la tabla de usuarios, porque hay potencialmente varios perfiles. Y tampoco podemos añadir un user_id al modelo Profile, porque hay potencialmente varios usuarios.

Para resolverlo necesitamos una especie de tabla intermedia. Esa tabla intermedia se llama tabla pivote y, en la mayoría de los casos, no hace otra cosa que guardar dos id en la misma fila.

Imagina el escenario: si nuestra tabla pivote tiene una columna user_id y otra profile_id, ya podemos vincular un User concreto con un Profile concreto. Y como el mismo user_id puede aparecer tantas veces como queramos, un mismo usuario puede quedar unido a tantos profile_id distintos como haga falta. Con los perfiles pasa lo mismo: el mismo profile_id puede repetirse con tantos user_id diferentes como quieras.

¿Cómo crear la migración de una tabla pivote en Laravel?

Primero vamos a crear la migración que necesitamos. Ejecuta el siguiente comando y fíjate en cómo se construye el nombre de la tabla: uniendo los nombres de las dos tablas, ambos en singular y en orden alfabético. Puedes saltarte esa convención, pero rara vez merece la pena.

shell
php artisan make:migration create_profile_user_table --create=profile_user

Ahora abre el archivo de migración y añade dos líneas al método up().

php
<?php

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

class CreateProfileUserTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('profile_user', function (Blueprint $table) {
            $table->id();
            $table->timestamps();

$table->foreignId('profile_id');
            $table->foreignId('user_id');
        });
    }

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

Como ves, la estructura de una tabla pivote es muy simple. Ejecuta ahora las migraciones.

Después abrimos los dos modelos y añadimos los dos métodos de relación. En ambos modelos el método tiene la misma forma: belongsToMany().

Añade esto a tu modelo User:

php
/**
 * Función pública para obtener todos los perfiles bajo los que este usuario puede publicar
 */
public function profiles()
{
    return $this->belongsToMany(Profile::class);
}

Y esto a tu modelo Profile:

php
/**
 *  Función pública para obtener todos los usuarios que pueden publicar bajo este perfil
 */
public function users()
{
    return $this->belongsToMany(User::class);
}

Si eliges otro nombre de tabla distinto del que Laravel deduce por defecto, pásalo como parámetro a los métodos belongsToMany:

php
return $this->belongsToMany(Profile::class, 'user_role'); // Aquí la tabla está en orden alfabético inverso

Por defecto, Eloquent espera las columnas user_id y profile_id en la tabla pivote. Si prefieres nombrarlas de otra forma, pásalas como tercer y cuarto parámetro:

php
return $this->belongsToMany(Profile::class, 'user_role', 'nom_de_la_colonne_qui_represente_l_identifiant_de_ce_modele', 'nom_de_la_colonne_qui_represente_l_identifiant_de_la_cle_etrangere');

// Parece innecesariamente complicado, así que aquí va un ejemplo.
// Digamos que añadimos esto en el modelo User; por defecto quedaría así:
return $this->belongsToMany(Profile::class, 'user_role', 'user_id', 'profile_id');

Usar una relación Many-To-Many de Eloquent

Usar esta relación es muy fácil y resulta parecido a lo que hemos visto antes. Mira estos ejemplos:

php
$user = User::find(1);

$user->profiles->each(function ($item) use ($user) {

$profile->name = $user->firstname . ' ' . $user->lastname;
   $profile->save();

});
php
$profile = Profile::find(1);

$profile->users()->orderBy('created_at', 'desc');

Guardar datos en la tabla pivote

La última parte de este artículo trata de guardar datos en la tabla pivote. Sí, también se puede. Puede que no lo necesites en todos los casos, pero hay situaciones en las que viene muy bien.

Recapitulemos: ¿qué es una fila en una tabla pivote? ¿Qué representa? Una fila de la tabla pivote representa la relación entre dos registros. Si borráramos esa fila, ¿seguiría existiendo la relación? No.

Y aquí está lo interesante. Si cada fila de la tabla pivote representa una relación, entonces también podemos guardar información sobre esa relación en la propia tabla pivote. Puede ser algo tan trivial como cuándo se creó la relación o cuándo se actualizó por última vez, con las columnas created_at y updated_at.

Recuperar datos de la relación desde la tabla pivote

En el ejemplo anterior ya escribimos la migración de la tabla pivote. Esa migración incluía las columnas created_at y updated_at gracias a $table->timestamps();. ¿Cómo accedemos a esos datos?

Primero hay que avisar a Eloquent: «mi modelo pivote tiene además estos atributos y quiero poder acceder a ellos».

Se hace así:

php
/**
 * Función pública para obtener todos los perfiles bajo los que este usuario puede publicar
 */
public function profiles()
{
    return $this->belongsToMany(Profile::class)->withPivot('created_at', 'updated_at', 'active');
}

O usar el práctico helper ->withTimestamps(), que ya declara las columnas created_at y updated_at:

php
/**
 * Función pública para obtener todos los perfiles bajo los que este usuario puede publicar
 */
public function profiles()
{
    return $this->belongsToMany(Profile::class)->withTimestamps()->withPivot('active');
}

Ya puedes acceder a la tabla pivote así:

php
$user = User::find(1);

foreach ($user->profiles as $profile) {
   echo $profile->pivot->created_at;
}

Cómodo, ¿verdad?

Renombrar el atributo pivot

Hay otro truco interesante: renombrar el atributo pivot. Eloquent apuesta por un lenguaje descriptivo y expresivo. La documentación pone el ejemplo de una relación entre un podcast y un usuario: esa relación es una suscripción, así que llamarla «pivot» queda raro.

php
// Mal:
$user->podcasts()->first()->pivot->price;

// Bien:
$user->podcasts()->first()->subscription->price;

Para renombrar el atributo pivot, encadena el método ->as($name) al return $this->belongsToMany():

php
return $this->belongsToMany(Podcast::class)
                ->as('subscription')
                ->withTimestamps();

Conclusión

Menudo artículo largo. Si has llegado hasta aquí, gracias por acompañarme. Hemos repasado todos los tipos de relaciones entre modelos Eloquent. Espero que te sirva en tu camino para dominarlas.

Las relaciones que has visto aquí son las que más vas a usar. En mi opinión, con ellas tienes de sobra por ahora.

Aun así, te encontrarás con casos en los que estas relaciones se quedan cortas. Hasta ahora siempre hemos hablado de la relación entre un modelo y otro modelo. Pero piensa en una situación bastante habitual: poder etiquetar tanto un Post como una Page. Ahí las relaciones anteriores no bastan, porque no hemos visto las relaciones polimórficas, ya escribí una introducción a las relaciones polimórficas de Eloquent, te invito a leerla para profundizar en el tema

Laravel

Damien Flandrin Desarrollador web desde 2010, creador de Gekkode y de Email Impact. Cada artículo se prueba en un proyecto real antes de publicarse. Contacto
Newsletter

Las nuevas pruebas, tutoriales y proyectos, por correo.

Pruebas reproducibles, código versionado, resultados fechados. Nunca spam.