قام مطور Magento باستبدال 300 استدعاء منفصل لـ ProductRepositoryInterface::getById() باستعلام مجموعة (collection query) واحد. انخفضت عمليات الاتصال بقاعدة البيانات من 300 إلى 1، وتحميل صفحة الفئة أصبح أسرع بشكل ملحوظ. ولكن ما هو الثمن؟ اختفت بعض المنتجات من واجهة المتجر، مما أربك المتسوقين وأصحاب المتجر.
مشكلة N+1 في صفحة فئة Magento
كانت الصفحة تعرض 300 منتج. ولكل منتج، كان الكود يجلب صفتين مخصصتين (custom attributes) عبر طريقة getById() الخاصة بالمستودع (repository)، مما يؤدي إلى إنشاء استعلام واحد لكل منتج — وهو نمط N+1 الكلاسيكي. أدى استبدال عمليات التحميل لكل منتج بمجموعة (collection) تسحب جميع الصفوف المطلوبة في استعلام واحد إلى تقليص عدد الاستعلامات بشكل كبير. تحسن زمن الاستجابة في الواجهة الأمامية (front-end latency)، ولكن قائمة المنتجات أصبحت تفتقر الآن إلى عناصر كانت تظهر سابقاً.
لماذا تخفي "المجموعة" المنتجات غير المتوفرة في المخزون
تقوم وحدة CatalogInventory في Magento تلقائياً بإضافة مرشح (filter) "متوفر في المخزون" (in-stock) إلى أي مجموعة منتجات يتم إنشاؤها للواجهة الأمامية. يتم تفعيل هذا المرشح عندما يتم إيقاف إعداد المتجر Display Out of Stock Products، وهو خيار شائع للتجار الذين لا يرغبون في عرض العناصر غير المتوفرة.
في المقابل، لا يطبق المستودع (repository) أبداً مرشح المخزون. فهو يتحقق ببساطة من وجود المنتج ويعيده، بغض النظر عن مستوى المخزون. عندما انتقل الكود من استدعاءات المستودع إلى "المجموعة" (collection)، قام مرشح المخزون بصمت بحذف كل منتج كانت كميته صفراً. لم يتم تسجيل أي خطأ؛ بل اكتفت المجموعة بإرجاع صفوف أقل مما توقعه المطور.
ماذا يعني هذا للتجار
إن واجهة المتجر التي تحذف وحدات حفظ المخزون (SKUs) غير المتوفرة بصمت تسبب عدة مشكلات:
- تتلقى مكونات الواجهة الأمامية مدخلات مفقودة، مما يؤدي إلى ظهور تسميات فارغة أو أسعار خاطئة.
- يصبح تصحيح الأخطاء (debugging) أصعب لأن العرض — المنتجات المفقودة — لا يولد أي تحذيرات.
ولأن معظم الاختبارات المؤتمتة تستخدم بيانات تجريبية (fixtures) لمنتجات متوفرة في المخزون، فإن هذا الخطأ غالباً ما يظل مخفياً حتى يعمل الموقع بمخزون حقيقي.
كيفية الحفاظ على السرعة دون إخفاء المخزون
- تجاوز مرشح المخزون الافتراضي – قم بإزالة المرشح صراحةً من المجموعة إذا كنت بحاجة إلى كل منتج بغض النظر عن توفره. توفر Magento طرقاً لتعطيل أو استبدال ملحق (plugin) المخزون لكل استعلام.
- استخدم المستودع (repository) عندما تكون الدقة مهمة – يضمن المستودع إرجاع كل معرف منتج (product ID) مطلوب، حتى لو كان غير متوفر في المخزون. قم بتخزين النتائج مؤقتاً (cache) أو قم بتحميل المعرفات التي تحتاجها فقط للسيطرة على الاستعلامات الإضافية.
- استخدم نموذج الموارد (resource model) أو SQL الخام – قم بالاستعلام من جدول المنتجات مباشرة للحصول على الصفات المطلوبة. هذا يتجاوز جميع ملحقات المجموعة (collection plugins) ويمنحك تحكماً كاملاً في الصفوف التي سيتم تضمينها.
السرعة مقابل الدقة
يبدو استبدال حلقة N+1 المزعجة باستعلام مجموعة واحد أمراً طبيعياً؛ فانخفاض زمن الاستجابة ملموس. ومع ذلك، فإن الاستعلام الأسرع الذي يعيد مجموعة خاطئة من المنتجات هو عيب (defect) وليس تحسيناً (optimization). يجب على المطورين الموازنة بين السرعة الخام ومخاطر عرض كتالوج غير مكتمل.
ما يجب مراقبته لاحقاً
- انحراف الإعدادات (Configuration drift) – تحقق من أن علامة Display Out of Stock Products تتوافق مع السلوك المقصود لأي كود مخصص للمجموعات.
- الآثار الجانبية للملحقات (Plugin side effects) – قد تضيف وحدات أخرى مرشحاتها الخاصة إلى المجموعات. راجع سلسلة الملحقات (plugin stack) كلما قمت بتغيير استراتيجيات الاستعلام.
الدرس واضح: إصلاح مشكلة N+1 باستخدام "المجموعة" لا يعد نجاحاً إلا إذا قمت أيضاً بمراجعة المرشحات الافتراضية للمجموعة. إن تجاهل مرشح المخزون المدمج في Magento يمكن أن يؤدي إلى تقليم كتالوجك بصمت، مما يحول مكاسب الأداء إلى خسارة في الإيرادات.
