حول المحتوى:
شرح أمثلة حقيقية لكيف يؤدي ORM إلى بطء التطبيق، وكيفية تحسين الاستعلامات بشكل جذري.
تحسين ORM Django ليس مجرد ترف برمجي، بل خطوة أساسية إذا كنت تبني تطبيقات حقيقية تحتاج لأداء عالٍ واستجابة سريعة. كثير من المطورين يبدأون باستخدام Django ORM بسهولة، لكن مع نمو المشروع تظهر مشاكل بطء مفاجئة، وغالبًا السبب يكون في ما يُعرف بمشكلة N+1 Queries.
في هذا المقال سنشرح بشكل عملي:
إذا كنت جديدًا على Django ORM، من المفيد الرجوع أولًا إلى دليل استخدام Django ORM، ثم العودة لهذا المقال للتعمق في تحسين الأداء.
مشكلة N+1 Queries تحدث عندما تقوم بتحميل قائمة من الكائنات من قاعدة البيانات، ثم لكل كائن تقوم بطلب استعلام إضافي لجلب بيانات مرتبطة به. النتيجة: عدد ضخم من الاستعلامات الصغيرة بدلًا من عدد قليل من الاستعلامات الذكية.
افترض أن لدينا نموذجين:
class Author(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
وفي view تقوم بجلب كل الكتب وعرض اسم الكاتب:
def books_list(request):
books = Book.objects.all()
for book in books:
print(book.title, book.author.name)
ما يحدث بالفعل في الخلفية:
النتيجة: N+1 Queries. لو عندك 500 كتاب، سيكون لديك 501 استعلام إلى قاعدة البيانات! في بيئة Production ومع ضغط عدد المستخدمين، هذا كفيل بقتل أداء التطبيق.
أول خطوة في تحسين ORM Django هي كشف هذه المشاكل. هناك أكثر من أداة تساعدك:
.query في QuerySet أثناء التطوير.بعد تثبيت وتفعيل Django Debug Toolbar، افتح صفحة تعرض قائمة بيانات كبيرة (مثل قائمة الكتب، المقالات، الطلبات...)، ثم:
للتعمق أكثر في تحسين الاستعلامات، يمكنك الاطلاع أيضًا على مقال: تحسين استعلامات Django ORM لأداء أعلى.
دالة select_related تُستخدم لتحميل العلاقات من نوع ForeignKey أو OneToOne في نفس الاستعلام باستخدام JOIN على مستوى قاعدة البيانات. هذا يقلل عدد الاستعلامات بشكل كبير.
def books_list(request):
books = Book.objects.select_related('author').all()
for book in books:
print(book.title, book.author.name)
الآن ما يحدث في الخلفية:
book.author.بدل 501 استعلام، أصبح لدينا استعلام واحد فقط.
book.author)، وليس العكس.افترض النماذج التالية:
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField()
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
address = models.ForeignKey('Address', on_delete=models.SET_NULL, null=True)
سيناريوهات استخدام:
# جلب بروفايل المستخدم مع بيانات الـ User
profile = Profile.objects.select_related('user').get(pk=1)
# جلب الطلبات مع المستخدم والعنوان في استعلام واحد
orders = Order.objects.select_related('user', 'address').all()
على عكس select_related، دالة prefetch_related تعمل بطريقة مختلفة: تقوم بتنفيذ عدة استعلامات منفصلة لكنها تقوم بدمج النتائج في الذاكرة، بطريقة تمنع تكرار الاستعلام لكل عنصر.
تُستخدم مع:
author.book_set أو related_name).افترض النماذج التالية:
class Tag(models.Model):
name = models.CharField(max_length=50)
class Article(models.Model):
title = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag, related_name='articles')
View سيئ الأداء:
def articles_list(request):
articles = Article.objects.all()
for article in articles:
print(article.title)
for tag in article.tags.all():
print(tag.name)
هنا يحدث التالي:
النتيجة مرة أخرى: N+1 Queries.
def articles_list(request):
articles = Article.objects.prefetch_related('tags').all()
for article in articles:
print(article.title)
for tag in article.tags.all():
print(tag.name)
الآن ما يحدث في الخلفية:
article.tags.all() داخل الحلقة.أيًا كان عدد المقالات، سيظل عدد الاستعلامات ثابتًا (2 فقط).
بالعودة لمثال الكتب والكتّاب:
class Author(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE)
نريد عرض المؤلفين مع جميع كتبهم:
def authors_list(request):
authors = Author.objects.all()
for author in authors:
print(author.name)
for book in author.books.all():
print(book.title)
هذا يؤدي إلى N+1 Queries. الحل:
def authors_list(request):
authors = Author.objects.prefetch_related('books').all()
for author in authors:
print(author.name)
for book in author.books.all():
print(book.title)
مرة أخرى، سيقوم Django بتنفيذ استعلامين فقط: واحد للكتّاب وآخر للكتب، ثم يربط النتائج في الذاكرة.
قاعدة بسيطة لتحسين ORM Django بشكل صحيح:
Prefetch مع شروط filter.ولا تنسَ أنه يمكنك الجمع بينهما في نفس الاستعلام:
books = (
Book.objects
.select_related('author', 'publisher')
.prefetch_related('tags', 'reviews')
)
النماذج المبسّطة:
class Customer(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
class Order(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
address = models.ForeignKey('Address', on_delete=models.SET_NULL, null=True)
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
product = models.ForeignKey('Product', on_delete=models.CASCADE)
الـ view الأول (البطيء):
def admin_orders_list(request):
orders = Order.objects.all()
for order in orders:
print(order.customer.user.username)
print(order.address.city)
for item in order.items.all():
print(item.product.name)
المشكلة:
from django.db.models import Prefetch
def admin_orders_list(request):
orders = (
Order.objects
.select_related('customer__user', 'address')
.prefetch_related(
Prefetch(
'items',
queryset=OrderItem.objects.select_related('product')
)
)
)
for order in orders:
print(order.customer.user.username)
print(order.address.city if order.address else '')
for item in order.items.all():
print(item.product.name)
النتيجة:
مثال:
qs = Order.objects.select_related(
'customer__user__profile__company__country'
)
كلما زادت عمق العلاقات، كبر حجم الـ JOIN وتعقّد الاستعلام، وقد يصبح أبطأ من عدة استعلامات منفصلة. جرّب، وقِس الأداء قبل وبعد.
إذا كنت تجلب آلاف الصفوف مع علاقاتها مرة واحدة، قد تستهلك الكثير من الذاكرة. في هذه الحالة:
only() أو values() عند الحاجة.حتى مع أفضل تحسينات ORM، إذا كانت الجداول بلا فهارس مناسبة، ستبقى الاستعلامات بطيئة. راجع مقال: أهم خوارزميات الفهرسة المستخدمة في قواعد البيانات، وراجع بنية الجداول خاصة مع PostgreSQL كما شرحنا في: ربط Django مع PostgreSQL بطريقة احترافية.
str(queryset.query) لفهم الاستعلامات وتحسينها أو إعادة كتابة الـ QuerySet.تحسين ORM Django لا يعني فقط استخدام دوال سحرية مثل select_related وprefetch_related، بل يعني فهم كيف يعمل ORM تحت الغطاء، وكيف تُترجَم أوامرك إلى SQL فعلي.
باختصار:
إذا أردت فهم أعمق لكيفية بناء النماذج في Django 5 لتسهيل تحسين الأداء لاحقًا، راجع أيضًا: دليلك الشامل إلى Django 5 Models. بهذه الأدوات والخبرات، ستتمكن من بناء تطبيقات Django سريعة وقابلة للتوسع دون مفاجآت مزعجة في الأداء.
شرح أمثلة حقيقية لكيف يؤدي ORM إلى بطء التطبيق، وكيفية تحسين الاستعلامات بشكل جذري.
مساحة اعلانية