لغز مشكلة وجود limit 21 بدلًا من 2 في Django

لغز مشكلة وجود limit 21 بدلًا من 2 في Django

أفاد بعض مطوّري Django بأنهم لاحظوا ظهور عبارة LIMIT 21 في الاستعلامات الناتجة عن استخدام Model.objects.get(...) أو get_object_or_404، بينما كان المتوقع إضافة حد 2 فقط (للكشف عن سجّلين على الأكثر). هذه الظاهرة تبدو غريبة لأن الاستعلام يستهدف عادةً كائنًا واحدًا. وجدت الأبحاث في مستودعات Django و StackOverflow و Google Groups أن هذا السلوك مقصود وليس خطأ. ففي الإصدارات الأقدم من Django (قبل 3.0) كانت دالة get() تسترجع جميع السجلات ثم تحسب عددها، مما قد يكون مكلفًا في حالة قواعد بيانات ضخمة. لذا أُدخلت تغييرات لإضافة حد على عدد الصفوف التي تسترجعها الدالة.

سلوك .get() في Django وتحديد الحد

في شيفرة Django (اعتبارًا من الإصدار 3.0)، تم تعديل دالة QuerySet.get() بحيث تضبط حد الصفوف المسترجعة إلى قيمة ثابتة تُسمّى MAX_GET_RESULTS. في وثائق الشيفرة المصدرية لنسخة Django 4.2 يظهر تعريف الثابت:

# The maximum number of results to fetch in a get() query.

MAX_GET_RESULTS = 21.

وبالتالي عندما يُنفَّذ استعلام get()، تضيف المكتبة شرطًا مثل LIMIT 21. وهذا يعني أنه في أسوأ الأحوال يتم استرجاع 21 صفًا من قاعدة البيانات للتحقق مما إذا كان هناك أكثر من سجل واحد يتطابق مع الشروط. بعد ذلك، تقوم get() بالعدّ ضمن الذاكرة: إذا كان عدد النتائج المسترجعة 1 فهذا يفي بالغرض، وإذا كان صفرًا يُطرح استثناء DoesNotExist، وإذا كان أكثر من 1 يُطرح استثناء MultipleObjectsReturned.

يشير Matthijs Kooijman في StackOverflow إلى أن هذا LIMIT 21 يُضاف بشكل مقصود كحماية لعدم تحميل كميات ضخمة من البيانات دون حاجة. فالهدف هو استرجاع على الأكثر 21 صفًا حتى في حالة خطأ (وجود سجلات متعددة)، بدلًا من جلب كل السجلات المحتملة. في الحالة الطبيعية (وجود سجل واحد فقط) لا يكون لذلك أي تأثير في الأداء.

السبب التقريبي لاختيار الرقم 21

لماذا الرقم 21 تحديدًا؟ يُبيّن النقاش بين مطوّري Django أن العدد 21 جاء من اعتبارات عملية وليس رقمًا معجزًا:

  • الغرض من اختيار 21 هو عرض 20 سجلًا فقط ضمن تمثيل الـ QuerySet (مثلما يحدث عند استدعاء repr() أو عند حدوث MultipleObjectsReturned) مع صفّ إضافي واحد للتحقق مما إذا كانت هناك نتائج أخرى أم لا. اقرأ النقاش

  • أشار مساهمون في المشروع إلى أن العدد 20 “ليس كبيرًا جدًا ولا صغيرًا جدًا” ويُستخدم كحد أمامي عند عرض النتائج للتصحيح. أي بمعنى آخر، يتم طلب 21 سجلًا فعليًا بهدف عرض أول 20 منها والكشف إذا كان هناك 21‑ أو أكثر من السجلات لإظهار رسالة توضح عدد السجلات الزائد.

  • استخدام 21 (بدلًا من 2 فقط) يسمح بإظهار عدد السجلات الفعلية في رسالة الخطأ (إذا كانت أقل من 21) عند وقوع MultipleObjectsReturned. هذا مفيد لأغراض التشخيص والتتبع. لو استُخدم الحد 2 فقط، فلن يُعلم المستخدم إلّا بأن هناك أكثر من سجل واحد دون معرفة العدد الحقيقي.

  • تم استغلال ثابت موجود سابقًا في الشيفرة هو MAX_GET_RESULTS = 21 (المستخدم أيضًا في تمثيل الـ QuerySet) بدلًا من تعريف ثابت جديد. وقد وضّح أحد مبرمجي Django أن السبب الحقيقي لـ 21 هو ببساطة أنه “لم يُخْلَق ثابت جديد عند تنفيذ هذا التصحيح، بل استُخدم الثابت الموجود أصلاً”.

بعبارة أخرى، الرقم 21 يُعادل عمليًا “عرض 20 وإظهار مؤشر على وجود المزيد”. تمّ الاتفاق على هذا الرقم بعد نقاش داخلي في المشروع. فبالرغم من اقتراحات باستخدام 2 أو 22، اختار المطوّرون 21 كحل وسط، معتبرين أنه يقدّم معلومات أكثر عن الخطأ دون كلفة أداء كبيرة مقارنة بتحميل كل السجلات.

مناقشات المطورين والوثائق ذات الصلة

  • شيفرة المصدر والدوال الرسمية: يُوثّق الكود المصدري لـ Django وجود الثابت MAX_GET_RESULTS = 21 مع تعليق يشرح الهدف منه. لم تُنقل هذه المعلومة في وثائق المستخدم العادية، لكنها موجودة في التعليق المصدري.

  • تذاكر وأحداث GitHub: نوقش هذا الموضوع في تذكرة (#6785) وتعُدّلية حول تحجيم صفوف get(). في البداية (إصدارات قديمة)، أُضيفت هذه الميزة ثم أُرجعت في إصدارات 1.x لاعتبارات توافقية. في Django 3.0 أعيد تطبيقها بضغط (انظر Pull Request #11215 في GitHub). يظهر في نقاشات هذا Pull Request إشارات واضحة لاستخدام LIMIT 21 للحد من الصفوف (مبنية على اقتراح قديم في Google Groups).

  • منتديات ومجموعات النقاش: في منتدى Django الرسمي ذكر أحد المطوّرين أن دالة get() تجري في الخلفية تصفية (filter()) ثم تقصّر النتائج باستخدام LIMIT 21 (الثابت MAX_GET_RESULTS) قبل إجراء الطباعة أو المراجعة. تُوضح هذه المشاركة أن LIMIT 21 يُستخدم في الأصل “كحد” عند تنفيذ get() ليلتقط حالات الخطأ بكفاءة.

  • مجموعات المستخدمين: تتوفر رسائل في Google Groups (دوري Django-users و Django-developers) تناقش ظاهرة LIMIT 21 منذ سنوات (مثلاً رسالة من 2014 تناولت نفس الموضوع). ففي إحدى المناقشات شرح James Bennett أنه لإصلاح مشكلة نفاد الذاكرة عند عرض QuerySet كبير في واجهة التصحيح، تمّ تغيير سلوك repr() بحيث لا تُظهر أكثر من 20 كائنًا. نفس الفكرة ترجمت إلى دالة get().

عموماً، لا يوجد توثيق نهائي في دليل الاستخدام يشرح ظهور LIMIT 21، لكن هذه المصادر التقنية توضح أن الأمر محسوم في شيفرة Django والنقاشات الرسمية: الحد اختياري وصحيح ويتم لأغراض الأداء والتشخيص.

الخلاصة

ظهور LIMIT 21 في استعلامات Model.objects.get() ليس خطأً عشوائيًا، بل نتيجة ميزة مقصودة في تصميم Django (اعتبارًا من الإصدار 3.0) لضبط الحد الأقصى للصفوف المسترجعة. يقوم Django بجلب ما يصل إلى 21 نتيجة بهدف عرض 20 منها ومعرفة إن كان هناك سجل إضافي، ثم يقرر رفع MultipleObjectsReturned إن لزم الأمر. هذا الإجراء يساعد على تقليل حمل الذاكرة وتحسين أداء الحالات الخطأ، كما يزوّد رسالة الخطأ بمعلومات أدق حول عدد السجلات الزائدة. قيمة 21 تم اختيارها بعد نقاش مطوّري Django باعتبارها “عددًا غير كبير جدًا ولا صغير جدًا” (20 للمخرجات و1 للمراقبة).

إذا كان هذا السلوك لا يزال يثير الحيرة لدى بعض المستخدمين، فذلك راجع غالبًا إلى عدم إشارتها الصريحة في وثائق المستخدم الرسمية. لكن المصادر التقنية (شيفرة المصدر، تذاكر التطوير، ومنتديات المجتمع) تؤكد أنه سلوك مقصود تمامًا. وبالتالي، لم يعد الأمر لغزًا تقنيًا خالصًا بل تفسيرًا برمجيًا متوفرًا ومناقشًا: LIMIT 21 هو ميزة لتحقيق توازن بين الأداء وفائدة الاستعلام.

المعلومات التي ضمنتها في ما سبق لم تكن مقنعة بالنسبة لي، فلا يزل لغز مشكلة وجود limit 21 بدلًا من 2 غامض حتى الآن.

حول المحتوى:

في شيفرة Django (اعتبارًا من الإصدار 3.0)، تم تعديل دالة QuerySet.get() بحيث تضبط حد الصفوف المسترجعة إلى قيمة ثابتة تُسمّى MAX_GET_RESULTS. في وثائق الشيفرة المصدرية لنسخة Django 4.2 يظهر تعريف الثابت