تذكير: on_delete التقليدية تعمل من جهة Django
تعلّمت بدرس العلاقات أن on_delete=models.CASCADE تحذف السجلات المرتبطة
تلقائيًا. لكن آلية العمل التقليدية تمرّ دائمًا عبر Django نفسه: عند حذف
سجل، Django يجلب كل السجلات المرتبطة أولًا (استعلام SELECT)، ثم
يحذفها واحدة واحدة أو بجملة DELETE منفصلة — حتى لو كانت قاعدة البيانات
نفسها تدعم CASCADE على مستوى القيد (constraint) مباشرة.
الجديد في Django 6.1: خيارات على مستوى قاعدة البيانات
بدءًا من Django 6.1، صار on_delete يقبل مجموعة قيم إضافية تُنفَّذ
داخل قاعدة البيانات نفسها عبر قيد ON DELETE بجدول SQL، بدل المرور
بـ Django:
from django.db import models
class Post(models.Model):
author = models.ForeignKey(
"Author",
on_delete=models.DB_CASCADE, # ← تُنفَّذ داخل قاعدة البيانات مباشرة
)
القيم الجديدة تُستورَد من django.db.models مثل القيم التقليدية تمامًا:
DB_CASCADE، DB_PROTECT، DB_RESTRICT، DB_SET_NULL، DB_SET_DEFAULT،
وDB_DO_NOTHING — كل واحدة تقابل نظيرتها التقليدية بالسلوك المنطقي، لكن
بتنفيذ مختلف جذريًا.
لماذا أسرع؟
مع CASCADE التقليدية، حذف كاتب له 10,000 منشور يعني: جلب كل المنشورات
لذاكرة Django أولًا، ثم حذفها. مع DB_CASCADE، قاعدة البيانات تحذفها
مباشرة بجملة SQL واحدة دون أن يلمسها Django إطلاقًا — أسرع بكثير مع كميات
بيانات كبيرة.
قيدان مهمّان قبل الاستخدام
1. لا إشارات pre_delete / post_delete للسجلات المرتبطة
# ❌ لن يُستدعى إطلاقًا للمنشورات المحذوفة عبر DB_CASCADE
@receiver(pre_delete, sender=Post)
def log_post_deletion(sender, instance, **kwargs):
print(f"سيُحذف: {instance.title}")
بما أن قاعدة البيانات هي من تحذف السجلات المرتبطة مباشرة، Django لا
"يعلم" بحذفها فرديًا، فلا تُطلَق إشاراته إطلاقًا لتلك السجلات — فقط
للسجل الأساسي الذي استدعيت عليه .delete().
2. نتيجة delete() تصبح جزئية
author.delete()
# مع CASCADE التقليدية: (10001, {'blog.Author': 1, 'blog.Post': 10000})
# مع DB_CASCADE: (1, {'blog.Author': 1}) ← لا تُحصي المنشورات المحذوفة!
القاموس المُرجَع من delete() يُبلّغ فقط عن الكائنات التي تعامل معها
Django نفسه. السجلات المحذوفة عبر قيد قاعدة البيانات لا تظهر بالعدّ، لأن
Django لا يعرف عددها أصلًا.
جدول سريع: التقليدية مقابل مستوى قاعدة البيانات
| المعيار | CASCADE (تقليدية) | DB_CASCADE (جديدة) |
|---|---|---|
| مكان التنفيذ | كود Django (Python) | قاعدة البيانات مباشرة (SQL) |
| الأداء مع بيانات كبيرة | أبطأ — يجلب السجلات أولًا | أسرع بكثير — بلا جلب مسبق |
| إشارات pre_delete/post_delete | تُطلَق لكل سجل مرتبط | لا تُطلَق للسجلات المحذوفة عبر القيد |
| دقّة نتيجة delete() | كاملة ومفصّلة | جزئية — لا تُحصي المحذوف بالقيد |
💡 استخدم الخيارات الجديدة (
DB_CASCADEوأخواتها) لعلاقات بحجم بيانات كبير لا تعتمد منطقك على إشارات الحذف. أبقِ الخيارات التقليدية (CASCADE,PROTECT, ...) حيث تحتاج التحكم الكامل أو منطق إشارات مخصّص.
أخطاء شائعة
- استبدال كل
CASCADEبـDB_CASCADEتلقائيًا بمشروع قائم يعتمد على إشارةpost_deleteلتنظيف ملفات مرتبطة أو تسجيل نشاط — يتوقّف هذا المنطق عن العمل بصمت للسجلات المحذوفة بالقيد. - الاعتماد على القاموس المُرجَع من
delete()لعدّ كل ما تأثّر بالعملية عند استخدام خيار DB-level — العدّ يكون جزئيًا كما شرحنا أعلاه. - توقّع أن
DB_RESTRICT/DB_PROTECTيمنعان الحذف بنفس رسالة الخطأ التقليدية بالضبط — الاستثناء يأتي غالبًا من قاعدة البيانات نفسها (IntegrityError) لا من طبقة Django كما بالخيار التقليدي.
🎯 التالي: الخلاصة الشاملة لمسار Django وخطواتك بعده.