یک توسعه‌دهنده Magento، ۳۰۰ فراخوانی مجزای ProductRepositoryInterface::getById() را با یک کوئری مجموعه (collection query) جایگزین کرد. تعداد درخواست‌ها به پایگاه داده از ۳۰۰ به ۱ کاهش یافت و صفحه دسته‌بندی به‌طور محسوسی سریع‌تر بارگذاری شد. اما هزینه این کار چه بود؟ برخی محصولات از ویترین فروشگاه ناپدید شدند و باعث سردرگمی خریداران و صاحبان فروشگاه شدند.

مشکل N+1 در صفحه دسته‌بندی Magento

این صفحه ۳۰۰ محصول را لیست می‌کرد. برای هر محصول، کد دو ویژگی سفارشی (custom attributes) را از طریق متد getById() در ریپازیتوری فراخوانی می‌کرد که منجر به ایجاد یک کوئری برای هر محصول می‌شد؛ یعنی همان الگوی کلاسیک N+1. جایگزین کردن این بارگذاری‌های تک‌محصولی با یک collection که تمام ردیف‌های مورد نیاز را در یک کوئری می‌آورد، تعداد کوئری‌ها را به شدت کاهش داد. تأخیر در سمت فرانت‌اند بهبود یافت، اما لیست محصولات اکنون فاقد آیتم‌هایی بود که قبلاً نمایش داده می‌شدند.

چرا collection محصولات ناموجود را پنهان می‌کند

ماژول CatalogInventory در Magento به‌طور خودکار یک فیلتر «موجود در انبار» (in-stock) به هر collection محصولی که برای فرانت‌اند ساخته می‌شود، اضافه می‌کند. این فیلتر زمانی فعال می‌شود که تنظیمات فروشگاه یعنی Display Out of Stock Products غیرفعال باشد؛ انتخابی رایج برای فروشندگانی که نمی‌خواهند کالاهای ناموجود را نمایش دهند.

در مقابل، ریپازیتوری هرگز فیلتر موجودی را اعمال نمی‌کند. این متد صرفاً بررسی می‌کند که آیا محصولی وجود دارد یا خیر و بدون توجه به سطح موجودی، آن را برمی‌گرداند. وقتی کد از فراخوانی‌های ریپازیتوری به collection تغییر یافت، فیلتر موجودی به‌طور بی‌صدا تمام محصولاتی را که موجودی آن‌ها صفر بود، حذف کرد. هیچ خطایی ثبت نشد؛ collection فقط ردیف‌های کمتری نسبت به آنچه توسعه‌دهنده انتظار داشت، برگرداند.

این موضوع چه معنایی برای فروشندگان دارد

یک ویترین فروشگاه که به‌طور بی‌صدا SKUهای ناموجود را حذف می‌کند، چندین مشکل ایجاد می‌کند:

  • کامپوننت‌های فرانت‌اند ورودی‌های ناقص دریافت می‌کنند که منجر به نمایش لیبل‌های خالی یا قیمت‌گذاری اشتباه می‌شود.
  • عیب‌یابی دشوارتر می‌شود، زیرا علامت بیماری (محصولات ناپدید شده) هیچ هشداری ایجاد نمی‌کند.

از آنجایی که اکثر تست‌های خودکار از داده‌های نمونه (fixtures) موجود در انبار استفاده می‌کنند، این باگ اغلب تا زمانی که سایت با موجودی واقعی کار کند، پنهان می‌ماند.

چگونه سرعت را بدون پنهان کردن موجودی حفظ کنیم

  1. فیلتر پیش‌فرض موجودی را نادیده بگیرید – اگر به تمام محصولات بدون توجه به موجود بودن آن‌ها نیاز دارید، فیلتر را به‌طور صریح از collection حذف کنید. Magento روش‌هایی برای غیرفعال کردن یا جایگزینی پلاگین موجودی (stock plugin) در هر کوئری ارائه می‌دهد.
  2. زمانی که دقت مهم است از ریپازیتوری استفاده کنید – ریپازیتوری تضمین می‌کند که هر شناسه محصول درخواستی برگردانده شود، حتی اگر ناموجود باشد. برای کنترل کوئری‌های اضافی، نتایج را کش (cache) کنید یا فقط شناسه‌های مورد نیاز خود را بارگذاری کنید.
  3. از یک resource model یا SQL خام استفاده کنید – برای دریافت ویژگی‌های مورد نیاز، مستقیماً جدول محصول را کوئری بزنید. این کار تمام پلاگین‌های collection را دور می‌زند و کنترل کامل روی ردیف‌های گنجانده شده به شما می‌دهد.

سرعت در مقابل صحت

جایگزین کردن یک حلقه پرسر و صدای N+1 با یک کوئری مجموعه واحد، طبیعی به نظر می‌رسد و کاهش تأخیر نیز ملموس است. با این حال، کوئری سریع‌تری که مجموعه اشتباهی از محصولات را برمی‌گرداند، یک نقص (defect) است، نه یک بهینه‌سازی. توسعه‌دهندگان باید سرعت خام را در برابر خطر نمایش کاتالوگ ناقص بسنجند.

موارد بعدی که باید مراقب آن‌ها باشید

  • انحراف تنظیمات (Configuration drift) – بررسی کنید که پرچم Display Out of Stock Products با رفتار مورد نظر در کدهای collection سفارشی شما مطابقت داشته باشد.
  • اثرات جانبی پلاگین‌ها – ماژول‌های دیگر ممکن است فیلترهای خود را به collectionها اضافه کنند. هر زمان که استراتژی‌های کوئری خود را تغییر می‌دهید، پشته پلاگین‌ها (plugin stack) را بازبینی کنید.

درس واضح است: رفع مشکل N+1 با استفاده از یک collection تنها زمانی موفقیت‌آمیز است که فیلترهای پیش‌فرض آن را نیز بازرسی کنید. نادیده گرفتن فیلتر موجودی داخلی Magento می‌تواند به‌طور بی‌صدا کاتالوگ شما را هرس کند و یک بهبود عملکرد را به از دست رفتن درآمد تبدیل کند.