العلاقات بين الجداول في Laravel Eloquent – HasMany وBelongsTo وManyToMany
بعد أن تعرّفنا في الدروس السابقة على قواعد البيانات و Migrations، وتعلمنا أساسيات Models و Eloquent، حان الوقت للانتقال إلى واحدة من أهم النقاط في بناء تطبيقات Laravel الحقيقية: العلاقات بين الجداول.
في أي تطبيق تقريبًا، نادرًا ما تكون الجداول منفصلة تمامًا عن بعضها.
في موقع مقالات مثلًا، لدينا مستخدم يمكن أن يكتب عدة مقالات، والمقال الواحد ينتمي إلى مستخدم واحد، والمقال يمكن أن يحتوي على عدة تعليقات، والتعليق ينتمي إلى مقال معين.
وفي متجر إلكتروني، لدينا:
- مستخدمون.
- منتجات.
- تصنيفات.
- طلبات.
- تفاصيل الطلبات.
وقد يكون المنتج موجودًا في عدة طلبات، والطلب يحتوي على عدة منتجات، والمستخدم لديه عدة طلبات.
بدون العلاقات، سنضطر إلى كتابة الكثير من الاستعلامات المعقدة يدويًا.
أما مع Eloquent ORM، فيمكننا تعريف العلاقات داخل Models، ثم التعامل معها بطريقة بسيطة وواضحة.
Laravel Eloquent يدعم مجموعة واسعة من العلاقات، من بينها One to One وOne to Many وMany to Many، بالإضافة إلى العلاقات عبر نموذج وسيط والعلاقات Polymorphic.
في هذا الدرس سنركز على العلاقات التي يحتاجها المبتدئ أولًا، ثم نأخذ نظرة على العلاقات المتقدمة.
ما المقصود بالعلاقات بين الجداول؟
لنفترض أن لدينا جدولين:
users articles
ولدينا البيانات التالية:
users id | name 1 | أحمد 2 | محمد
وجدول المقالات:
articles id | user_id | title 1 | 1 | تعلم Laravel 2 | 1 | شرح Routing 3 | 2 | تعلم PHP
لاحظ أن المقال الأول والثاني يحتويان على:
user_id = 1
وهذا يعني أن كلا المقالين يتبعان المستخدم الذي يحمل:
id = 1
إذن لدينا علاقة:
User | | hasMany ↓ Articles
وفي الاتجاه العكسي:
Article | | belongsTo ↓ User
هذه هي فكرة العلاقات الأساسية في Eloquent.
لماذا نستخدم Eloquent Relationships؟
بدلًا من كتابة استعلامات SQL طويلة للحصول على المقالات الخاصة بالمستخدم، يمكننا تعريف العلاقة داخل Model.
مثلًا داخل User:
public function articles()
{
return $this->hasMany(Article::class);
}
ثم يمكننا كتابة:
$user->articles;
للحصول على مقالات المستخدم.
وبالعكس داخل Article:
public function user()
{
return $this->belongsTo(User::class);
}
ثم:
$article->user;
للحصول على المستخدم الذي كتب المقال.
Laravel يجعل العلاقات عبارة عن Methods داخل Eloquent Models، وهذه العلاقات يمكن استخدامها أيضًا كـ Query Builders لإضافة شروط وترتيب واستعلامات أخرى.
أنواع العلاقات الأساسية
هناك عدة أنواع من العلاقات في Laravel، لكن أهم ما يجب أن تتعلمه في البداية هو:
- One to One
- One to Many
- Belongs To
- Many to Many
- Has Many Through
- Polymorphic Relationships
سنبدأ بالأنواع الأربعة الأساسية.
One to One علاقة واحد إلى واحد
في علاقة One to One يكون لكل سجل سجل واحد مرتبط به.
مثال بسيط:
User ↓ Phone
يمكن أن يكون لكل مستخدم هاتف واحد.
في قاعدة البيانات قد يكون لدينا:
users id | name 1 | أحمد 2 | محمد
وجدول:
phones id | user_id | phone_number 1 | 1 | 555123 2 | 2 | 555456
هنا كل مستخدم لديه هاتف واحد.
تعريف Has One
داخل User Model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class User extends Model
{
public function phone(): HasOne
{
return $this->hasOne(Phone::class);
}
}
لاحظ أننا استخدمنا:
$this->hasOne(Phone::class);
أي أن User لديه Phone واحد.
بعد ذلك يمكننا استخدام:
$user = User::find(1); $phone = $user->phone;
وهنا سيحاول Eloquent الحصول على الهاتف المرتبط بالمستخدم.
تعريف العلاقة العكسية BelongsTo
داخل Phone:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Phone extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
الآن يمكننا كتابة:
$phone = Phone::find(1); $user = $phone->user;
وهكذا يستطيع الهاتف الوصول إلى المستخدم المرتبط به.
One to Many – واحد إلى عدة
هذه من أهم العلاقات في Laravel.
مثال:
User | | hasMany ↓ Articles
المستخدم الواحد يمكن أن يكتب عدة مقالات.
مثلًا:
أحمد ├── تعلم Laravel ├── شرح Controllers └── شرح Eloquent
بينما المقال الواحد ينتمي إلى مستخدم واحد.
إنشاء علاقة hasMany
داخل User:
use Illuminate\Database\Eloquent\Relations\HasMany;
public function articles(): HasMany
{
return $this->hasMany(Article::class);
}
الآن:
$user = User::find(1); $articles = $user->articles;
سيحصل Eloquent على المقالات المرتبطة بهذا المستخدم.
قراءة المقالات
يمكننا استخدام:
foreach ($user->articles as $article) {
echo $article->title;
}
إذا كان المستخدم لديه ثلاث مقالات، فسيتم المرور عليها واحدة تلو الأخرى.
استخدام العلاقة كـ Query Builder
العلاقات في Eloquent ليست مجرد خصائص للقراءة.
يمكنك استدعاء Method العلاقة باستخدام:
$user->articles()
ثم إضافة شروط.
مثلًا:
$articles = $user->articles()
->where('is_published', true)
->get();
أو:
$articles = $user->articles()
->latest()
->get();
وهذا من نقاط القوة المهمة في Eloquent Relationships، لأن العلاقات نفسها توفر Query Builder يمكن تقييده بشروط إضافية.
BelongsTo العلاقة العكسية
إذا كان User لديه عدة Articles، فإن Article ينتمي إلى User.
داخل Article:
use Illuminate\Database\Eloquent\Relations\BelongsTo;
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
بعد ذلك:
$article = Article::find(1); echo $article->user->name;
يمكننا الآن الوصول إلى اسم صاحب المقال بسهولة.
أين يوجد Foreign Key؟
عادة يكون لدينا في جدول articles:
id user_id title content
ويكون:
user_id
هو المفتاح الأجنبي الذي يشير إلى:
users.id
أي:
articles.user_id
↓
users.id
وهذه النقطة مهمة جدًا لفهم العلاقة.
إذا كان لدينا:
articles id | user_id | title 1 | 5 | Laravel
فهذا يعني أن المقال رقم 1 يتبع المستخدم رقم 5.
كيف يعرف Laravel اسم Foreign Key؟
عند كتابة:
return $this->belongsTo(User::class);
يحاول Eloquent عادة استنتاج اسم المفتاح الأجنبي من اسم الـ Model.
في حالة:
User
يكون المتوقع:
user_id
ولهذا لا نحتاج غالبًا إلى كتابة اسم العمود يدويًا إذا اتبعنا conventions الخاصة بـ Laravel.
تحديد Foreign Key بشكل يدوي
إذا كان اسم العمود مختلفًا، يمكننا تحديده:
return $this->belongsTo(
User::class,
'author_id'
);
هنا أصبح اسم المفتاح:
author_id
بدل:
user_id
ويمكن أيضًا تحديد المفتاح الأساسي الأب إذا كان مختلفًا عن id. توثيق Laravel يوضح أن belongsTo يقبل تحديد الـ foreign key والـ owner key عند الحاجة.
مثال كامل User وArticle
لننشئ العلاقة بشكل كامل.
User Model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
public function articles(): HasMany
{
return $this->hasMany(Article::class);
}
}
Article Model
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Article extends Model
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
الآن لدينا:
User | | hasMany ↓ Article
والاتجاه العكسي:
Article | | belongsTo ↓ User
Many to Many – علاقة متعدد إلى متعدد
هذه العلاقة مهمة جدًا في المشاريع الكبيرة.
لنفترض أن لدينا:
Users Roles
والمستخدم الواحد يمكن أن يمتلك عدة Roles.
وفي نفس الوقت، الـ Role الواحد يمكن أن يكون مرتبطًا بعدة Users.
مثلًا:
Ahmed ├── Admin └── Editor Mohamed ├── Editor └── Author
إذن لدينا:
User ↔ Role
وهي Many to Many.
لماذا نحتاج جدولًا وسيطًا؟
لا يمكننا وضع:
role_id
داخل users إذا كان المستخدم يمكن أن يمتلك عدة Roles.
ولا يمكننا وضع:
user_id
داخل roles إذا كان الـ Role يمكن أن يرتبط بعدة مستخدمين.
لذلك نستخدم جدولًا وسيطًا.
مثلًا:
users roles role_user
ويحتوي جدول role_user على:
user_id role_id
توثيق Laravel يستخدم هذا النمط للعلاقات Many to Many، ويشير إلى أن جدول الـ intermediate table يكون عادة باسم يجمع أسماء النموذجين بترتيب أبجدي مثل role_user.
إنشاء belongsToMany
داخل User:
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
الآن:
$user = User::find(1); $roles = $user->roles;
سيحصل Eloquent على الأدوار المرتبطة بالمستخدم.
العلاقة العكسية
داخل Role:
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
وبالتالي يمكننا:
$role = Role::find(1); $users = $role->users;
وهكذا نستطيع التنقل في الاتجاهين.
جدول Pivot
الجدول الوسيط في Many to Many يسمى غالبًا:
Pivot Table
مثل:
role_user
ويحتوي:
user_id role_id
لنفترض أن لدينا:
role_user user_id | role_id 1 | 1 1 | 2 2 | 2 2 | 3
هذا يعني:
User 1 → Role 1 User 1 → Role 2 User 2 → Role 2 User 2 → Role 3
إضافة علاقة Many to Many
يمكننا استخدام:
$user->roles()->attach($roleId);
مثل:
$user->roles()->attach(2);
وهذا يضيف ارتباطًا بين المستخدم والـ Role.
يمكن أيضًا ربط أكثر من Role:
$user->roles()->attach([1, 2, 3]);
حذف علاقة من Pivot
إذا أردنا إزالة ارتباط:
$user->roles()->detach(2);
أو:
$user->roles()->detach([1, 2]);
وهذا يحذف الارتباط من جدول Pivot دون حذف الـ Role نفسه.
استخدام sync()
إذا أردت أن تجعل Roles الخاصة بالمستخدم مطابقة تمامًا لقائمة معينة:
$user->roles()->sync([1, 2, 3]);
فسيقوم Laravel بتحديث جدول Pivot بحيث تصبح العلاقات الحالية متوافقة مع هذه القائمة.
هذه الطريقة مفيدة جدًا عند التعامل مع نماذج الصلاحيات أو التصنيفات أو الوسوم.
بيانات إضافية في Pivot
أحيانًا لا يحتوي جدول Pivot على مفتاحين فقط.
مثلًا:
role_user user_id role_id assigned_by created_at updated_at
يمكننا طلب بيانات إضافية من Pivot.
مثلًا:
return $this->belongsToMany(Role::class)
->withPivot('assigned_by');
وبعد الحصول على Role:
$role->pivot->assigned_by;
يمكن أيضًا استخدام:
->withTimestamps()
إذا كان جدول Pivot يحتوي على:
created_at updated_at
وتدعم Eloquent التعامل مع هذه الأعمدة تلقائيًا عند تعريف العلاقة بهذه الطريقة.
Eager Loading
الآن وصلنا إلى نقطة مهمة جدًا في الأداء.
لنفترض أننا نريد عرض 100 مقال، وكل مقال نريد عرض اسم صاحبه:
$articles = Article::all();
foreach ($articles as $article) {
echo $article->user->name;
}
قد يؤدي هذا إلى تنفيذ استعلام للحصول على المقالات، ثم استعلامات إضافية للحصول على المستخدمين عند الوصول إلى العلاقة.
وهذا يمكن أن يؤدي إلى مشكلة تسمى:
N + 1 Query Problem
Laravel يوفر Eager Loading لتجنب هذا النوع من المشاكل.
استخدام with()
يمكننا كتابة:
$articles = Article::with('user')->get();
الآن نطلب من Laravel تحميل العلاقة مع المقالات.
ثم:
foreach ($articles as $article) {
echo $article->user->name;
}
وبذلك يكون التعامل مع البيانات أكثر كفاءة في الحالات المناسبة.
Eager Loading لأكثر من علاقة
يمكنك تحميل عدة علاقات:
$articles = Article::with([
'user',
'comments',
])->get();
إذا كان Article لديه:
user comments
فسيتم تحميل العلاقات المطلوبة مع الاستعلام الأساسي.
Eager Loading المتداخل
يمكن أيضًا تحميل علاقات داخل علاقات.
مثلًا:
$articles = Article::with('user.profile')->get();
الفكرة هنا:
Article ↓ User ↓ Profile
وهذا مفيد عندما يكون لديك أكثر من مستوى في البيانات.
Lazy Loading
عندما تكتب:
$article->user
فإن Eloquent يستطيع تحميل العلاقة عند الوصول إليها.
وهذا يسمى:
Lazy Loading
أي أن العلاقة لا يتم تحميلها إلا عند الحاجة إليها.
أما:
Article::with('user')->get();
فهو مثال على:
Eager Loading
أي أنك تطلب من Laravel تحميل العلاقة مسبقًا.
الفرق بينهما مهم جدًا عندما تبدأ التعامل مع عدد كبير من السجلات.
Querying Relationship
يمكنك البحث عن النماذج بناءً على وجود علاقة.
مثلًا:
Article::has('comments')->get();
هذا يمكن استخدامه للحصول على المقالات التي لديها تعليقات.
ويمكن أيضًا استخدام:
Article::whereHas('comments', function ($query) {
$query->where('approved', true);
})->get();
وهنا نطلب المقالات التي لديها تعليقات تحقق شرطًا معينًا.
Eloquent يوفر has وwhereHas للاستعلام بناءً على وجود العلاقات وشروطها.
مثال عملي: نظام المقالات والتعليقات
لنقم ببناء مثال بسيط.
لدينا:
users articles comments
العلاقات:
User | | hasMany ↓ Articles | | hasMany ↓ Comments
والاتجاهات العكسية:
Article | | belongsTo ↓ User
و:
Comment | | belongsTo ↓ Article
User Model
public function articles(): HasMany
{
return $this->hasMany(Article::class);
}
Article Model
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
Comment Model
public function article(): BelongsTo
{
return $this->belongsTo(Article::class);
}
الآن يمكننا كتابة:
$article = Article::find(1);
ثم:
$article->user;
للحصول على صاحب المقال.
و:
$article->comments;
للحصول على التعليقات.
ومن التعليق:
$comment->article;
للحصول على المقال.
استخدام العلاقات داخل Controller
يمكن أن نستخدم العلاقات داخل Controller بطريقة بسيطة.
مثال:
public function show(Article $article)
{
$article->load('user', 'comments');
return view('articles.show', compact('article'));
}
هنا استخدمنا:
load()
لتحميل العلاقات على Model موجود بالفعل.
ثم داخل Blade:
<h1>{{ $article->title }}</h1>
<p>
الكاتب: {{ $article->user->name }}
</p>
@foreach ($article->comments as $comment)
<p>{{ $comment->content }}</p>
@endforeach
وهكذا أصبح لدينا صفحة مقال تعرض:
- عنوان المقال.
- اسم الكاتب.
- التعليقات.
Has Many Through
هناك حالات نحتاج فيها إلى الوصول إلى بيانات من خلال علاقة وسيطة.
مثال:
Country ↓ Users ↓ Articles
قد نريد الحصول على جميع المقالات الخاصة بدولة معينة من خلال المستخدمين.
هنا يمكن استخدام:
hasManyThrough
مثال مبسط:
public function articles(): HasManyThrough
{
return $this->hasManyThrough(
Article::class,
User::class
);
}
هذا النوع من العلاقات يسمح للنموذج بالوصول إلى نماذج بعيدة من خلال نموذج وسيط. Laravel يوفر hasManyThrough لهذا النوع من السيناريوهات.
لكن لا تقلق إذا بدت لك هذه العلاقة صعبة الآن؛ في أغلب المشاريع التعليمية الأولى ستستخدم:
hasOne hasMany belongsTo belongsToMany
بشكل أكبر.
متى أستخدم كل علاقة؟
يمكنك حفظها بهذه الطريقة:
HasOne
عندما يكون لديك:
User → Phone
أي واحد إلى واحد.
HasMany
عندما يكون لديك:
User → Articles
أي واحد إلى عدة.
BelongsTo
عندما يكون السجل الحالي ينتمي إلى سجل أب.
مثل:
Article → User
BelongsToMany
عندما يكون الطرفان يمكن أن يرتبطا بعدة سجلات من الطرف الآخر.
مثل:
User ↔ Roles
أو:
Articles ↔ Tags
مثال عملي باستخدام Articles وCategories
لنفترض أن لدينا:
categories articles
وكل Category لديها عدة Articles:
Category | | hasMany ↓ Articles
داخل Category:
public function articles(): HasMany
{
return $this->hasMany(Article::class);
}
وفي Article:
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
ويكون جدول articles مثل:
id category_id title content
ثم يمكننا:
$category->articles;
أو:
$article->category;
العلاقة بين Migration وRelationship
من المهم أن تفهم أن تعريف العلاقة داخل Model وحده لا ينشئ Foreign Key في قاعدة البيانات.
مثلًا:
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
هذا يخبر Eloquent بكيفية الوصول إلى المستخدم.
لكن قاعدة البيانات نفسها يجب أن تحتوي على:
user_id
وفي Migration يمكن أن نكتب:
$table->foreignId('user_id')
->constrained();
إذن لدينا جزآن:
Migration ↓ Database Structure ↓ Foreign Key
و:
Model ↓ Eloquent Relationship ↓ Application Logic
يجب أن يعمل الجزآن معًا.
أخطاء شائعة في العلاقات
1 - نسيان Foreign Key
إذا كان Article يعتمد على:
user_id
لكن العمود غير موجود في قاعدة البيانات، فلن تعمل العلاقة بالشكل المتوقع.
2 - كتابة اسم Model خاطئ
إذا كان لديك:
Article::class
تأكد من وجود:
app/Models/Article.php
واستخدام Namespace صحيح.
3 - الخلط بين hasMany وbelongsTo
إذا كان:
User → Articles
فاستخدم:
hasMany()
أما:
Article → User
فاستخدم:
belongsTo()
قاعدة سهلة للحفظ:
الأب لديه أبناء = hasMany
الابن ينتمي إلى أب = belongsTo
4 - تحميل العلاقات داخل Loop بطريقة غير محسوبة
إذا كان لديك عدد كبير من المقالات وكتبت:
$articles = Article::all();
foreach ($articles as $article) {
echo $article->user->name;
}
فانتبه إلى مشكلة N+1 المحتملة.
في مثل هذه الحالة غالبًا يكون:
$articles = Article::with('user')->get();
أفضل من ناحية الاستعلامات، لأن Eager Loading مصمم لمعالجة هذا النوع من السيناريوهات.
تمرين عملي
قم ببناء نظام صغير للمقالات يحتوي على:
users articles comments categories
واجعل العلاقات كالتالي:
User └── hasMany Articles Article ├── belongsTo User ├── belongsTo Category └── hasMany Comments Category └── hasMany Articles Comment └── belongsTo Article
ثم أنشئ Models والعلاقات المناسبة.
بعد ذلك جرّب:
$user->articles;
ثم:
$article->user;
ثم:
$article->category;
ثم:
$article->comments;
ثم:
$comment->article;
وأخيرًا حاول تحميل المقالات مع الكاتب والتعليقات باستخدام:
$articles = Article::with([
'user',
'comments',
])->get();
إذا تمكنت من تنفيذ هذا التمرين، فأنت بدأت فعليًا في فهم كيفية بناء العلاقات داخل تطبيق Laravel حقيقي.
في هذا الدرس تعلمنا كيف نتعامل مع العلاقات بين جداول قاعدة البيانات باستخدام Eloquent.
تعرفنا على العلاقة:
hasOne()
والتي تستخدم عندما يكون هناك سجل واحد مرتبط بسجل آخر.
ثم:
hasMany()
للعلاقة واحد إلى عدة.ء
وتعلمنا:
belongsTo()
للعلاقة العكسية، عندما يكون النموذج الحالي تابعًا لنموذج آخر.
ثم تعرفنا على:
belongsToMany()
للعلاقات Many to Many، واستخدمنا جدول Pivot لربط الطرفين.
كما تعلمنا كيفية استخدام:
attach() detach() sync()
لإدارة العلاقات Many to Many.
وتعرفنا أيضًا على:
with()
و:
load()
لاستخدام Eager Loading وتقليل مشكلة N+1 Query.
وتعلمنا أن العلاقات في Eloquent لا تعني فقط إمكانية الوصول إلى البيانات، بل يمكن استخدامها أيضًا لبناء استعلامات أكثر قوة باستخدام Methods مثل:
whereHas() has()
وأصبح لدينا الآن تصور واضح عن العلاقة بين:
Database ↓ Migrations ↓ Models ↓ Relationships ↓ Controllers ↓ Views
وهذا هو الأساس الذي سنبني عليه بقية تطبيق Laravel.
بعد أن تعلمنا Models وEloquent والعلاقات بين الجداول، أصبحنا جاهزين للانتقال إلى جزء عملي مهم جدًا: Factories وSeeders.
سنستخدمها لإنشاء بيانات تجريبية داخل قاعدة البيانات بدلًا من إدخال عشرات السجلات يدويًا.
وسنتعلم كيف ننشئ مستخدمين ومقالات وتعليقات تلقائيًا، وكيف نستخدم Faker لتوليد بيانات واقعية، وكيف نربط البيانات ببعضها باستخدام العلاقات التي تعلمناها في هذا الدرس.
وبذلك سننتقل من مجرد إنشاء الجداول والعلاقات إلى تعبئة قاعدة البيانات ببيانات حقيقية للاختبار والتطوير.
