recent
أخبار ساخنة

العلاقات بين الجداول HasMany في Laravel

الدرس العاشر: العلاقات بين الجداول في Laravel Eloquent – HasMany وBelongsTo وManyToMany

العلاقات بين الجداول في 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، لكن أهم ما يجب أن تتعلمه في البداية هو:

  1. One to One
  2. One to Many
  3. Belongs To
  4. Many to Many
  5. Has Many Through
  6. 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 لتوليد بيانات واقعية، وكيف نربط البيانات ببعضها باستخدام العلاقات التي تعلمناها في هذا الدرس.

وبذلك سننتقل من مجرد إنشاء الجداول والعلاقات إلى تعبئة قاعدة البيانات ببيانات حقيقية للاختبار والتطوير.

google-playkhamsatmostaqltradentX