Relaciones entre modelos
Aprende a definir y usar relaciones Eloquent: hasOne, hasMany, belongsTo, belongsToMany y relaciones polimórficas en Laravel.
Relaciones entre modelos en Eloquent
Una de las funcionalidades más poderosas de Eloquent es su sistema de relaciones. En el mundo real, los datos raramente existen de forma aislada: un usuario tiene muchos posts, un post tiene muchos comentarios, un post puede tener muchas etiquetas y una etiqueta puede pertenecer a muchos posts. Eloquent modela estos vínculos con métodos expresivos que ocultan la complejidad del SQL subyacente.
Tipos de relaciones
Eloquent soporta las relaciones más comunes:
hasOne— Uno a unohasMany— Uno a muchosbelongsTo— Inversa de hasOne/hasManybelongsToMany— Muchos a muchoshasManyThrough— Uno a muchos a través de un intermediariomorphTo/morphMany— Polimórficas
hasOne — Uno a uno
Un usuario tiene un perfil. La clave foránea user_id vive en la tabla perfiles.
// Modelo User
class User extends Model
{
public function perfil(): HasOne
{
return $this->hasOne(Perfil::class);
// Laravel busca perfil.user_id por convención
}
}
// Modelo Perfil
class Perfil extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Uso:
$user = User::find(1);
$perfil = $user->perfil; // objeto Perfil o null
// Crear perfil asociado
$user->perfil()->create([
'bio' => 'Desarrollador Laravel',
'twitter' => '@laravel_dev',
'website' => 'https://ejemplo.com',
]);
hasMany — Uno a muchos
Un usuario tiene muchos posts. La clave foránea user_id vive en la tabla posts.
// Modelo User
class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function postsPublicados(): HasMany
{
return $this->hasMany(Post::class)
->where('estado', 'publicado')
->latest();
}
}
// Modelo Post
class Post extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
Uso:
$user = User::find(1);
$posts = $user->posts; // colección de Posts
$count = $user->posts()->count(); // consulta directa, no carga todos
// Acceder al autor desde el post
$post = Post::find(1);
$autor = $post->user; // objeto User
$nombre = $post->user->name;
belongsTo — La inversa
belongsTo siempre va en el modelo que contiene la clave foránea. En el ejemplo anterior, Post tiene user_id, así que Post “pertenece a” User.
Puedes personalizar la clave foránea si no sigue la convención:
// Si la columna fuera 'autor_id' en lugar de 'user_id'
public function autor(): BelongsTo
{
return $this->belongsTo(User::class, 'autor_id');
}
belongsToMany — Muchos a muchos
Un post puede tener muchas etiquetas, y una etiqueta puede pertenecer a muchos posts. Esto requiere una tabla pivot.
Migración de la tabla pivot:
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
Modelos:
// Modelo Post
class Post extends Model
{
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
// Laravel busca tabla post_tag por convención
}
}
// Modelo Tag
class Tag extends Model
{
public function posts(): BelongsToMany
{
return $this->belongsToMany(Post::class);
}
}
Uso:
$post = Post::find(1);
// Obtener etiquetas
$tags = $post->tags;
// Adjuntar etiquetas
$post->tags()->attach([1, 2, 3]);
// Desvincular etiquetas
$post->tags()->detach([1, 2]);
// Sincronizar (deja solo las indicadas)
$post->tags()->sync([2, 3, 4]);
// Datos extra en la tabla pivot
$post->tags()->attach(1, ['destacado' => true]);
// Acceder a datos del pivot
foreach ($post->tags as $tag) {
echo $tag->pivot->destacado;
}
Para acceder al pivot debes indicarlo en la relación:
return $this->belongsToMany(Tag::class)->withPivot('destacado')->withTimestamps();
hasManyThrough — A través de
Permite acceder a relaciones indirectas. Por ejemplo, obtener todos los comentarios de los posts de un usuario:
// Modelo User
class User extends Model
{
public function comentarios(): HasManyThrough
{
return $this->hasManyThrough(
Comentario::class, // modelo final
Post::class, // modelo intermedio
'user_id', // FK en posts
'post_id', // FK en comentarios
);
}
}
Relaciones polimórficas
Las relaciones polimórficas permiten que un modelo se relacione con múltiples modelos usando una sola relación. Por ejemplo, tanto Post como Video pueden tener Comentario.
Migración:
Schema::create('comentarios', function (Blueprint $table) {
$table->id();
$table->text('cuerpo');
$table->morphs('comentable'); // crea comentable_id y comentable_type
$table->foreignId('user_id')->constrained();
$table->timestamps();
});
Modelos:
class Comentario extends Model
{
public function comentable(): MorphTo
{
return $this->morphTo();
}
}
class Post extends Model
{
public function comentarios(): MorphMany
{
return $this->morphMany(Comentario::class, 'comentable');
}
}
class Video extends Model
{
public function comentarios(): MorphMany
{
return $this->morphMany(Comentario::class, 'comentable');
}
}
Uso:
// Comentar un post
$post->comentarios()->create(['cuerpo' => 'Excelente artículo', 'user_id' => 1]);
// Comentar un video
$video->comentarios()->create(['cuerpo' => 'Gran video', 'user_id' => 1]);
// Obtener el modelo padre desde un comentario
$comentario = Comentario::find(1);
$padre = $comentario->comentable; // devuelve un Post o un Video
Eager Loading — evitar el problema N+1
El problema N+1 ocurre cuando cargas una colección y luego accedes a una relación dentro de un bucle, generando una consulta extra por cada elemento:
// MAL: genera 1 + N consultas (1 para posts, N para cada user)
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // consulta extra por cada iteración
}
La solución es Eager Loading con with():
// BIEN: genera solo 2 consultas (posts + users en batch)
$posts = Post::with('user')->get();
// Cargar múltiples relaciones
$posts = Post::with(['user', 'tags', 'comentarios'])->get();
// Relaciones anidadas
$posts = Post::with('user.perfil')->get();
// Eager loading condicional
$posts = Post::with(['comentarios' => function ($query) {
$query->where('aprobado', true)->latest();
}])->get();
Lazy Eager Loading
Si ya tienes una colección cargada y necesitas cargar relaciones después:
$posts = Post::all();
// Cargar relación sobre una colección ya existente
$posts->load('user');
$posts->load(['user', 'tags']);
Contar relaciones sin cargarlas
// withCount agrega columna {relacion}_count al modelo
$posts = Post::withCount('comentarios')->get();
foreach ($posts as $post) {
echo $post->comentarios_count;
}
Insertar a través de relaciones
$user = User::find(1);
// Crear un post asociado al usuario
$post = $user->posts()->create([
'titulo' => 'Nuevo post',
'slug' => 'nuevo-post',
'contenido' => 'Contenido...',
]);
// Asociar un modelo existente
$post = Post::find(5);
$user->posts()->save($post);
Las relaciones de Eloquent son una de las razones por las que Laravel es tan productivo. Con pocas líneas de código puedes navegar estructuras de datos complejas de forma legible y eficiente.
Pon a prueba lo aprendido
Responde las preguntas para comprobar que has entendido los conceptos clave.
1. ¿Qué relación Eloquent usarías para representar que un Post pertenece a un User?
2. ¿Cómo se llama la técnica para cargar relaciones en la misma consulta y evitar el problema N+1?
3. ¿Qué tabla requiere una relación belongsToMany entre Posts y Tags?