404 Not Found

404 Not Found


nginx

ارتباطات Eloquent في Laravel

الارتباطات هي "القوة الخارقة" لـ Eloquent — بسطر واحد من الكود، يمكنك ربط الجداول عبر المفاتيح الأجنبية والوداع لـ JOINs اليدوية.

1. ما ستتعلمه


2. قصة حقيقعة لفريق تحليلات بيانات

(1) المشكلة: 50 استعلامًا فقط لعرض 10 طلبات

يرى Charlie 10 طلبات على لوحة تحكم ShopMetrics — يشغل استعلامًا واحدًا لاسترداد الطلبات، ثم لكل طلب، يشغل استعلامًا للمستخدم وواحدًا للمتجر وواحدًا للمنتج، بإجمالي 1 + 10 x 4 = 41 استعلام SQL. ارتفع حمل قاعدة البيانات بشكل كبير، وانتقل وقت تحميل الصفحة من 200 مللي ثانية إلى 3 ثوانٍ. أضاف Bob المزيد من الوصلات (طلب ← منتج ← فئة ← وسم)، مما رفع عدد الاستعلامات إلى أكثر من 200، وشكت Alice أن النظام "بطيء كالحلزون".

(2) حلول التحميل المسبق

يسترد التحميل المسبق with() في Eloquent جميع البيانات المرتبطة في استعلام واحد، مما يقلل 41 عبارة SQL إلى 4.

PHP
// مشكلة N+1 — 41 استعلامًا
$orders = Order::take(10)->get();
foreach ($orders as $order) {
    echo $order->user->name;    // +1 استعلام لكل طلب
    echo $order->shop->name;    // +1 استعلام لكل طلب
}

// تحميل مسبق — 4 استعلامات إجمالًا
$orders = Order::with(['user', 'shop', 'items.product'])->take(10)->get();
foreach ($orders as $order) {
    echo $order->user->name;    // 0 استعلامات إضافية
    echo $order->shop->name;    // 0 استعلامات إضافية
}

(3) العائد

بعد أن نفذ Charlie التحميل المسبق، انخفض عدد استعلامات لوحة التحكم من 41 إلى 4، وانخفض وقت تحميل الصفحة من 3 ثوانٍ إلى 300 مللي ثانية.


3. ارتباط واحد لواحد

(1) hasOne / belongsTo

PHP
// المستخدم لديه ملف شخصي واحد
class User extends Model
{
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }
}

// الملف الشخصي ينتمي لمستخدم
class Profile extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

// الاستخدام
$profile = $user->profile;
$user = $profile->user;
الاتجاه الطريقة موقع المفتاح الأجنبي الوصف
المستخدم ← الملف الشخصي hasOne جدول profiles يملك
الملف الشخصي ← المستخدم belongsTo جدول profiles ينتمي إلى

(1) ▶ مثال: مستخدمو ShopMetrics وخطط الاشتراك

PHP
// المستخدم لديه اشتراك نشط واحد
class User extends Model
{
    public function activeSubscription(): HasOne
    {
        return $this->hasOne(Subscription::class)
            ->where('status', 'active')
            ->latestOfMany();
    }
}

// الاشتراك ينتمي لمستخدم
class Subscription extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

// الاستخدام
$plan = $user->activeSubscription->plan;

الناتج:

TEXT
// تم التنفيذ بنجاح

4. ارتباط واحد لكثير

(1) hasMany / belongsTo

PHP
// المستأجر لديه عدة متاجر
class Tenant extends Model
{
    public function shops(): HasMany
    {
        return $this->hasMany(Shop::class);
    }

    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }
}

// المتجر ينتمي لمستأجر
class Shop extends Model
{
    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }

    public function products(): HasMany
    {
        return $this->hasMany(Product::class);
    }

    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }
}

(2) مخطط UML لارتباطات العلاقات السبع الرئيسية

100%
classDiagram
    class Tenant {
        +shops() HasMany
        +orders() HasMany
        +users() HasMany
        +subscription() HasOne
    }
    class User {
        +tenant() BelongsTo
        +profile() HasOne
        +orders() HasMany
    }
    class Shop {
        +tenant() BelongsTo
        +products() HasMany
        +orders() HasMany
    }
    class Product {
        +shop() BelongsTo
        +categories() BelongsToMany
    }
    class Category {
        +products() BelongsToMany
    }
    class Order {
        +shop() BelongsTo
        +user() BelongsTo
        +items() HasMany
    }
    class OrderItem {
        +order() BelongsTo
        +product() BelongsTo
    }
    Tenant "1" --> "*" Shop : hasMany
    Tenant "1" --> "*" User : hasMany
    Tenant "1" --> "1" Subscription : hasOne
    Shop "1" --> "*" Product : hasMany
    Shop "1" --> "*" Order : hasMany
    Product "*" --> "*" Category : belongsToMany
    Order "1" --> "*" OrderItem : hasMany

(1) ▶ مثال: المتاجر والطلبات في مستأجر ShopMetrics

PHP
// جلب المستأجر مع جميع متاجره وطلباته الأخيرة
$tenant = Tenant::with(['shops' => function ($query) {
    $query->withCount(['orders' => function ($q) {
        $q->where('created_at', '>=', now()->subDays(30));
    }])->orderBy('revenue', 'desc');
}])->findOrFail($tenantId);

foreach ($tenant->shops as $shop) {
    echo "{$shop->name}: {$shop->orders_count} طلبات حديثة";
}

الناتج:

TEXT
// تم التنفيذ بنجاح

5. ارتباط كثير لكثير

(1) belongsToMany والجداول المحورية

PHP
// المنتج ينتمي لعدة فئات (عبر جدول category_product المحوري)
class Product extends Model
{
    public function categories(): BelongsToMany
    {
        return $this->belongsToMany(Category::class)
            ->withPivot('is_primary')
            ->withTimestamps();
    }
}

class Category extends Model
{
    public function products(): BelongsToMany
    {
        return $this->belongsToMany(Product::class)
            ->withPivot('is_primary')
            ->withTimestamps();
    }
}

(2) بنية الجدول المحوري

TEXT
category_product
├── id
├── category_id (FK)
├── product_id  (FK)
├── is_primary  (BOOLEAN)
├── created_at
└── updated_at
طريقة المحور الغرض
withPivot() قراءة عمود محوري إضافي
withTimestamps() صيانة طابع المحور الزمني
as('alias') اسم مستعار للمحور
wherePivot() تصفية شروط المحور
sync() مزامنة الارتباط (تعيين الفرق)
attach() إضافة ارتباط
detach() إزالة ارتباط

(1) ▶ مثال: فئات منتجات ShopMetrics (كثير لكثير)

PHP
// إرفاق فئات بمنتج
$product->categories()->attach([1, 2, 3], ['is_primary' => false]);
$product->categories()->attach(4, ['is_primary' => true]);

// مزامنة — تعيين فئات محددة (يزيل الأخرى)
$product->categories()->sync([
    1 => ['is_primary' => false],
    4 => ['is_primary' => true],
]);

// مزامنة بدون فصل — إضافة فقط، بدون إزالة
$product->categories()->syncWithoutDetaching([5, 6]);

// استعلام مع شرط محوري
$primaryCategory = $product->categories()
    ->wherePivot('is_primary', true)
    ->first();

// فصل فئات محددة
$product->categories()->detach([1, 2]);

الناتج:

TEXT
// تم التنفيذ بنجاح

6. الارتباط البعيد والارتباط متعدد الأشكال

(1) HasManyThrough

PHP
// المستأجر لديه عدة منتجات عبر المتجر
class Tenant extends Model
{
    public function products(): HasManyThrough
    {
        return $this->hasManyThrough(
            Product::class,    // الهدف النهائي
            Shop::class,       // الوسيط
            'tenant_id',       // FK على shops
            'shop_id',         // FK على products
            'id',              // PK على tenants
            'id',              // PK على shops
        );
    }
}

// الاستخدام: وصول مباشر بدون تحميل المتاجر
$products = $tenant->products()->where('is_active', true)->get();

(2) الارتباطات متعددة الأشكال

PHP
// الصورة يمكن أن تنتمي لمتجر أو منتج (قابل للتشكيل)
class Image extends Model
{
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}

class Shop extends Model
{
    public function images(): MorphMany
    {
        return $this->morphMany(Image::class, 'imageable');
    }
}

class Product extends Model
{
    public function images(): MorphMany
    {
        return $this->morphMany(Image::class, 'imageable');
    }
}

// تهجير متعدد الأشكال
Schema::create('images', function (Blueprint $table) {
    $table->id();
    $table->morphs('imageable'); // imageable_type + imageable_id
    $table->string('path');
    $table->timestamps();
});
نوع الارتباط الطريقة حالة الاستخدام
واحد لواحد hasOne/belongsTo المستخدم ← الملف الشخصي
واحد لكثير hasMany/belongsTo المستأجر ← المتجر
كثير لكثير belongsToMany المنتج ↔ الفئة
واحد لكثير بعيد hasManyThrough المستأجر ← المنتج (عبر المتجر)
متعدد الأشكال واحد لواحد morphOne/morphTo صورة ← منتج/متجر
متعدد الأشكال واحد لكثير morphMany/morphTo تعليقات ← منتجات/مقالات
متعدد الأشكال كثير لكثير morphToMany/morphByMany وسم ← منتج/مقالة

(1) ▶ مثال: نظام صور متعدد الأشكال لـ ShopMetrics

PHP
// إضافة صورة لمتجر
$shop->images()->create(['path' => 'shops/alice-store/banner.jpg']);

// إضافة صورة لمنتج
$product->images()->create(['path' => 'products/widget-a/thumb.jpg']);

// استعلام متعدد الأشكال — جلب مالك الصورة
$image = Image::find(1);
$image->imageable; // يعيد نسخة Shop أو Product

// تحميل مسبق متعدد الأشكال
$images = Image::with('imageable')->get();
foreach ($images as $image) {
    echo $image->imageable->name; // يعمل لكل من Shop وProduct
}

الناتج:

TEXT
// تم التنفيذ بنجاح

7. التحميل المسبق وتحسين N+1

(1) مشكلة N+1

TEXT
بدون تحميل مسبق:
1. SELECT * FROM orders WHERE tenant_id = 1 LIMIT 10     -- 1 استعلام
2. SELECT * FROM users WHERE id = 1                       -- +1 لكل طلب
3. SELECT * FROM users WHERE id = 2
4. SELECT * FROM shops WHERE id = 5
... (حتى 30+ استعلامًا لـ 10 طلبات)

(2) التحميل المسبق مع with()

PHP
// تحميل مسبق — 4 استعلامات إجمالًا
$orders = Order::with(['user', 'shop', 'items.product'])->paginate(15);

// تحميل مسبق متداخل
$orders = Order::with(['items.product.categories'])->get();

// تحميل مسبق شرطي
$orders = Order::with(['items' => function ($query) {
    $query->where('quantity', '>', 1);
}])->get();

// تحميل مسبق كسول — تحميل بعد الاستعلام الأولي
$orders = Order::all();
if ($needItems) {
    $orders->load('items.product');
}
الطريقة التوقيت السيناريوهات المناسبة
with() تحميل عند الاستعلام معروف الحاجة للربط
load() تحميل بعد الاستعلام تحميل شرطي
loadCount() تحميل العدد فقط كمية فقط، لا بيانات مطلوبة
loadMissing() تحميل عند الفقد تجنب إعادة التحميل

(1) ▶ مثال: تحسين N+1 في لوحة تحكم ShopMetrics

PHP
// سيء — مشكلة N+1 في لوحة التحكم
$shops = Shop::where('tenant_id', $tenantId)->get();
foreach ($shops as $shop) {
    echo $shop->orders->count();       // +1 استعلام لكل متجر
    echo $shop->products->count();     // +1 استعلام لكل متجر
}

// جيد — withCount + تحميل مسبق
$shops = Shop::where('tenant_id', $tenantId)
    ->withCount(['orders', 'products', 'orders as recent_orders_count' => function ($q) {
        $q->where('created_at', '>=', now()->subDays(30));
    }])
    ->with(['latestOrder'])
    ->get();

foreach ($shops as $shop) {
    echo $shop->orders_count;           // 0 استعلامات إضافية
    echo $shop->recent_orders_count;    // 0 استعلامات إضافية
    echo $shop->latestOrder->total;     // 0 استعلامات إضافية
}

الناتج:

TEXT
// تم التنفيذ بنجاح

8. مثال شامل: تجميع بيانات مستأجر ShopMetrics

PHP
// ============================================
// شامل: تجميع بيانات مستأجر ShopMetrics
// يغطي: جميع أنواع الارتباطات، التحميل المسبق، المحور، متعدد الأشكال
// ============================================

// app/Models/Tenant.php
class Tenant extends Model
{
    public function users(): HasMany
    {
        return $this->hasMany(User::class);
    }

    public function shops(): HasMany
    {
        return $this->hasMany(Shop::class);
    }

    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }

    public function subscription(): HasOne
    {
        return $this->hasOne(Subscription::class)->where('status', 'active');
    }

    public function products(): HasManyThrough
    {
        return $this->hasManyThrough(Product::class, Shop::class);
    }

    public function scopeWithStats(Builder $query): Builder
    {
        return $query->withCount([
            'shops as active_shops_count' => fn ($q) => $q->where('status', 'active'),
            'orders as monthly_orders_count' => fn ($q) => $q->whereBetween('created_at', [
                now()->startOfMonth(), now()->endOfMonth(),
            ]),
        ])->withSum('orders as total_revenue', 'total');
    }
}

// الاستخدام — استعلام واحد بكل شيء
$tenant = Tenant::withStats()
    ->with(['subscription.plan', 'shops' => fn ($q) => $q->orderBy('revenue', 'desc')->take(5)])
    ->findOrFail($tenantId);

$tenant->active_shops_count;     // 15
$tenant->monthly_orders_count;   // 234
$tenant->total_revenue;          // 45678.90
$tenant->subscription->plan->name; // Pro
$tenant->shops->first()->name;   // Alice Store

❓ أسئلة شائعة

س كيف أحدد أي ارتباط أستخدم؟
ج انظر إلى موقع المفتاح الأجنبي — إذا كان المفتاح الأجنبي في الجدول الآخر، استخدم hasOne أو hasMany؛ إذا كان المفتاح الأجنبي في هذا الجدول، استخدم belongsTo؛ إذا لم يكن أي جدول به مفتاح أجنبي، استخدم belongsToMany (يتطلب جدولًا محوريًا). يمكن أن ينتمي النموذج لعدة أنواع باستخدام الارتباطات متعددة الأشكال.
س متى أستخدم with() وload()؟
ج with() يحمل البيانات أثناء الاستعلام، مما يوفر أفضل أداء؛ load() يحمل البيانات عند الطلب بعد الاستعلام، مما يجعله مناسبًا للتحميل الشرطي (مثل الحاجة للبيانات المرتبطة فقط في ظل شروط معينة).
س ما الفرق بين "sync" و"attach"؟
ج "attach" يضيف ارتباطًا فقط (بدون إزالة القديم)، بينما "sync" يعين قائمة ارتباطات محددة (أي إدخالات زائدة يتم "فصلها"). استخدم "sync" لـ "الاستبدال الكامل" و"attach" لـ "الإضافة التدريجية".
س هل يؤثر الربط متعدد الأشكال على أداء الاستعلام؟
ج يستخدم الربط متعدد الأشكال عمود imageable_type للتمييز بين الأنواع، لذا لا يمكن إنشاء قيود مفتاح أجنبي تقليدية. إضافة فهرس على (imageable_type, imageable_id) يمكن أن يحسن الأداء. لمجموعات البيانات الكبيرة، فكر في استخدام جداول منفصلة بدلاً من الربط متعدد الأشكال.
س كيف أكتشف مشكلة N+1؟
ج ثبّت Laravel Debugbar وتحقق من عدد استعلامات SQL في كل صفحة. إذا كان أكثر من 20، فعادةً يشير إلى مشكلة N+1. يمكنك أيضًا استخدام DB::listen() لتسجيل عدد الاستعلامات أو استخدام laravel/telescope للمراقبة.
س ما الفرق بين استخدام طريقة الارتباط بأقواس وبدون أقواس؟
ج $shop->products هي خاصية ديناميكية (تعيد Collection)، بينما $shop->products() هي منشئ استعلام ارتباطي (يسمح بالاستعلامات المتسلسلة). الأول ينفذ الاستعلام تلقائيًا، بينما الثاني يؤخره حتى استدعاء get() يدويًا.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): حدد ارتباط Tenant←Shop←Product لـ ShopMetrics. استخدم Tinker لإنشاء بيانات اختبار والوصول إلى الارتباط: $tenant->shops->first()->products.

  2. تمرين متقدم (⭐⭐): نفذ ارتباط كثير لكثير بين Product وCategory، أنشئ تهجيرًا محوريًا يتضمن عمود is_primary، استخدم sync() لمزامنة الفئات، واستعلام عن الفئة الرئيسية لمنتج معين.

  3. تحدٍ (⭐⭐⭐): نفذ نظام تعليقات متعدد الأشكال (حيث يمكن ربط نموذج Comment بكل من المنتجات والمتاجر) وحمّل مسبقًا جميع التعليقات وارتباطات commentable في لوحة التحكم، مع التأكد من الحاجة لـ 3 عبارات SQL فقط (1 لاسترداد التعليقات + 1 لاسترداد المنتجات + 1 لاسترداد المتاجر).

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%