در پروژههای واقعی Laravel معمولاً دادهها فقط با روابط یک به یک یا یک به چند ارتباط ندارند. در بسیاری از سیستمها ممکن است هر رکورد از یک جدول با چند رکورد جدول دیگر ارتباط داشته باشد و برعکس، هر رکورد جدول دوم نیز بتواند با چند رکورد جدول اول مرتبط باشد.
برای مثال:
- یک دانشجو میتواند در چند دوره آموزشی ثبتنام کند.
- هر دوره آموزشی میتواند چند دانشجو داشته باشد.
- یک محصول میتواند در چند دستهبندی قرار بگیرد.
- هر دستهبندی میتواند چند محصول داشته باشد.
- یک کاربر میتواند چند Role داشته باشد.
- هر Role نیز میتواند متعلق به چند User باشد.
- یک مقاله میتواند چند Tag داشته باشد.
- هر Tag نیز میتواند برای چند مقاله استفاده شود.
این نوع ارتباط را Many to Many یا رابطه چند به چند مینامیم.
در Laravel، Eloquent برای مدیریت این روابط امکانات بسیار کاملی دارد. مهمترین ابزار در این رابطه متد:
belongsToMany()
است.
در این مقاله بهصورت کامل یاد میگیریم که رابطه Many to Many چیست، Pivot Table چگونه کار میکند، چگونه Relationship را در Laravel تعریف کنیم و چطور دادهها را با attach، detach و sync مدیریت کنیم.
Many to Many در Laravel چیست؟
در رابطه Many to Many، هر رکورد از جدول اول میتواند با چند رکورد از جدول دوم ارتباط داشته باشد و هر رکورد از جدول دوم نیز میتواند با چند رکورد از جدول اول مرتبط باشد.
برای مثال، رابطه بین User و Role را در نظر بگیرید.
یک User میتواند چند Role داشته باشد:
User
├── Admin
├── Editor
└── Author
از طرف دیگر، یک Role نیز میتواند به چند User اختصاص داده شود:
Admin
├── Ali
├── Reza
└── Sara
پس رابطه به این شکل است:
User N ←────────→ N Role
این رابطه با One to Many تفاوت دارد.
در One to Many:
User
├── Order
├── Order
└── Order
اما در Many to Many:
User
├── Role
├── Role
└── Role
Role
├── User
├── User
└── User
# چرا به Pivot Table نیاز داریم؟
در دیتابیس رابطهای نمیتوانیم بهسادگی چند شناسه را داخل یک ستون ذخیره کنیم.
مثلاً این ساختار مناسب نیست:
users
id | role_ids
1 | 1,2,3
روش صحیح این است که یک جدول واسط ایجاد کنیم.
مثلاً:
users
------
id
name
roles
------
id
name
و:
role_user
--------
user_id
role_id
این جدول واسط را معمولاً Pivot Table مینامیم.
ساختار رابطه به این شکل خواهد بود:
User
│
│
▼
role_user
│
│
▼
Role
یا:
users
│
│
▼
role_user
▲
│
│
roles
Pivot Table باعث میشود بتوانیم هر User را به چند Role و هر Role را به چند User متصل کنیم.
# مثال واقعی: User و Role
فرض کنیم جدول users به شکل زیر است:
users
id name
1 Ali
2 Reza
3 Sara
جدول roles:
roles
id name
1 Admin
2 Editor
3 Author
و Pivot Table:
role_user
user_id role_id
1 1
1 2
2 2
2 3
3 3
یعنی:
Ali
├── Admin
└── Editor
Reza
├── Editor
└── Author
Sara
└── Author
در همین زمان:
Admin
└── Ali
Editor
├── Ali
└── Reza
Author
├── Reza
└── Sara
این دقیقاً همان چیزی است که Many to Many برای آن طراحی شده است.
# ساخت Pivot Table در Laravel
فرض کنیم میخواهیم رابطه User و Role را ایجاد کنیم.
میتوانیم Migration مربوط به Pivot Table را بسازیم:
php artisan make:migration create_role_user_table
سپس:
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('role_id')
->constrained()
->cascadeOnDelete();
$table->primary(['user_id', 'role_id']);
});
در اینجا دو Foreign Key داریم:
user_id
role_id
و هر دو به جداول اصلی خودشان اشاره میکنند.
# نام Pivot Table در Laravel
Laravel برای بسیاری از Relationshipها Convention مشخصی دارد.
اگر دو Model داشته باشیم:
User
Role
نام متداول Pivot Table:
role_user
است.
در این نام، نام دو Model بهصورت alphabetical order قرار میگیرند.
مثلاً برای:
Post
Tag
معمولاً:
post_tag
است.
برای:
Product
Category
میتوان از:
category_product
استفاده کرد.
البته Laravel اجازه میدهد نام جدول Pivot را بهصورت دستی نیز مشخص کنیم.
# تعریف Many to Many در Model
حالا باید Relationship را در Modelها تعریف کنیم.
فرض کنیم دو Model داریم:
app/Models/User.php
app/Models/Role.php
در Model User:
public function roles()
{
return $this->belongsToMany(Role::class);
}
در Model Role نیز:
public function users()
{
return $this->belongsToMany(User::class);
}
بنابراین:
User
└── belongsToMany()
↓
Role
Role
└── belongsToMany()
↓
User
# belongsToMany چیست؟
متد:
belongsToMany()
برای تعریف رابطه Many to Many در Eloquent استفاده میشود.
مثلاً:
public function roles()
{
return $this->belongsToMany(Role::class);
}
یعنی:
> این User میتواند چند Role داشته باشد و هر Role نیز میتواند متعلق به چند User باشد.
# دریافت Roleهای یک User
بعد از تعریف Relationship میتوانیم بنویسیم:
$user = User::find(1);
$roles = $user->roles;
حالا $roles شامل تمام Roleهای User خواهد بود.
مثلاً:
foreach ($user->roles as $role) {
echo $role->name;
}
# دریافت Userهای یک Role
چون رابطه را در هر دو Model تعریف کردهایم، میتوانیم مسیر معکوس را نیز طی کنیم:
$role = Role::find(1);
$users = $role->users;
مثلاً:
foreach ($role->users as $user) {
echo $user->name;
}
این یکی از تفاوتهای مهم Many to Many با One to Many است؛ رابطه در هر دو طرف میتواند چند رکورد داشته باشد.
# ایجاد رابطه با attach
یکی از مهمترین متدهای Many to Many در Laravel، attach() است.
فرض کنیم User شماره ۱ را داریم و میخواهیم Role شماره ۲ را به او اختصاص دهیم:
$user->roles()->attach(2);
Laravel یک رکورد در Pivot Table ایجاد میکند:
user_id role_id
1 2
اگر بخواهیم چند Role را همزمان اضافه کنیم:
$user->roles()->attach([1, 2, 3]);
در این حالت سه رابطه ایجاد میشود.
# attach با آرایه کلید و مقدار
attach() فقط برای ارسال ID ساده نیست.
میتوانیم اطلاعات اضافی Pivot را نیز ارسال کنیم.
مثلاً فرض کنید در رابطه User و Role میخواهیم زمان اختصاص Role را ذخیره کنیم:
$user->roles()->attach(2, [
'assigned_at' => now(),
]);
در این حالت Pivot Table میتواند شامل:
user_id
role_id
assigned_at
باشد.
# Pivot Table با ستونهای اضافی
Pivot Table همیشه مجبور نیست فقط دو Foreign Key داشته باشد.
مثلاً در یک سیستم فروشگاهی:
products
categories
category_product
ممکن است Pivot Table شامل اطلاعات بیشتری باشد:
category_product
product_id
category_id
is_primary
sort_order
created_at
یا در یک سیستم آموزشی:
course_user
course_id
user_id
enrolled_at
completed_at
status
این اطلاعات به خود رابطه مربوط هستند، نه مستقیماً به یکی از دو Model اصلی.
# دریافت اطلاعات Pivot با withPivot
اگر Pivot Table ستونهای اضافی داشته باشد، میتوانیم مشخص کنیم که این اطلاعات هنگام دریافت Relationship نیز در دسترس باشند.
مثلاً:
public function roles()
{
return $this->belongsToMany(Role::class)
->withPivot('assigned_at');
}
حالا میتوانیم بنویسیم:
$user->roles->first()->pivot->assigned_at;
در اینجا:
pivot
به اطلاعات رکورد Pivot دسترسی میدهد.
# دریافت چند ستون Pivot
میتوانیم چند ستون را نیز مشخص کنیم:
public function courses()
{
return $this->belongsToMany(Course::class)
->withPivot([
'status',
'enrolled_at',
'completed_at',
]);
}
سپس:
$course->pivot->status;
در دسترس خواهد بود.
# withTimestamps در Pivot
اگر Pivot Table دارای:
created_at
updated_at
باشد، میتوانیم از:
->withTimestamps()
استفاده کنیم.
مثلاً:
public function courses()
{
return $this->belongsToMany(Course::class)
->withTimestamps();
}
در این حالت Laravel هنگام ایجاد یا بهروزرسانی Pivot، Timestampها را نیز مدیریت میکند.
# حذف رابطه با detach
اگر بخواهیم یک Role را از User حذف کنیم، بدون اینکه خود User یا Role حذف شوند، از detach() استفاده میکنیم.
مثلاً:
$user->roles()->detach(2);
این دستور فقط رابطه موجود در Pivot Table را حذف میکند.
یعنی:
User
X
Role
اما خود رکوردهای:
users
roles
باقی میمانند.
# حذف چند رابطه با detach
میتوانیم چند Role را همزمان جدا کنیم:
$user->roles()->detach([1, 2, 3]);
یا تمام Relationshipهای User را حذف کنیم:
$user->roles()->detach();
در این حالت تمام ارتباطهای Pivot مربوط به User حذف میشوند.
# sync در Laravel چیست؟
یکی از مهمترین قابلیتهای Many to Many، متد sync() است.
فرض کنید User در حال حاضر Roleهای زیر را دارد:
1
2
3
حالا میخواهیم وضعیت Roleهای او دقیقاً به:
2
4
تغییر کند.
میتوانیم بنویسیم:
$user->roles()->sync([2, 4]);
Laravel رابطهها را با لیست جدید هماهنگ میکند.
یعنی:
Role 1 → حذف
Role 2 → باقی میماند
Role 3 → حذف
Role 4 → اضافه میشود
این قابلیت برای فرمهای مدیریت Role و Permission بسیار کاربردی است.
# تفاوت attach و sync
این دو متد را نباید با یکدیگر اشتباه بگیریم.
attach
رابطه جدید اضافه میکند:
$user->roles()->attach(4);
sync
رابطههای موجود را با لیست جدید هماهنگ میکند:
$user->roles()->sync([2, 4]);
مثلاً اگر User از قبل Roleهای:
1, 2, 3
را داشته باشد، attach(4) باعث میشود:
1, 2, 3, 4
داشته باشد.
اما:
sync([2, 4])
باعث میشود نتیجه نهایی:
2, 4
باشد.
# syncWithoutDetaching چیست؟
گاهی میخواهیم Role جدیدی اضافه کنیم، اما Relationshipهای قبلی حذف نشوند.
در این حالت میتوانیم از:
$user->roles()->syncWithoutDetaching([4]);
استفاده کنیم.
این روش زمانی کاربردی است که بخواهیم رابطههای جدید را اضافه کنیم و روابط قبلی حفظ شوند.
# sync با Pivot Data
میتوانیم هنگام sync() اطلاعات Pivot را نیز ارسال کنیم.
مثلاً:
$user->roles()->sync([
1 => [
'assigned_at' => now(),
],
2 => [
'assigned_at' => now(),
],
]);
در این حالت برای هر Role اطلاعات Pivot مربوطه نیز مشخص میشود.
# toggle در Many to Many
Laravel متد toggle() را نیز برای مدیریت Relationshipهای Many to Many در اختیارمان قرار میدهد.
مثلاً:
$user->roles()->toggle([2, 3]);
اگر Role شماره ۲ وجود داشته باشد، حذف میشود و اگر وجود نداشته باشد، اضافه میشود.
همین رفتار برای Role شماره ۳ نیز اتفاق میافتد.
این قابلیت برای سیستمهایی که کاربر با کلیک روی یک گزینه رابطه را فعال یا غیرفعال میکند، کاربردی است.
# Eager Loading در Many to Many
همانند One to Many، در Many to Many نیز باید مراقب N+1 Query باشیم.
مثلاً:
$users = User::all();
foreach ($users as $user) {
foreach ($user->roles as $role) {
echo $role->name;
}
}
در پروژههای بزرگ ممکن است باعث Queryهای اضافی شود.
بهتر است Relationship را با with() بارگذاری کنیم:
$users = User::with('roles')->get();
سپس:
foreach ($users as $user) {
foreach ($user->roles as $role) {
echo $role->name;
}
}
# فیلتر کردن بر اساس Many to Many
فرض کنید میخواهیم Userهایی را پیدا کنیم که Role خاصی دارند.
میتوانیم از whereHas() استفاده کنیم:
$users = User::whereHas('roles', function ($query) {
$query->where('name', 'Admin');
})->get();
در این حالت فقط Userهایی دریافت میشوند که Role با نام Admin دارند.
# فیلتر بر اساس Pivot Table
اگر Pivot Table اطلاعات اضافی داشته باشد، میتوانیم بر اساس آن نیز فیلتر کنیم.
مثلاً فرض کنیم:
course_user
course_id
user_id
status
داریم.
میتوانیم Userهایی را پیدا کنیم که وضعیت ثبتنامشان active است:
$users = User::whereHas('courses', function ($query) {
$query->wherePivot('status', 'active');
})->get();
# wherePivot چیست؟
wherePivot() برای فیلتر کردن Relationship بر اساس ستونهای Pivot استفاده میشود.
مثلاً:
$user->courses()
->wherePivot('status', 'active')
->get();
این Query فقط دورههایی را برمیگرداند که رکورد Pivot آنها وضعیت active دارد.
# orderByPivot چیست؟
اگر بخواهیم دادهها را بر اساس یکی از ستونهای Pivot مرتب کنیم، میتوانیم از orderByPivot() استفاده کنیم.
مثلاً:
$user->courses()
->orderByPivot('enrolled_at', 'desc')
->get();
در اینجا دورهها بر اساس زمان ثبتنام مرتب میشوند.
# مثال مهم: User و Course
یکی از بهترین مثالها برای Many to Many، سیستم آموزشی است.
فرض کنیم:
users
courses
course_user
داریم.
یک User میتواند در چند Course ثبتنام کند:
Ali
├── Laravel
├── PHP
└── MySQL
و هر Course نیز چند User دارد:
Laravel
├── Ali
├── Reza
├── Sara
└── Amir
# Migration رابطه User و Course
Pivot Table:
Schema::create('course_user', function (Blueprint $table) {
$table->foreignId('course_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
$table->timestamp('enrolled_at')->nullable();
$table->string('status')->default('active');
$table->timestamps();
$table->unique(['course_id', 'user_id']);
});
در اینجا:
course_id
user_id
enrolled_at
status
داریم.
# Model User
class User extends Authenticatable
{
public function courses()
{
return $this->belongsToMany(Course::class)
->withPivot([
'enrolled_at',
'status',
])
->withTimestamps();
}
}
# Model Course
class Course extends Model
{
public function users()
{
return $this->belongsToMany(User::class)
->withPivot([
'enrolled_at',
'status',
])
->withTimestamps();
}
}
حالا رابطه از هر دو طرف قابل استفاده است.
# ثبتنام User در Course
برای ثبتنام:
$user->courses()->attach($course->id, [
'enrolled_at' => now(),
'status' => 'active',
]);
اکنون در Pivot Table یک رکورد ایجاد میشود.
# دریافت دورههای User
$courses = $user->courses;
و برای نمایش وضعیت:
foreach ($user->courses as $course) {
echo $course->name;
echo $course->pivot->status;
}
# تغییر وضعیت رابطه
مثلاً میخواهیم وضعیت ثبتنام را به completed تغییر دهیم.
میتوانیم از updateExistingPivot() استفاده کنیم:
$user->courses()->updateExistingPivot($course->id, [
'status' => 'completed',
'completed_at' => now(),
]);
البته اگر completed_at در Pivot Table وجود داشته باشد.
این روش زمانی مفید است که اطلاعات خود Course یا User تغییر نمیکند و فقط اطلاعات رابطه بین آنها باید تغییر کند.
# جدول واسط با نام سفارشی
Laravel بهصورت پیشفرض Conventionهایی برای نام Pivot Table دارد.
اما ممکن است جدول شما نام متفاوتی داشته باشد.
در این شرایط میتوانیم نام جدول را مشخص کنیم:
public function roles()
{
return $this->belongsToMany(
Role::class,
'user_roles'
);
}
در اینجا Laravel بهجای role_user از:
user_roles
استفاده میکند.
# Foreign Keyهای سفارشی در belongsToMany
اگر نام ستونهای Pivot با Convention لاراول متفاوت باشد، میتوانیم آنها را مشخص کنیم.
مثلاً:
return $this->belongsToMany(
Role::class,
'user_roles',
'customer_id',
'permission_id'
);
در پروژههای استاندارد معمولاً بهتر است تا حد امکان از Conventionهای Laravel استفاده کنیم، اما شناخت این قابلیت برای کار با دیتابیسهای موجود یا پروژههای Legacy بسیار مهم است.
# آیا برای Pivot Model جداگانه لازم است؟
در بسیاری از پروژهها نیازی به ساخت Model جداگانه برای Pivot نداریم.
برای مثال:
$user->roles()->attach(1);
و:
$user->roles()->detach(1);
بدون Pivot Model نیز کاملاً قابل استفاده هستند.
اما اگر Pivot Table منطق و رفتار پیچیدهای داشته باشد، ممکن است استفاده از Pivot Model مناسبتر باشد.
مثلاً اگر Pivot اطلاعات و منطق زیادی داشته باشد:
course_user
course_id
user_id
status
price
discount
enrolled_at
completed_at
cancelled_at
در چنین شرایطی ممکن است مدیریت رابطه با یک Pivot Model اختصاصی خواناتر و قابل نگهداریتر باشد.
# تفاوت Many to Many با One to Many
این تفاوت را بهخوبی یاد بگیرید.
One to Many
Category
│
├── Product
├── Product
└── Product
هر Product معمولاً متعلق به یک Category است.
Many to Many
Product
├── Category A
├── Category B
└── Category C
Category A
├── Product 1
├── Product 2
└── Product 3
در Many to Many هر دو طرف میتوانند چند رکورد مرتبط داشته باشند.
# اشتباهات رایج در Many to Many
۱. ذخیره چند ID در یک ستون
این ساختار:
user_id | role_ids
1 | 1,2,3
راهکار مناسبی برای دیتابیس رابطهای نیست.
بهجای آن باید Pivot Table داشته باشیم.
۲. نداشتن Unique Constraint
در Pivot Table معمولاً نباید یک رابطه یکسان چند بار ثبت شود.
مثلاً:
user_id | role_id
1 | 2
1 | 2
1 | 2
معمولاً نباید اجازه چنین دادهای را بدهیم.
به همین دلیل میتوانیم از Unique Constraint استفاده کنیم:
$table->unique(['user_id', 'role_id']);
۳. اشتباه گرفتن attach و sync
attach() رابطه جدید اضافه میکند.
sync() وضعیت رابطهها را با لیست دادهشده هماهنگ میکند.
پس اگر از sync() در جای اشتباه استفاده کنید، ممکن است Relationshipهای قبلی حذف شوند.
۴. فراموش کردن Pivot Data
اگر Pivot Table اطلاعات اضافی دارد، باید Relationship را با:
withPivot()
تعریف کنید تا بتوانید این اطلاعات را به شکل مورد انتظار از Relationship دریافت کنید.
۵. ایجاد N+1 Query
اگر Relationship را داخل Loop بدون Eager Loading استفاده کنید، ممکن است Queryهای اضافی ایجاد شود.
در صورت نیاز:
User::with('roles')->get();
استفاده کنید.
# Many to Many در سیستم Permission
یکی از مهمترین کاربردهای Many to Many در پروژههای واقعی، سیستم Role و Permission است.
ساختار میتواند چیزی شبیه این باشد:
User
↕
Role
↕
Permission
مثلاً:
Ali
├── Admin
└── Editor
Admin
├── create-user
├── delete-user
├── edit-user
└── view-dashboard
در چنین سیستمهایی معمولاً چندین Pivot Table وجود دارد.
این ساختار پایه بسیاری از سیستمهای مدیریت دسترسی است و در پروژههای Laravel بزرگ اهمیت زیادی دارد.
# چه زمانی از Many to Many استفاده کنیم؟
هر زمان که هر رکورد از جدول A بتواند چند رکورد از جدول B داشته باشد و هر رکورد از جدول B نیز بتواند چند رکورد از جدول A داشته باشد، Many to Many گزینه مناسبی است.
نمونههای رایج:
| جدول اول | جدول دوم | Pivot |
|---|---|---|
| User | Role | role_user |
| User | Course | course_user |
| Post | Tag | post_tag |
| Product | Category | category_product |
| Product | Attribute | attribute_product |
| Student | Course | course_student |
| User | Permission | permission_user |
# جمعبندی
Many to Many در Laravel برای مدیریت رابطههایی استفاده میشود که هر دو طرف میتوانند چند رکورد مرتبط داشته باشند.
ساختار اصلی:
Model A
↕
Pivot Table
↕
Model B
در Laravel برای تعریف این رابطه از:
belongsToMany()
استفاده میکنیم.
برای مثال:
public function roles()
{
return $this->belongsToMany(Role::class);
}
برای اضافه کردن رابطه:
$user->roles()->attach(2);
برای حذف:
$user->roles()->detach(2);
برای هماهنگ کردن مجموعه Relationshipها:
$user->roles()->sync([1, 2, 3]);
و برای حفظ روابط قبلی هنگام اضافه کردن روابط جدید:
$user->roles()->syncWithoutDetaching([4]);
اگر Pivot Table اطلاعات اضافی داشته باشد:
return $this->belongsToMany(Role::class)
->withPivot('assigned_at')
->withTimestamps();
و سپس میتوانیم به اطلاعات Pivot دسترسی داشته باشیم:
$role->pivot->assigned_at;
بنابراین یادگیری Many to Many یکی از مراحل مهم کار حرفهای با Eloquent است، مخصوصاً در پروژههایی مانند فروشگاه اینترنتی، سیستم آموزشی، مدیریت کاربران، Role & Permission و شبکههای اجتماعی.
# سوالات متداول
Many to Many در Laravel چیست؟
رابطهای است که در آن هر رکورد از یک جدول میتواند با چند رکورد از جدول دیگر ارتباط داشته باشد و برعکس.
برای Many to Many در Laravel از چه متدی استفاده میشود؟
از:
belongsToMany()
استفاده میشود.
Pivot Table چیست؟
جدول واسطی است که ارتباط بین دو جدول را در رابطه Many to Many نگهداری میکند.
مثلاً:
user_id
role_id
attach در Laravel چه کاری انجام میدهد؟
یک یا چند رابطه جدید در Pivot Table ایجاد میکند.
$user->roles()->attach(2);
detach چه کاری انجام میدهد؟
رابطه را از Pivot Table حذف میکند، بدون اینکه خود Modelهای اصلی حذف شوند.
sync چه کاری انجام میدهد؟
Relationshipهای موجود را با لیست جدید هماهنگ میکند و روابطی که در لیست جدید نیستند را حذف میکند.
syncWithoutDetaching چه کاربردی دارد؟
رابطههای جدید را اضافه میکند بدون اینکه Relationshipهای قبلی حذف شوند.
چگونه اطلاعات اضافی Pivot را دریافت کنیم؟
با withPivot():
return $this->belongsToMany(Role::class)
->withPivot('assigned_at');
سپس:
$role->pivot->assigned_at;
آیا برای Pivot Table همیشه Model جداگانه لازم است؟
خیر. برای Relationshipهای ساده معمولاً belongsToMany() کافی است. اگر Pivot منطق و دادههای پیچیدهای داشته باشد، استفاده از Pivot Model میتواند مناسبتر باشد.





