العلاقة One To Many في Laravel: المرجع الشامل من المبتدئ إلى المتقدم
تُعد العلاقات بين الجداول من أهم المفاهيم التي يجب إتقانها عند العمل باستخدام Laravel وEloquent ORM، ومن أكثر هذه العلاقات استخدامًا علاقة One To Many أو واحد إلى متعدد.
في هذا الدليل لن نكتفي بإنشاء العلاقة باستخدام hasMany() وbelongsTo() فقط، وإنما سنبدأ من تصميم قاعدة البيانات وفهم الـForeign Key، ثم ننتقل إلى إدخال واسترجاع وتحديث وفلترة البيانات، وبعد ذلك إلى موضوعات أكثر تقدمًا مثل Eager Loading، ومشكلة N+1، وwhereHas()، وwithCount()، والاستعلامات التي ينفذها Laravel تقريبًا خلف الكواليس.
تم تقسيم الدليل إلى ثلاثة مستويات:
- Beginner: فهم العلاقة وإنشاؤها والتعامل الأساسي معها.
- Intermediate: الاستعلامات والفلترة وEager Loading.
- Advanced: الأداء، N+1، SQL، العلاقات المقيدة، وتصميم التطبيقات الاحترافية.
الهدف من هذا المقال ليس حفظ أوامر Laravel، وإنما فهم الطريقة التي يفكر بها Eloquent عند التعامل مع العلاقات.
جدول المحتويات
- المستوى الأول: Beginner
- ما هي علاقة One To Many؟
- تصميم العلاقة في قاعدة البيانات
- إنشاء Models وMigrations
- إنشاء Foreign Key
- تعريف hasMany
- تعريف belongsTo
- استرجاع البيانات
- إدخال البيانات
- ```
- المستوى الثاني: Intermediate
- الفرق بين products و products()
- Lazy Loading
- Eager Loading
- فلترة العلاقات
- whereHas وwhereDoesntHave
- withCount
- تحديث وربط العلاقات
- حذف البيانات
- المستوى الثالث: Advanced
- SQL خلف Eloquent
- مشكلة N+1
- اختيار الأعمدة المطلوبة
- العلاقات المقيدة
- Pagination مع العلاقات
- تحسين الأداء
- أخطاء شائعة
- Controller متكامل
- تمارين عملية
- أسئلة مقابلات Laravel
- ```
المستوى الأول: Beginner
ما هي علاقة One To Many؟
علاقة One To Many تعني أن سجلًا واحدًا في جدول معين يستطيع الارتباط بعدة سجلات في جدول آخر، بينما كل سجل من الجدول الثاني ينتمي إلى سجل واحد فقط من الجدول الأول.
سنستخدم طوال المقال مثال متجر إلكتروني يحتوي على:
- جدول للعلامات التجارية
brands. - جدول للمنتجات
products.
الـBrand الواحد يمكن أن يحتوي على عدد كبير من المنتجات.
Apple
│
├── iPhone 15
├── iPhone 15 Pro
├── MacBook Pro
└── iPad Proبالتالي:
One Brand → Many Productsأمثلة أخرى من التطبيقات الحقيقية
User → Posts
Post → Comments
Category → Products
Country → Cities
Order → Order Items
Course → Lessons
Brand → Productsإذا استطعت أن تقول: "السجل X يمتلك عدة سجلات من Y، وكل Y ينتمي إلى X واحد"، فأنت غالبًا أمام علاقة One To Many.
تصميم علاقة One To Many داخل قاعدة البيانات
من الأخطاء الشائعة الاعتقاد أن Laravel هو الذي يصنع مفهوم One To Many.
في الحقيقة العلاقة تبدأ من تصميم قاعدة البيانات نفسها.
لدينا:
brandsو:
productsيجب أن يحتوي جدول المنتجات على عمود يحدد الـBrand الذي ينتمي إليه المنتج:
brand_idالشكل الكامل للعلاقة
┌─────────────────────┐
│ brands │
├─────────────────────┤
│ id │
│ name │
│ created_at │
│ updated_at │
└──────────┬──────────┘
│
│ 1
│
│
│ many
▼
┌─────────────────────┐
│ products │
├─────────────────────┤
│ id │
│ brand_id │
│ name │
│ price │
│ qty │
│ created_at │
│ updated_at │
└─────────────────────┘لاحظ أن:
products.brand_id → brands.idوهذا هو أساس العلاقة بالكامل.
مثال على البيانات
brands
+----+---------+
| id | name |
+----+---------+
| 1 | Samsung |
| 2 | Apple |
+----+---------+ثم:
products
+----+----------+----------------+
| id | brand_id | name |
+----+----------+----------------+
| 1 | 2 | iPhone 15 |
| 2 | 2 | MacBook Pro |
| 3 | 1 | Galaxy S25 |
+----+----------+----------------+يمكننا الآن معرفة:
iPhone 15 → brand_id = 2 → Apple
MacBook Pro → brand_id = 2 → Apple
Galaxy S25 → brand_id = 1 → Samsungفي علاقة One To Many يكون الـForeign Key عادة في جدول جهة الـMany، أي جدول الأبناء.
إنشاء Models وMigrations
إنشاء Brand
php artisan make:model Brand -mإنشاء Product
php artisan make:model Product -mالخيار:
-mيجعل Laravel ينشئ ملف Migration مع الـModel.
إنشاء جدول brands
Schema::create('brands', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});إنشاء جدول products والـForeign Key
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('brand_id')
->constrained()
->cascadeOnDelete();
$table->string('name');
$table->string('slug')
->unique();
$table->decimal('price', 10, 2);
$table->unsignedInteger('qty')
->default(0);
$table->timestamps();
});ماذا يفعل foreignId؟
$table->foreignId('brand_id');ينشئ عمودًا مناسبًا لتخزين معرف الـBrand.
ماذا يفعل constrained؟
->constrained()يعتمد Laravel على Naming Conventions ويستنتج أن:
brand_idيشير إلى:
brands.idأي أن:
products.brand_id
│
└──────→ brands.idماذا يفعل cascadeOnDelete؟
->cascadeOnDelete()يعني أنه إذا تم حذف Brand، تقوم قاعدة البيانات بحذف المنتجات التابعة له أيضًا.
Apple
│
├── iPhone
├── MacBook
└── iPadعند حذف:
Appleيتم حذف المنتجات المرتبطة به.
لا تجعل Cascade Delete خيارًا افتراضيًا في كل مشروع. أحيانًا يكون حذف الأبناء خطرًا، خصوصًا في الأنظمة المالية وأنظمة الطلبات والسجلات التاريخية.
تعريف علاقة hasMany
داخل:
app/Models/Brand.phpنكتب:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Brand extends Model
{
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}السطر:
return $this->hasMany(Product::class);يعني:
هذا الـBrand يمتلك عدة Products.
ماذا يفترض Laravel خلف الكواليس؟
بسبب اسم Model:
Brandيفترض Laravel أن الـForeign Key هو:
brand_idوأنه موجود في جدول:
productsتعريف العلاقة العكسية belongsTo
حتى الآن نستطيع الانتقال:
Brand → Productsلكن نحتاج أيضًا إلى الانتقال بالعكس:
Product → Brandداخل:
app/Models/Product.phpنكتب:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Product extends Model
{
public function brand(): BelongsTo
{
return $this->belongsTo(Brand::class);
}
}المخطط الكامل
Brand
│
│ hasMany
▼
Products
Product
│
│ belongsTo
▼
Brandالعلاقة العكسية لـhasMany هي belongsTo، وليست belongsToMany. belongsToMany تُستخدم لعلاقات Many To Many.
استخدام Foreign Key مخصص
الأفضل الالتزام بتسمية Laravel:
brand_idلكن لنفترض أن قاعدة البيانات تستخدم:
company_brand_idيمكن تحديده يدويًا:
public function products(): HasMany
{
return $this->hasMany(
Product::class,
'company_brand_id'
);
}وفي Product:
public function brand(): BelongsTo
{
return $this->belongsTo(
Brand::class,
'company_brand_id'
);
}تحديد Foreign Key وLocal Key
return $this->hasMany(
Product::class,
'company_brand_id',
'id'
);الترتيب:
hasMany(
Related Model,
Foreign Key,
Local Key
)استرجاع البيانات من العلاقة
جلب Brand
$brand = Brand::find(1);جلب المنتجات التابعة له
$products = $brand->products;القيمة:
$brand->productsستكون Collection تحتوي جميع المنتجات التي يكون:
brand_id = 1عرض المنتجات
foreach ($brand->products as $product) {
echo $product->name;
}جلب Brand من Product
$product = Product::find(1);
$brand = $product->brand;يمكن كتابة:
echo $product->brand->name;إدخال البيانات باستخدام العلاقة
الطريقة الأولى: save
$brand = Brand::findOrFail(1);
$product = new Product();
$product->name = 'iPhone 15 Pro';
$product->slug = 'iphone-15-pro';
$product->price = 999.99;
$product->qty = 10;
$brand->products()->save($product);Laravel يقوم بتحديد:
brand_idتلقائيًا من العلاقة.
الطريقة الثانية: create
$brand = Brand::findOrFail(1);
$product = $brand->products()->create([
'name' => 'iPhone 15 Pro',
'slug' => 'iphone-15-pro',
'price' => 999.99,
'qty' => 10,
]);إذا كنت تستخدم Mass Assignment يجب إعداد:
protected $fillable = [
'name',
'slug',
'price',
'qty',
];إدخال عدة Products باستخدام createMany
$brand->products()->createMany([
[
'name' => 'iPhone 15',
'slug' => 'iphone-15',
'price' => 799,
'qty' => 10,
],
[
'name' => 'iPhone 15 Pro',
'slug' => 'iphone-15-pro',
'price' => 999,
'qty' => 5,
],
]);استخدام saveMany
$brand->products()->saveMany([
new Product([
'name' => 'iPhone 15',
'slug' => 'iphone-15',
'price' => 799,
'qty' => 10,
]),
new Product([
'name' => 'iPhone 15 Pro',
'slug' => 'iphone-15-pro',
'price' => 999,
'qty' => 5,
]),
]);المستوى الثاني: Intermediate
الفرق بين products و products()
هذه من أهم النقاط في علاقات Eloquent.
الحالة الأولى
$brand->productsتعني:
أعطني البيانات الناتجة من العلاقة.
الحالة الثانية
$brand->products()تعني:
أعطني Relationship Query Builder حتى أستطيع إضافة شروط على الاستعلام.
مثال
$products = $brand->products()
->where('price', '>', 500)
->get();يمكن أيضًا:
$products = $brand->products()
->where('qty', '>', 0)
->orderByDesc('price')
->get();قاعدة سهلة للحفظ
$brand->products
= Result
$brand->products()
= QueryLazy Loading
لننظر إلى:
$brand = Brand::find(1);
$products = $brand->products;الاستعلام الأول يجلب Brand.
بعد ذلك، عندما يصل Laravel إلى:
$brand->productsيقوم بتنفيذ استعلام إضافي للحصول على المنتجات.
يسمى ذلك:
Lazy Loadingأي تحميل العلاقة عندما نحتاج إليها.
Eager Loading
إذا كنا نعرف مسبقًا أننا سنحتاج Products، يمكن تحميلها من البداية:
$brands = Brand::with('products')->get();يسمى ذلك:
Eager Loadingوهو مهم جدًا لتحسين الأداء عندما نتعامل مع عدة Models.
تحميل علاقة بعد جلب Model
$brand = Brand::findOrFail(1);
$brand->load('products');الفرق بين with وload
with()
تستخدم أثناء بناء Query.مثال:
Brand::with('products')->get();بينما:
load()تستخدم عندما يكون Model موجودًا بالفعل.
$brand = Brand::find(1);
$brand->load('products');تحميل أكثر من علاقة
Brand::with([
'products',
'country',
'owner'
])->get();Nested Relationships
إذا كان Product يحتوي Reviews:
Brand
│
▼
Products
│
▼
Reviewsيمكن:
Brand::with('products.reviews')->get();فلترة المنتجات المرتبطة
لنفرض أننا نريد Brands كاملة، لكن نريد تحميل المنتجات التي يزيد سعرها عن 500 فقط.
$brands = Brand::with([
'products' => function ($query) {
$query->where('price', '>', 500)
->orderBy('name');
}
])->get();هذا الاستعلام لا يقوم بفلترة Brands نفسها. هو يعيد Brands ثم يحدد المنتجات التي سيتم تحميلها لكل Brand.
فلترة Parent باستخدام whereHas
إذا أردنا فقط Brands التي لديها منتج سعره أكبر من 500:
$brands = Brand::whereHas(
'products',
function ($query) {
$query->where('price', '>', 500);
}
)->get();الفرق المهم
with()
→ يحدد البيانات التي سيتم تحميلها من Relationship.
whereHas()
→ يحدد أي Parent Models يجب إرجاعها.الجمع بين whereHas وwith
نريد:
- Brands التي لديها Products سعرها أكبر من 500.
- تحميل Products التي سعرها أكبر من 500 فقط.
$brands = Brand::whereHas(
'products',
function ($query) {
$query->where('price', '>', 500);
}
)
->with([
'products' => function ($query) {
$query->where('price', '>', 500);
}
])
->get();doesntHave
للحصول على Brands التي لا تحتوي أي Products:
$brands = Brand::doesntHave('products')
->get();whereDoesntHave
للحصول على Brands التي لا تحتوي منتجات متوفرة:
$brands = Brand::whereDoesntHave(
'products',
function ($query) {
$query->where('qty', '>', 0);
}
)->get();حساب عدد المنتجات باستخدام withCount
إذا كنا نريد فقط معرفة عدد المنتجات، ليس من المنطقي تحميلها جميعًا.
يمكن:
$brands = Brand::withCount('products')
->get();Laravel سيضيف خاصية:
products_countمثال:
foreach ($brands as $brand) {
echo $brand->name;
echo $brand->products_count;
}استخدام has
للحصول على Brands لديها Products:
$brands = Brand::has('products')
->get();Brands التي لديها خمسة Products أو أكثر:
$brands = Brand::has(
'products',
'>=',
5
)->get();ربط Product موجود بBrand
إذا كان Product موجودًا بالفعل:
$product = Product::findOrFail(10);
$brand = Brand::findOrFail(2);
$product->brand()->associate($brand);
$product->save();أصبح:
product.brand_id = 2تغيير Brand الخاص بالمنتج
يمكن استخدام نفس الطريقة:
$product = Product::findOrFail(10);
$newBrand = Brand::findOrFail(5);
$product->brand()->associate($newBrand);
$product->save();حذف Product
$product = Product::findOrFail(1);
$product->delete();حذف جميع منتجات Brand
$brand = Brand::findOrFail(1);
$brand->products()->delete();لاحظ أن هذا لا يحذف Brand نفسه.
حذف Brand باستخدام Cascade Delete
إذا كان Migration يحتوي:
->cascadeOnDelete()فيمكن:
$brand = Brand::findOrFail(1);
$brand->delete();وتقوم قاعدة البيانات بحذف Products التابعة له.
المستوى الثالث: Advanced
ما هو SQL الذي ينفذه Eloquent خلف الكواليس؟
فهم SQL التقريبي خلف Eloquent يجعل التعامل مع العلاقات أسهل بكثير.
الـSQL التالي توضيحي وتقريبي، وقد يختلف قليلًا حسب Database Driver وإصدار Laravel والاستعلام المستخدم.
Brand::find(1)
Eloquent:
$brand = Brand::find(1);SQL تقريبًا:
SELECT *
FROM brands
WHERE id = 1
LIMIT 1;$brand->products
$products = $brand->products;SQL تقريبًا:
SELECT *
FROM products
WHERE brand_id = 1;belongsTo
$product = Product::find(10);
$brand = $product->brand;إذا كان:
product.brand_id = 2فسيكون الاستعلام تقريبًا:
SELECT *
FROM brands
WHERE id = 2
LIMIT 1;فلترة Relationship
$brand->products()
->where('price', '>', 500)
->get();SQL تقريبًا:
SELECT *
FROM products
WHERE brand_id = 1
AND price > 500;مع شرط الكمية
$brand->products()
->where('qty', '>', 0)
->orderByDesc('price')
->get();SQL تقريبًا:
SELECT *
FROM products
WHERE brand_id = 1
AND qty > 0
ORDER BY price DESC;SQL التقريبي لـEager Loading
لدينا:
$brands = Brand::with('products')
->get();قد ينفذ Laravel استعلامًا لجلب Brands:
SELECT *
FROM brands;ثم استعلامًا آخر لجلب جميع Products المطلوبة:
SELECT *
FROM products
WHERE brand_id IN (1, 2, 3, 4, 5);ثم يقوم Eloquent بتوزيع Products داخل الـBrand المناسب في الذاكرة.
وهنا تكمن فائدة Eager Loading: بدل تنفيذ Query منفصل لكل Brand، يتم جلب المنتجات المطلوبة في عدد أقل من الاستعلامات.
مشكلة N+1
من أشهر مشاكل الأداء عند العمل مع ORM.
لننظر إلى:
$brands = Brand::all();
foreach ($brands as $brand) {
echo $brand->name;
foreach ($brand->products as $product) {
echo $product->name;
}
}إذا كان لدينا 100 Brand، قد يحدث:
1 Query لجلب Brands
+
100 Queries لجلب Products
=
101 Queriesوهذا يسمى:
N + 1 Problemالحل
$brands = Brand::with('products')
->get();في السيناريو المعتاد تصبح العملية أقرب إلى:
Query 1:
SELECT * FROM brands;
Query 2:
SELECT *
FROM products
WHERE brand_id IN (...);أي تقريبًا:
2 Queriesبدل:
101 Queriesكيف تكتشف N+1؟
أثناء التطوير، راقب عدد الاستعلامات التي ينفذها التطبيق.
إذا رأيت نفس Query يتكرر عشرات أو مئات المرات مع اختلاف ID فقط، فهذه علامة قوية على وجود N+1.
SELECT * FROM products WHERE brand_id = 1;
SELECT * FROM products WHERE brand_id = 2;
SELECT * FROM products WHERE brand_id = 3;
SELECT * FROM products WHERE brand_id = 4;
SELECT * FROM products WHERE brand_id = 5;غالبًا يمكن تحسينها باستخدام:
with()اختيار الأعمدة المطلوبة
ليس من الضروري دائمًا تحميل كل أعمدة الجداول.
بدل:
Brand::with('products')->get();يمكن:
$brands = Brand::select(
'id',
'name'
)
->with(
'products:id,brand_id,name,price'
)
->get();عند تحديد Columns الخاصة بالـRelationship، لا تنس مفتاح الربط مثل brand_id، وإلا قد لا يستطيع Eloquent بناء العلاقة كما تتوقع.
مثال خاطئ
Brand::with(
'products:id,name,price'
)->get();المشكلة أننا حذفنا:
brand_idالصحيح
Brand::with(
'products:id,brand_id,name,price'
)->get();إنشاء Relationship بشروط ثابتة
أحيانًا نستخدم Products كاملة:
public function products(): HasMany
{
return $this->hasMany(Product::class);
}لكن نحتاج أيضًا علاقة خاصة بالمنتجات المتوفرة.
public function availableProducts(): HasMany
{
return $this->hasMany(Product::class)
->where('qty', '>', 0);
}الآن:
$brand->availableProductsيعيد فقط المنتجات التي:
qty > 0إنشاء علاقة للمنتجات الغالية
public function expensiveProducts(): HasMany
{
return $this->hasMany(Product::class)
->where('price', '>', 1000);
}ثم:
$brand->expensiveProducts;ترتيب افتراضي داخل Relationship
public function products(): HasMany
{
return $this->hasMany(Product::class)
->orderBy('name');
}لكن انتبه:
إذا أضفت orderBy داخل تعريف Relationship، سيصبح هذا الترتيب جزءًا افتراضيًا من العلاقة في كل مكان تستخدمها فيه.
إذا كنت تحتاجه في مكان واحد فقط، قد يكون الأفضل:
$brand->products()
->orderBy('name')
->get();فلترة متقدمة
نريد Brands لديها Products:
- متوفرة.
- سعرها بين 500 و1500.
ثم نريد تحميل هذه المنتجات وترتيبها من الأغلى إلى الأرخص.
$brands = Brand::query()
->whereHas(
'products',
function ($query) {
$query->where('qty', '>', 0)
->whereBetween(
'price',
[500, 1500]
);
}
)
->with([
'products' => function ($query) {
$query->where('qty', '>', 0)
->whereBetween(
'price',
[500, 1500]
)
->orderByDesc('price');
}
])
->get();ماذا يحدث هنا؟
الجزء:
whereHas()يقرر أي Brands يتم إرجاعها.
والجزء:
with()يحدد أي Products يتم تحميلها داخل Brands.
Pagination مع العلاقات
في المشاريع الحقيقية قد يكون لديك آلاف Brands وعشرات الآلاف من Products.
لا تستخدم:
Brand::with('products')->get();على مجموعة بيانات ضخمة دون سبب.
يمكن:
$brands = Brand::with(
'products:id,brand_id,name,price'
)
->paginate(20);وبذلك يتم جلب 20 Brand لكل صفحة بدل تحميل الجدول كاملًا.
قواعد مهمة لتحسين الأداء
1. تجنب N+1
استخدم:
with()عندما تعرف أنك ستحتاج إلى العلاقة.
2. لا تحمل بيانات لن تستخدمها
استخدم:
select()عند الحاجة.
3. استخدم withCount بدل تحميل Relation للحساب فقط
بدل:
$brand->products->count();إذا كان هدفك مجرد العدد لعدد كبير من Brands، استخدم:
Brand::withCount('products')
->get();4. استخدم Pagination
خصوصًا في APIs وصفحات الإدارة.
5. دع قاعدة البيانات تنفذ Filtering
يفضل:
Product::where('price', '>', 500)
->get();على:
Product::all()
->where('price', '>', 500);في الأولى يتم تنفيذ الفلترة في قاعدة البيانات.
أما الثانية فتحمل البيانات أولًا إلى PHP ثم تقوم Collection بالفلترة.
أشهر الأخطاء في One To Many
الخطأ الأول: استخدام belongsToMany
الخطأ:
return $this->belongsToMany(Brand::class);إذا كان Product ينتمي إلى Brand واحد.
الصحيح:
return $this->belongsTo(Brand::class);الخطأ الثاني: وضع Foreign Key في الجدول الخطأ
في مثالنا يجب أن يكون:
products.brand_idوليس:
brands.product_idلأن Brand يمتلك عدة Products وليس Product واحدًا فقط.
الخطأ الثالث: Naming Conventions غير واضحة
يفضل:
brand_idبدل أسماء مثل:
brands_id
brandID
brandId
id_brandيمكن لـLaravel التعامل معها، لكن ستحتاج إلى إعدادات إضافية.
الخطأ الرابع: نسيان Eager Loading
هذا:
$brands = Brand::all();
foreach ($brands as $brand) {
echo $brand->products;
}قد يؤدي إلى N+1.
الأفضل:
$brands = Brand::with('products')
->get();الخطأ الخامس: نسيان Foreign Key عند Select
الخطأ:
Brand::with(
'products:id,name'
)->get();الأفضل:
Brand::with(
'products:id,brand_id,name'
)->get();الخطأ السادس: استخدام find دون فحص null
$brand = Brand::find(500);
$brand->products;إذا لم يوجد Brand:
$brand = nullلذلك في الحالات التي يجب أن يكون فيها السجل موجودًا:
$brand = Brand::findOrFail(500);مثال Controller متكامل
عرض Brands
public function index()
{
$brands = Brand::query()
->select(
'id',
'name'
)
->with([
'products:id,brand_id,name,slug,price,qty'
])
->withCount('products')
->paginate(20);
return response()->json([
'data' => $brands
]);
}عرض Brand واحد
public function show(Brand $brand)
{
$brand->load([
'products:id,brand_id,name,slug,price,qty'
]);
return response()->json([
'data' => $brand
]);
}إنشاء Product
public function store(
Request $request,
Brand $brand
) {
$data = $request->validate([
'name' => [
'required',
'string',
'max:255'
],
'slug' => [
'required',
'string',
'max:255',
'unique:products,slug'
],
'price' => [
'required',
'numeric',
'min:0'
],
'qty' => [
'required',
'integer',
'min:0'
],
]);
$product = $brand
->products()
->create($data);
return response()->json([
'message' => 'Product created successfully',
'data' => $product,
], 201);
}لماذا لم نستقبل brand_id؟
لأن Brand معروف من:
Brand $brandثم قمنا بالإنشاء من خلال:
$brand->products()->create($data);وهذا يجعل علاقة المنتج بالـBrand واضحة ويمنع تمرير brand_id من المستخدم بلا حاجة.
مخطط دورة العلاقة بالكامل
DATABASE
│
│
┌─────────▼─────────┐
│ brands │
│ │
│ id = 1 │
│ name = Apple │
└─────────┬─────────┘
│
│ hasMany
│
┌─────────▼─────────┐
│ products │
│ │
│ brand_id = 1 │
├───────────────────┤
│ iPhone │
│ MacBook │
│ iPad │
└───────────────────┘
Laravel:
$brand->products
↓
SELECT *
FROM products
WHERE brand_id = 1;
Product:
$product->brand
↓
SELECT *
FROM brands
WHERE id = product.brand_id
LIMIT 1;تمارين عملية
إذا أردت التأكد أنك فهمت علاقة One To Many، حاول حل التمارين التالية دون الرجوع إلى الحل مباشرة.
التمرين الأول
لديك:
Category
Productكل Category تحتوي عدة Products.
المطلوب:
- إنشاء Migration.
- إنشاء Foreign Key.
- تعريف hasMany.
- تعريف belongsTo.
الحل المختصر
// Category
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
// Product
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}التمرين الثاني
اكتب Query يعيد Brand رقم 5 والمنتجات التابعة له فقط إذا كانت:
- متوفرة.
- سعرها أكبر من 100.
الحل
$brand = Brand::findOrFail(5);
$products = $brand->products()
->where('qty', '>', 0)
->where('price', '>', 100)
->get();التمرين الثالث
أعد جميع Brands التي لديها منتج واحد على الأقل سعره أكبر من 1000.
الحل
$brands = Brand::whereHas(
'products',
function ($query) {
$query->where(
'price',
'>',
1000
);
}
)->get();التمرين الرابع
أعد Brands مع عدد Products لكل Brand بدون تحميل Products.
الحل
$brands = Brand::withCount('products')
->get();التمرين الخامس
لديك 500 Brand، والكود التالي بطيء:
$brands = Brand::all();
foreach ($brands as $brand) {
foreach ($brand->products as $product) {
echo $product->name;
}
}ما المشكلة؟
الإجابة
احتمال وجود:
N+1 Query Problemوالحل:
$brands = Brand::with('products')
->get();أسئلة مقابلات Laravel حول One To Many
السؤال الأول
ما الفرق بين:
hasMany()و:
belongsTo()الإجابة
hasMany() يتم تعريفها في جهة الأب الذي يمتلك عدة سجلات، بينما belongsTo() يتم تعريفها في جهة الابن الذي يحتوي عادة على الـForeign Key.
السؤال الثاني
أين يوضع Foreign Key في علاقة One To Many؟
الإجابة
عادة في جدول جهة الـMany.
مثال:
Brand → Products
products.brand_idالسؤال الثالث
ما الفرق بين:
$brand->productsو:
$brand->products()الإجابة
الأولى تعيد البيانات الناتجة من Relationship، بينما الثانية تعيد Relationship Query Builder ويمكن إضافة شروط مثل:
$brand->products()
->where('price', '>', 500)
->get();السؤال الرابع
ما هي مشكلة N+1؟
الإجابة
هي تنفيذ Query إضافي لكل Model داخل Loop عند تحميل Relationship باستخدام Lazy Loading.
ومن أشهر طرق حلها:
with()السؤال الخامس
ما الفرق بين:
with()و:
load()الإجابة
with() تستخدم أثناء إنشاء Query، بينما load() تستخدم عندما يكون Model قد تم جلبه بالفعل.
السؤال السادس
ما الفرق بين:
with()و:
whereHas()الإجابة
with() تستخدم لتحميل Relationship، بينما whereHas() تستخدم لفلترة Parent Models بناءً على شروط موجودة في Relationship.
السؤال السابع
كيف تحصل على عدد Products لكل Brand دون تحميل Products نفسها؟
الإجابة
Brand::withCount('products')
->get();السؤال الثامن
كيف تحصل على Brands التي ليس لديها أي Product؟
الإجابة
Brand::doesntHave('products')
->get();السؤال التاسع
ماذا يفعل:
$table->foreignId('brand_id')
->constrained()
->cascadeOnDelete();الإجابة
ينشئ Foreign Key يشير وفق Laravel conventions إلى brands.id، ويحدد أن حذف Brand يؤدي إلى حذف السجلات المرتبطة به في Products.
السؤال العاشر
ما الفرق بين:
Product::get()
->where('price', '>', 500);و:
Product::where('price', '>', 500)
->get();الإجابة
في المثال الأول يتم جلب البيانات أولًا من قاعدة البيانات ثم تتم الفلترة على Collection داخل PHP.
أما المثال الثاني فيتم إرسال شرط WHERE إلى قاعدة البيانات نفسها، وهو غالبًا الخيار الأفضل عند التعامل مع كميات كبيرة من البيانات.
اختبار نهائي
إذا أصبحت قادرًا على تفسير الكود التالي بالكامل، فأنت أصبحت تفهم One To Many بصورة جيدة:
$brands = Brand::query()
->select(
'id',
'name'
)
->whereHas(
'products',
function ($query) {
$query->where(
'qty',
'>',
0
);
}
)
->with([
'products' => function ($query) {
$query
->select(
'id',
'brand_id',
'name',
'price',
'qty'
)
->where(
'qty',
'>',
0
)
->orderByDesc(
'price'
);
}
])
->withCount('products')
->paginate(20);ماذا يفعل هذا الاستعلام؟
- يجلب Brands.
- يحدد أعمدة Brand المطلوبة.
- يعيد فقط Brands التي لديها Products متوفرة.
- يحمل Products المتوفرة فقط.
- يرتب Products من الأعلى سعرًا إلى الأقل.
- يضيف عدد Products لكل Brand.
- يقسم النتائج إلى 20 Brand في الصفحة.
الخلاصة
علاقة One To Many في Laravel ليست مجرد:
hasMany()
belongsTo()بل تبدأ أولًا من تصميم قاعدة البيانات بصورة صحيحة:
brands.id
│
│
└────→ products.brand_idثم تعريف الاتجاهين:
Brand
↓
hasMany
↓
Products
Product
↓
belongsTo
↓
Brandبعد ذلك يمكن استخدام Eloquent للتعامل مع العلاقة:
$brand->products
$brand->products()
$product->brandثم الانتقال إلى الأدوات الأكثر تقدمًا:
with()
load()
whereHas()
whereDoesntHave()
has()
doesntHave()
withCount()
save()
saveMany()
create()
createMany()
associate()وأخيرًا يجب الانتباه إلى الأداء وفهم الفرق بين:
Lazy Loading
vs
Eager Loadingومراقبة مشكلة:
N + 1أفضل طريقة لإتقان علاقات Laravel ليست حفظ الدوال، وإنما فهم ثلاث طبقات: تصميم العلاقة في قاعدة البيانات، طريقة تعريفها في Eloquent، والاستعلامات التي سينفذها التطبيق عند استخدام العلاقة.
عندما تفهم هذه الطبقات الثلاث، يصبح الانتقال إلى العلاقات الأخرى مثل One To One وMany To Many وHas Many Through وPolymorphic Relationships أسهل بكثير.
شرح جميل كافي ووافي
كنت انتظر حديتك عن العلاقات شكرا خصوصا معقدة منها
شكرا ياغلى الله يوفقك ارجو ان تفتح قناه على يوتيوب والله محتواك قوى جدا من شخص محترف الرجاء كمل فى العلاقات شكرا اخى العزيز