Relaciones Muchos a Muchos
Las relaciones muchos a muchos conectan múltiples registros de una tabla
con múltiples registros de otra. En esta lección aprenderás a usar
belongsToMany, tablas pivot y los métodos attach,
detach y sync.
Concepto de relación muchos a muchos
Una relación muchos a muchos (N:M) ocurre cuando un registro puede estar asociado con múltiples registros de otra tabla, y viceversa. Por ejemplo:
- Un post puede tener muchas etiquetas
- Una etiqueta puede pertenecer a muchos posts
Para implementar esta relación necesitas una tabla intermedia (pivot table) que almacena las conexiones entre ambas tablas.
La tabla pivot
La tabla pivot contiene las claves foráneas de ambas tablas relacionadas. Por convención, su nombre combina los nombres de ambas tablas en singular, ordenados alfabéticamente.
<?php
// Migración de tags (etiquetas)
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
// Migración de la tabla pivot: post_tag
// Nombre: post + tag (orden alfabético)
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->timestamps();
// Evitar duplicados
$table->unique(['post_id', 'tag_id']);
});
Laravel espera que la tabla pivot se llame con los nombres
de ambos modelos en singular y orden alfabético: post_tag,
no tag_post. Si usas otro nombre, deberás especificarlo
en la relación.
Definir la relación con belongsToMany
Usa belongsToMany en ambos modelos. A diferencia de las
relaciones uno a uno/muchos, aquí ambos lados usan el mismo método.
<?php
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Post extends Model
{
protected $fillable = ['user_id', 'title', 'slug', 'content'];
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
}
<?php
// app/Models/Tag.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Tag extends Model
{
protected $fillable = ['name', 'slug'];
public function posts(): BelongsToMany
{
return $this->belongsToMany(Post::class);
}
}
Acceder a los datos relacionados
<?php
use App\Models\Post;
use App\Models\Tag;
// Obtener las etiquetas de un post
$post = Post::find(1);
foreach ($post->tags as $tag) {
echo $tag->name;
}
// Obtener los posts de una etiqueta
$tag = Tag::where('slug', 'laravel')->first();
foreach ($tag->posts as $post) {
echo $post->title;
}
Gestionar la relación: attach, detach, sync
Laravel proporciona métodos para añadir, eliminar y sincronizar registros en la tabla pivot.
attach() - Añadir relaciones
<?php
use App\Models\Post;
$post = Post::find(1);
// Añadir una etiqueta (por ID)
$post->tags()->attach(1);
// Añadir múltiples etiquetas
$post->tags()->attach([1, 2, 3]);
// Añadir con datos extra en la tabla pivot
$post->tags()->attach(1, ['added_by' => auth()->id()]);
detach() - Eliminar relaciones
<?php
$post = Post::find(1);
// Eliminar una etiqueta específica
$post->tags()->detach(1);
// Eliminar múltiples etiquetas
$post->tags()->detach([1, 2]);
// Eliminar TODAS las etiquetas
$post->tags()->detach();
sync() - Sincronizar relaciones
sync() es el método más útil: reemplaza todas las
relaciones existentes con las que le pases.
<?php
$post = Post::find(1);
// El post tendrá SOLO estas etiquetas (elimina las demás)
$post->tags()->sync([1, 3, 5]);
// syncWithoutDetaching: añade sin eliminar las existentes
$post->tags()->syncWithoutDetaching([2, 4]);
Usa sync() cuando el usuario envía una lista completa
de relaciones (ej: checkboxes en un formulario). Usa attach()
cuando añades relaciones incrementalmente sin afectar las existentes.
Datos adicionales en la tabla pivot
Puedes almacenar información extra en la tabla pivot, como la fecha en que se añadió una etiqueta o quién la añadió.
<?php
// Migración con columnas extra
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->foreignId('added_by')->nullable()->constrained('users');
$table->timestamps();
$table->unique(['post_id', 'tag_id']);
});
Para acceder a estos datos, usa withPivot() y
withTimestamps() en la relación:
<?php
// app/Models/Post.php
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)
->withPivot('added_by')
->withTimestamps();
}
// Uso
$post = Post::find(1);
foreach ($post->tags as $tag) {
echo $tag->name;
echo $tag->pivot->added_by; // ID del usuario
echo $tag->pivot->created_at; // Cuándo se añadió
}
Ejemplo práctico: Roles de usuario
Un caso común es asignar roles a usuarios, donde un usuario puede tener múltiples roles y un rol puede pertenecer a múltiples usuarios.
<?php
// Migraciones
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name'); // admin, editor, author
$table->string('slug')->unique();
$table->timestamps();
});
Schema::create('role_user', function (Blueprint $table) {
$table->id();
$table->foreignId('role_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['role_id', 'user_id']);
});
<?php
// app/Models/Role.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
protected $fillable = ['name', 'slug'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)->withTimestamps();
}
}
// En User.php, añadir:
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)->withTimestamps();
}
// Método helper para verificar rol
public function hasRole(string $slug): bool
{
return $this->roles()->where('slug', $slug)->exists();
}
<?php
use App\Models\User;
use App\Models\Role;
// Asignar roles a un usuario
$user = User::find(1);
$adminRole = Role::where('slug', 'admin')->first();
$editorRole = Role::where('slug', 'editor')->first();
$user->roles()->attach([$adminRole->id, $editorRole->id]);
// Verificar si tiene un rol
if ($user->hasRole('admin')) {
// Es administrador
}
// Obtener usuarios con un rol específico
$admins = Role::where('slug', 'admin')
->first()
->users;
Eager Loading en relaciones N:M
Al igual que con otras relaciones, usa with() para
evitar el problema N+1:
<?php
// Cargar posts con sus etiquetas
$posts = Post::with('tags')->get();
foreach ($posts as $post) {
foreach ($post->tags as $tag) {
echo $tag->name; // Sin consulta adicional
}
}
// Cargar usuarios con sus roles
$users = User::with('roles')->get();
Resumen
- Las relaciones muchos a muchos requieren una tabla pivot
- El nombre de la tabla pivot es alfabético:
post_tag,role_user - Ambos modelos usan
belongsToMany attach()añade relaciones,detach()las eliminasync()reemplaza todas las relaciones con las especificadas- Usa
withPivot()para acceder a columnas extra de la tabla pivot - Recuerda usar
with()para eager loading
Ejercicios
Ejercicio 1: Sistema de etiquetas
Crea las migraciones y modelos para un sistema de etiquetas en posts.
Implementa belongsToMany en ambos modelos con timestamps.
Ver solución
<?php
// database/migrations/xxxx_create_tags_table.php
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
// database/migrations/xxxx_create_post_tag_table.php
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['post_id', 'tag_id']);
});
// app/Models/Tag.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Tag extends Model
{
protected $fillable = ['name', 'slug'];
public function posts(): BelongsToMany
{
return $this->belongsToMany(Post::class)->withTimestamps();
}
}
// En Post.php, añadir:
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
Ejercicio 2: Gestionar etiquetas de un post
Escribe el código para: crear 3 etiquetas, asignarlas a un post
usando sync(), y luego mostrar las etiquetas del post.
Ver solución
<?php
use App\Models\Post;
use App\Models\Tag;
// Crear etiquetas
$laravel = Tag::create(['name' => 'Laravel', 'slug' => 'laravel']);
$php = Tag::create(['name' => 'PHP', 'slug' => 'php']);
$tutorial = Tag::create(['name' => 'Tutorial', 'slug' => 'tutorial']);
// Obtener un post
$post = Post::find(1);
// Asignar etiquetas con sync
$post->tags()->sync([
$laravel->id,
$php->id,
$tutorial->id,
]);
// Mostrar etiquetas del post
foreach ($post->tags as $tag) {
echo $tag->name . ' '; // Laravel PHP Tutorial
}
// También puedes cargar con eager loading
$post = Post::with('tags')->find(1);
echo 'Etiquetas: ' . $post->tags->pluck('name')->join(', ');
// Etiquetas: Laravel, PHP, Tutorial
Ejercicio 3: Posts por etiqueta
Escribe una consulta que obtenga todos los posts que tienen la etiqueta "laravel", ordenados por fecha de creación, con eager loading del autor.
Ver solución
<?php
use App\Models\Post;
use App\Models\Tag;
// Opción 1: Desde el modelo Tag
$tag = Tag::where('slug', 'laravel')->first();
$posts = $tag->posts()
->with('user')
->orderBy('created_at', 'desc')
->get();
// Opción 2: Desde el modelo Post con whereHas
$posts = Post::with('user')
->whereHas('tags', function ($query) {
$query->where('slug', 'laravel');
})
->orderBy('created_at', 'desc')
->get();
// Uso
foreach ($posts as $post) {
echo $post->title . ' por ' . $post->user->name;
}
¿Has encontrado un error o tienes una sugerencia para mejorar esta lección?
Escríbenos¿Te está gustando el curso?
Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.
Descubrir cursos premium