تخطَّ إلى المحتوى

🎸 شرح Django

أنماط جلب الحقول (Fetch Modes)

الدرس 34 من 35· ⏱ 3 دقائق قراءة

تذكير بالمشكلة: N+1 queries

تعلّمت بدرس العلاقات أن select_related وprefetch_related يحلّان مشكلة N+1 queries — لكن كلاهما يتطلّب منك تحديد اسم الحقل مسبقًا بالاستعلام الأصلي. إذا نسيت حقلًا، أو وصل الكود لاحقًا لعلاقة لم تحدّدها، تعود المشكلة من جديد بصمت دون أي تحذير.

Django 6.1 يضيف حلًا جديدًا مكمّلًا: أنماط جلب الحقول (fetch modes) — تتحكّم بسلوك الجلب التلقائي نفسه، لا فقط بحقل معيّن مذكور صراحة.

QuerySet.fetch_mode()

كل استعلام يحمل نمط جلب (fetch mode) يتحكّم بما يحدث عند الوصول لحقل أو علاقة لم تُجلَب بعد ضمن نفس الاستعلام:

from django.db import models

books = Book.objects.fetch_mode(models.FETCH_PEERS)

توجد ثلاثة أنماط جاهزة بـ django.db.models:

النمطالسلوك
FETCH_ONE (الافتراضي)يجلب الحقل الناقص للكائن الحالي فقط — نفس سلوك Django التقليدي قبل 6.1
FETCH_PEERSيجلب الحقل الناقص لكل الكائنات القادمة من نفس QuerySet دفعة واحدة
RAISEيرفع خطأ FieldFetchBlocked بدل تنفيذ أي استعلام إضافي غير متوقّع

FETCH_PEERS: يحل N+1 دون تحديد أي حقل مسبقًا

هذا هو الأهم عمليًا. بدل تذكّر كتابة select_related("author")، يكفي ضبط نمط الاستعلام مرّة واحدة:

from django.db import models

# بدون أي تحديد لحقل author مسبقًا
for book in Book.objects.fetch_mode(models.FETCH_PEERS):
    print(book.author.name)  # ← استعلامان فقط بالمجمل، مهما كان عدد الكتب

بالحلقة أعلاه، أوّل وصول لـ book.author يجلب المؤلّفين لكل الكتب بالـ QuerySet دفعة واحدة (وليس مؤلّف كتاب واحد فقط) — تمامًا كأثر prefetch_related لكنه يعمل تلقائيًا مع أي حقل تصل له بالحلقة، لا فقط الحقول المذكورة صراحة.

💡 Django ينقل نمط الجلب لأي كائنات مرتبطة تُجلَب لاحقًا، فيسري النمط على شجرة العلاقات كاملة لا فقط النموذج الأول بالاستعلام.

RAISE: كشف استعلامات غير مقصودة قبل الإنتاج

مفيد بكود حسّاس للأداء أو بالاختبارات، لمنع أي استعلام إضافي مخفي من المرور بصمت:

from django.db import models
from django.db.models.fields.related_descriptors import FieldFetchBlocked

books = Book.objects.fetch_mode(models.RAISE)

for book in books:
    try:
        print(book.author.name)  # لم يُجلَب مسبقًا بالاستعلام
    except FieldFetchBlocked:
        print("⚠️ استعلام إضافي غير مقصود — أضف select_related أو غيّر النمط")

تعيين نمط افتراضي لنموذج كامل

عبر Manager مخصّص، يمكن جعل FETCH_PEERS هو السلوك الافتراضي لكل استعلام على نموذج معيّن، دون تكرار .fetch_mode(...) بكل مكان بالكود:

from django.db import models


class BookManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().fetch_mode(models.FETCH_PEERS)


class Book(models.Model):
    objects = BookManager()
    title = models.CharField(max_length=200)
    author = models.ForeignKey("Author", on_delete=models.CASCADE)
المعيارselect_related/prefetch_relatedfetch_mode(FETCH_PEERS)
التحديدصريح — تكتب اسم الحقل بنفسكضمني — يعمل مع أي حقل تصل له بالحلقة
الدقّةتتحكّم بالضبط بأي حقل يُجلَب مسبقًا (JOIN أو استعلام إضافي)أعمّ؛ لا يميّز بين الحقول المهمّة وغيرها تلقائيًا
مناسب لـكود إنتاج معروف مسبقًا شكل استعلاماته بدقّةنماذج/كود يصعب حصر كل حقولها مسبقًا، أو حماية من نسيان مستقبلي

💡 لا تُلغي أنماط الجلب حاجتك لـ select_related/prefetch_related — بالعكس، تبقى الأداة الأدق حين تعرف احتياجك مسبقًا. أنماط الجلب طبقة حماية إضافية للحالات التي يصعب فيها ضبط كل شيء صراحة.

أخطاء شائعة

  • الظن أن FETCH_PEERS يستبدل prefetch_related كليًا — بحالات معقّدة (تصفية على العلاقة نفسها مثلًا) لا يزال prefetch_related مع Prefetch المخصّص أدق وأكثر تحكّمًا.
  • استخدام RAISE بكود إنتاج عادي دون تعامل مع FieldFetchBlocked — يحوّل أي وصول لحقل غير مُجهَّز إلى خطأ فوري بدل استعلام إضافي بطيء لكنه يعمل؛ هذا النمط مخصّص للكشف والاختبارات أكثر من التشغيل العادي.
  • توقّع أن fetch_mode() يعمل بإصدارات Django أقدم من 6.1 — الميزة جديدة كليًا ولا بديل لها بالإصدارات السابقة سوى select_related/prefetch_related.

🎯 التالي: الخلاصة الشاملة لمسار Django وخطواتك بعده.

شرح أنماط جلب الحقول (Fetch Modes) — Django بالعربي
أنماط جلب الحقول (Fetch Modes)Django بالعربي · The Code Fix

📚 لمزيد من التعمّق في Django، راجِع التوثيق الرسمي لـ Django.

هل كان هذا الدرس مفيدًا؟