إطار Laravel

استعلامات Eloquent المتقدمة في Laravel

18 دقيقة الدرس 21 من 45

استعلامات Eloquent المتقدمة في Laravel

بينما تكون استعلامات Eloquent الأساسية قوية، يوفر Laravel إمكانيات استعلام متقدمة لسيناريوهات استرجاع البيانات المعقدة. يستكشف هذا الدرس تقنيات الاستعلام المتطورة التي ستساعدك على بناء عمليات قاعدة بيانات فعالة.

تقنيات Query Builder المتقدمة

بعيداً عن جمل where الأساسية، يوفر query builder في Laravel طرقاً متقدمة للشروط المعقدة:

جمل Where المتقدمة:
// Where مع استعلامات فرعية
$users = User::where('votes', '>', function ($query) {
    $query->selectRaw('avg(votes)')
          ->from('users')
          ->where('status', 'active');
})->get();

// whereColumn - مقارنة عمودين
$products = Product::whereColumn('sale_price', '<', 'regular_price')->get();

// whereIn مع استعلام فرعي
$activeUsers = User::whereIn('id', function ($query) {
    $query->select('user_id')
          ->from('orders')
          ->where('created_at', '>=', now()->subMonth());
})->get();

// whereBetween مع التواريخ
$recentOrders = Order::whereBetween('created_at', [
    now()->subDays(7),
    now()
])->get();

// whereDate, whereMonth, whereDay, whereYear
$todayOrders = Order::whereDate('created_at', today())->get();
$decemberSales = Sale::whereMonth('created_at', 12)->get();
نصيحة للأداء: عند استخدام الاستعلامات الفرعية في جمل where، تأكد من أن جدول الاستعلام الفرعي لديه فهارس مناسبة على الأعمدة المستعلم عنها للحفاظ على الأداء الجيد.

التعبيرات الخام وأمان SQL Injection

أحياناً تحتاج إلى SQL خام للعمليات المعقدة. يوفر Laravel طرقاً آمنة لتضمين التعبيرات الخام:

استخدام التعبيرات الخام بأمان:
// selectRaw للأعمدة المحسوبة
$users = User::selectRaw('name, email, age * 12 as months_lived')
             ->get();

// whereRaw مع الربط (يمنع SQL injection)
$orders = Order::whereRaw('price > IF(status = "premium", ?, ?)', [100, 50])
               ->get();

// orderByRaw للفرز المخصص
$products = Product::orderByRaw('FIELD(category, "featured", "new", "sale")')
                   ->get();

// havingRaw لشروط التجميع
$results = DB::table('orders')
             ->select('customer_id', DB::raw('SUM(price) as total'))
             ->groupBy('customer_id')
             ->havingRaw('SUM(price) > ?', [1000])
             ->get();

// DB::raw للتعبيرات المعقدة
$users = User::select(DB::raw('COUNT(*) as user_count, status'))
             ->where('status', '<>', 'deleted')
             ->groupBy('status')
             ->get();
تحذير أمني: لا تقم أبداً بدمج مدخلات المستخدم مباشرة في SQL الخام. استخدم دائماً ربط المعاملات (علامات ? البديلة) لمنع هجمات SQL injection.

تقسيم مجموعات النتائج الكبيرة

عند التعامل مع آلاف السجلات، قد يسبب تحميلها جميعاً في الذاكرة مشاكل. التقسيم يعالج النتائج في دفعات أصغر:

طرق Chunk و Cursor:
// Chunk - يعالج في دفعات
User::where('active', true)->chunk(200, function ($users) {
    foreach ($users as $user) {
        // معالجة كل مستخدم
        $user->sendMonthlyReport();
    }
});

// chunkById - أكثر كفاءة لمجموعات البيانات الكبيرة (يستخدم ترتيب ID)
Order::where('status', 'pending')
     ->chunkById(100, function ($orders) {
         foreach ($orders as $order) {
             $order->process();
         }
     }, $column = 'id');

// Cursor - يحمل واحداً تلو الآخر بشكل كسول (كفاءة في الذاكرة)
foreach (User::where('votes', '>', 100)->cursor() as $user) {
    // يتم تحميل مستخدم واحد فقط في الذاكرة في كل مرة
    $user->calculateStatistics();
}

// lazy - مشابه لـ cursor ولكن مع تحكم أفضل
User::where('active', true)->lazy()->each(function ($user) {
    // معالجة المستخدم
    $user->updateMetrics();
});

// lazyById - أكثر كفاءة لمجموعات البيانات الضخمة جداً
User::where('subscribed', true)
    ->lazyById(500)
    ->each(function ($user) {
        $user->sendNewsletter();
    });
اختيار الطريقة المناسبة: استخدم chunk() للمعالجة الدفعية، cursor() أو lazy() عند المعالجة واحداً تلو الآخر، وchunkById() أو lazyById() لأفضل أداء على الجداول الضخمة جداً.

الدوال التجميعية والتجميع المتقدم

يوفر Laravel طرقاً للحسابات الإحصائية والتجميع المعقد:

التجميعات والإحصائيات:
// التجميعات الأساسية
$totalOrders = Order::count();
$averagePrice = Order::avg('price');
$maxPrice = Order::max('price');
$minPrice = Order::min('price');
$totalRevenue = Order::sum('price');

// التجميع مع التجميعات
$salesByCategory = Product::select('category', DB::raw('COUNT(*) as count'))
                          ->groupBy('category')
                          ->get();

// جمل Having مع التجميعات
$activeCustomers = Order::select('customer_id', DB::raw('COUNT(*) as order_count'))
                        ->groupBy('customer_id')
                        ->having('order_count', '>', 5)
                        ->get();

// أعمدة groupBy متعددة
$monthlySales = Order::select(
    DB::raw('YEAR(created_at) as year'),
    DB::raw('MONTH(created_at) as month'),
    DB::raw('SUM(price) as revenue')
)
->groupBy('year', 'month')
->orderBy('year', 'desc')
->orderBy('month', 'desc')
->get();

// التجميعات الشرطية
$stats = Order::selectRaw('
    COUNT(*) as total_orders,
    SUM(CASE WHEN status = "completed" THEN 1 ELSE 0 END) as completed,
    SUM(CASE WHEN status = "pending" THEN 1 ELSE 0 END) as pending,
    AVG(price) as average_price
')->first();

الجمل الشرطية والاستعلامات الديناميكية

بناء الاستعلامات ديناميكياً بناءً على الشروط دون جمل if/else فوضوية:

طرق When و Unless:
// when() - تضيف جملة فقط إذا كان الشرط صحيحاً
$sortBy = request('sort_by');
$products = Product::when($sortBy, function ($query, $sortBy) {
    return $query->orderBy($sortBy);
})->get();

// when() مع callback افتراضي
$role = request('role');
$users = User::when($role, function ($query, $role) {
    // يُطبق عندما يكون role موجوداً
    return $query->where('role', $role);
}, function ($query) {
    // يُطبق عندما لا يكون role موجوداً (افتراضي)
    return $query->where('role', 'user');
})->get();

// unless() - عكس when()
$showInactive = request('show_inactive');
$users = User::unless($showInactive, function ($query) {
    // يُطبق فقط عندما يكون showInactive خطأ
    return $query->where('active', true);
})->get();

// مثال بحث عملي
$search = request('search');
$category = request('category');
$minPrice = request('min_price');
$maxPrice = request('max_price');

$products = Product::query()
    ->when($search, function ($query, $search) {
        $query->where('name', 'like', "%{$search}%");
    })
    ->when($category, function ($query, $category) {
        $query->where('category_id', $category);
    })
    ->when($minPrice, function ($query, $minPrice) {
        $query->where('price', '>=', $minPrice);
    })
    ->when($maxPrice, function ($query, $maxPrice) {
        $query->where('price', '<=', $maxPrice);
    })
    ->get();
كود نظيف: استخدام when() وunless() يحافظ على كود بناء الاستعلام نظيفاً وقابلاً للقراءة، متجنباً جمل if المتداخلة بعمق.

الاستعلامات الفرعية في جمل Select

قم بتضمين البيانات المحسوبة من الجداول المرتبطة بكفاءة باستخدام الاستعلامات الفرعية:

استعلامات Select الفرعية:
// إضافة نتائج الاستعلام الفرعي كأعمدة
$users = User::select(['name', 'email'])
    ->selectSub(function ($query) {
        $query->from('orders')
              ->whereColumn('user_id', 'users.id')
              ->selectRaw('COUNT(*)');
    }, 'orders_count')
    ->selectSub(function ($query) {
        $query->from('orders')
              ->whereColumn('user_id', 'users.id')
              ->selectRaw('SUM(price)');
    }, 'total_spent')
    ->get();

// استخدام addSelect لاستعلامات فرعية إضافية
$products = Product::select('id', 'name', 'price')
    ->addSelect([
        'reviews_avg' => Review::selectRaw('AVG(rating)')
            ->whereColumn('product_id', 'products.id'),
        'reviews_count' => Review::selectRaw('COUNT(*)')
            ->whereColumn('product_id', 'products.id')
    ])
    ->get();

// ترتيب الاستعلام الفرعي
$users = User::orderBy(
    Order::selectRaw('COUNT(*)')->whereColumn('user_id', 'users.id'),
    'desc'
)->get();
تمرين عملي 1: أنشئ استعلاماً يسترجع جميع المنتجات مع اسم الفئة ومتوسط التقييم وعدد المراجعات وإجمالي المبيعات. قم بالتصفية لإظهار المنتجات التي لديها أكثر من 10 مراجعات ومتوسط تقييم أعلى من 4.0. الترتيب حسب إجمالي المبيعات تنازلياً.
تمرين عملي 2: ابنِ استعلام بحث ديناميكي لمدونة يقبل مرشحات اختيارية: كلمة مفتاحية (تبحث في العنوان والمحتوى)، category_id، author_id، تاريخ published_after، وتاريخ published_before. استخدم طريقة when() لتطبيق كل مرشح بشكل مشروط.
تمرين عملي 3: اكتب سكريبت يعالج جميع المستخدمين في أجزاء من 500، يحسب إجمالي مشترياتهم، ويحدث عمود "total_spent". استخدم التقسيم للتعامل مع ملايين المستخدمين المحتملة بكفاءة دون نفاد الذاكرة.

الخلاصة

استعلامات Eloquent المتقدمة تمنحك القوة للتعامل مع استرجاع البيانات المعقدة بكفاءة:

  • الاستعلامات الفرعية: تمكّن من التصفية المعقدة والأعمدة المحسوبة
  • التعبيرات الخام: توفر مرونة SQL مع الأمان من خلال الربط
  • التقسيم: يعالج مجموعات البيانات الكبيرة دون مشاكل في الذاكرة
  • الجمل الشرطية: بناء الاستعلامات الديناميكية بشكل نظيف مع when() و unless()
  • التجميعات: حساب الإحصائيات مباشرة في قاعدة البيانات

في الدرس التالي، سنستكشف API Resources وكيفية تحويل نماذج Eloquent لاستجابات API.