Menu
flag Ar iconالعربيةdown icon
جرّب في Playground

المفاتيح الأجنبية في SQLite: REFERENCES و ON DELETE

تعرّف على آلية عمل المفاتيح الأجنبية في SQLite: كيفية تعريفها بـ REFERENCES، تفعيلها عبر PRAGMA، واختيار سلوك ON DELETE المناسب لجداولك.

تحتوي هذه الصفحة على محررات قابلة للتشغيل — حرّر، شغّل، وشاهد النتيجة فوراً.

المفتاح الأجنبي: رابط بين جدولين

المفتاح الأجنبي (foreign key) في sqlite هو ببساطة عمود في جدول لا تُقبل قيمته إلا إذا كانت موجودة فعلاً في صف بجدول آخر. هكذا تقول قواعد البيانات العلائقية إنّ "هذا الصف في جدول posts يخصّ ذاك الصف في جدول authors"، دون الحاجة إلى تكرار اسم الكاتب وبريده في كل تدوينة.

إليك أبسط مثال ممكن: جدول أب وجدول ابن يربط بينهما مفتاح أجنبي.

author_id INTEGER REFERENCES authors(id) هو تعريف المفتاح الأجنبي بالكامل. معناه ببساطة: هذا العمود يحمل قيمة id مأخوذة من جدول authors. الآن أصبحت قاعدة البيانات على علم بأن الجدولين مرتبطان ببعضهما، وستقوم — إذا كان التحقق مفعّلاً — برفض أي إدخال يشير إلى مؤلف غير موجود.

المفاتيح الأجنبية في sqlite معطّلة افتراضياً

هذه أهم معلومة يجب أن تعرفها عن المفاتيح الأجنبية في sqlite، وهي تفاجئ الجميع: محرّك SQLite يقرأ جملة REFERENCES ويفهمها، لكنه لا يطبّقها فعلياً ما لم تطلب منه ذلك صراحةً. والسبب يعود إلى التوافق مع الإصدارات القديمة، إذ بُنيت قواعد بيانات كثيرة قبل أن تظهر هذه الميزة أصلاً.

شاهد ما يحدث حين يكون التحقق معطّلاً:

لقد أُدرج السطر اليتيم مباشرةً دون أي اعتراض. ولكي تحصل فعليًا على الحماية التي تريدها من قيود المفاتيح الأجنبية، نفّذ الأمر PRAGMA foreign_keys = ON; في بداية كل اتصال جديد بقاعدة البيانات:

الآن ستفشل عملية الإدراج وتظهر لك رسالة FOREIGN KEY constraint failed. السبب أنّ هذا الـ pragma مرتبط بالاتصال نفسه لا بالملف، فلا يُحفَظ داخل قاعدة البيانات. يعني كل تطبيق وكل جلسة CLI وكل test fixture يجب أن يضبطه من جديد. ولهذا السبب، معظم الأكواد في بيئة الإنتاج تنفّذ الأمر PRAGMA foreign_keys = ON; مباشرةً بعد فتح الاتصال.

ماذا تشترط جملة REFERENCES فعليًا؟

العمود الذي تُشير إليه يجب أن يكون PRIMARY KEY أو يحمل قيد UNIQUE، حتى يستطيع SQLite ضمان أنّ عملية البحث ستُرجع نتيجة واحدة لا لبس فيها. كذلك يُفضَّل أن تكون الأنواع متوافقة، فرغم أنّ SQLite متساهل في موضوع الأنواع، إلا أنّ خلطها يفتح الباب لمفاجآت غير مرغوبة.

يمكنك كتابة المفتاح الأجنبي بطريقتين. الأولى مدمجة داخل تعريف العمود نفسه:

أو يمكنك تعريفه كقيد مستقل على مستوى الجدول، وهذا ضروري عندما يمتد المفتاح الأجنبي عبر أكثر من عمود:

كلا الصيغتين تُنتجان نفس القيد تمامًا، فاستخدم ما يبدو أوضح وأكثر انسجامًا مع الجدول الذي بين يديك.

ON DELETE: ماذا يحدث للأبناء عند حذف الأب؟

عندما تحذف صفًا من الجدول الأب، يجب على SQLite أن يقرر ماذا يفعل بالصفوف الأبناء التي تشير إليه. أنت من يحدد هذه السياسة عبر ON DELETE:

حذف Ada أدّى إلى حذف منشوريها معًا. الخيارات المتاحة هي:

  • CASCADE — يحذف السجلات التابعة أيضًا. مناسب للبيانات "المملوكة" مثل المنشورات التابعة لكاتب أو العناصر التابعة لطلب شراء.
  • SET NULL — يجعل قيمة عمود المفتاح الأجنبي NULL. مفيد عندما تريد بقاء السجلات التابعة بعد اختفاء الأب (مثلًا: التعليقات على مستخدم محذوف تتحول إلى مجهولة).
  • SET DEFAULT — يضبط عمود المفتاح الأجنبي على القيمة الافتراضية المعرَّفة له.
  • RESTRICT — يمنع الحذف إذا وُجد أي سجل تابع. يفشل فورًا عند تنفيذ العبارة.
  • NO ACTION — السلوك الافتراضي. يشبه RESTRICT عمليًا في معظم الحالات (يؤجل الفحص إلى وقت الـ commit، لكن النتيجة واحدة: لا يُسمح بترك سجلات تابعة معلّقة).

أما ON UPDATE فيعمل بالطريقة نفسها عند تغيير قيمة مفتاح الأب، وإن كان تعديل المفاتيح الأساسية أمرًا نادرًا في الواقع.

حل خطأ foreign key constraint failed وفهم سببه

ستواجه هذا الخطأ في حالتين. الأولى: عند إدراج أو تحديث سجل تابع بقيمة لا يقابلها أي سجل في جدول الأب:

sqlite> INSERT INTO posts (title, author_id) VALUES ('Stray', 999);
Runtime error: FOREIGN KEY constraint failed

إمّا أنّ المؤلف صاحب المعرّف 999 غير موجود، أو أنّك خلطت بين أنواع الأعمدة. أدخِل السجل الأب أولاً، أو صحّح القيمة.

ثانياً: حذف (أو تحديث) سجل أب لا يزال مرتبطاً بسجلات أبناء، عندما يستخدم المفتاح الأجنبي RESTRICT أو NO ACTION:

sqlite> DELETE FROM authors WHERE id = 1;
Runtime error: FOREIGN KEY constraint failed

إمّا أن تحذف السجلات الأبناء أولًا، أو أن تغيّر المفتاح الأجنبي إلى ON DELETE CASCADE أو SET NULL إذا كان السلوك التتابعي هو ما تريده فعلًا.

وهناك أيضًا خطأ أقل شيوعًا من نفس العائلة، وهو FOREIGN KEY mismatch. يظهر هذا الخطأ حين لا يكون العمود المُشار إليه مفتاحًا أساسيًا أو فريدًا، أو حين لا يتطابق عدد الأعمدة بين الطرفين. وهو خطأ في تصميم المخطط (schema)، لا في البيانات.

إضافة مفتاح أجنبي إلى جدول موجود مسبقًا

الأمر ALTER TABLE في SQLite محدود الإمكانيات؛ فبإمكانك إضافة عمود جديد يحتوي على مفتاح أجنبي، لكن لا يمكنك إضافة مفتاح أجنبي إلى عمود موجود سلفًا. والحل المعتاد هو ما يُعرف بحركة "إعادة التسمية وإعادة البناء":

النمط المتّبع: عطّل التحقق من المفاتيح الأجنبية، أنشئ الجدول الجديد بالقيود التي تريدها، انسخ البيانات، احذف الجدول القديم، ثم أعد التسمية. وجود BEGIN/COMMIT يضمن أن العملية ذرّية. بعد الانتهاء، أعد تفعيل التحقق وسيقوم SQLite بفحص جميع الصفوف الموجودة مقابل القيود الجديدة — لكن انتبه: إن كانت هناك بيانات غير صالحة، فالمعاملة تكون قد تمّت بالفعل، لذا تحقّق من البيانات مسبقاً إذا كان الأمر يقلقك.

شغّل PRAGMA foreign_key_check; بعد الترحيل للتأكد من عدم وجود صفوف يتيمة.

مثال واقعي على المخطط

لنجمع كل ما سبق في مخطط بسيط لمدوّنة، يحتوي على جداول أب وجداول ابن، إضافة إلى جدول ربط لعلاقة many-to-many مع الوسوم:

ثلاث ملاحظات مهمّة هنا. أولاً، الحقل author_id معرَّف على أنه NOT NULL، يعني كل تدوينة يجب أن يكون لها مؤلف. ثانياً، علاقة posts → authors فيها CASCADE، فلو حذفت مؤلفاً، تروح كل تدويناته معه. ثالثاً، جدول الربط post_tags يطبّق CASCADE من الطرفين، فحذف تدوينة أو وسم يُنظِّف صفوف الربط تلقائياً.

عادات توفّر عليك وجع الرأس لاحقاً

  • فعِّل PRAGMA foreign_keys = ON; مع كل اتصال جديد بقاعدة البيانات. اجعلها جزءاً من روتين فتح القاعدة في تطبيقك، لا أمراً تتذكّره أحياناً وتنساه أحياناً.
  • أضف فهرساً (index) على عمود المفتاح الأجنبي. SQLite يفهرس مفتاح الجدول الأب تلقائياً، لكنه لا يفعل ذلك للجدول الابن، وON DELETE CASCADE يُجري بحثاً في الجدول الابن في كل مرة تُحذف فيها صفوف من الأب.
  • اختر سلوك ON DELETE بوعي. القيمة الافتراضية (NO ACTION) آمنة، لكنها تعني أنك ستصطدم برسالة "constraint failed" في كل محاولة تنظيف. حدِّد ما الذي يجب أن يحدث وصرِّح به في تعريف الجدول.
  • شغِّل PRAGMA foreign_key_check; بعد عمليات الترحيل (migrations) أو الاستيراد بالجملة، حتى تكتشف الصفوف اليتيمة قبل أن تتحوّل إلى أخطاء غامضة.

التالي: INNER JOIN

المفاتيح الأجنبية تصف العلاقة بين الجداول، أما عمليات الـ joins فهي الطريقة الفعلية للاستعلام عبرها. الصفحة التالية تشرح INNER JOIN: كيف تدمج صفوفاً من جداول مرتبطة وتختار الأعمدة التي تريدها من كل جدول.

الأسئلة الشائعة

كيف أُنشئ مفتاحًا أجنبيًا في SQLite؟

أضِف عبارة REFERENCES other_table(column) ضمن تعريف العمود داخل CREATE TABLE. مثلًا: author_id INTEGER REFERENCES authors(id) تجعل العمود author_id يشير إلى صف في جدول authors. لاحظ أنّ العمود المُشار إليه يجب أن يكون PRIMARY KEY أو يحمل قيدًا من نوع UNIQUE.

لماذا لا تعمل المفاتيح الأجنبية في SQLite ولا تُطبَّق القيود؟

SQLite يقرأ تعريفات المفاتيح الأجنبية لكنه لا يُطبّقها افتراضيًا، فلا بدّ من تفعيلها يدويًا. نفّذ الأمر PRAGMA foreign_keys = ON; في بداية كل اتصال جديد. هذا الإعداد مرتبط بالاتصال نفسه وليس مخزّنًا داخل قاعدة البيانات، لذا تحتاج المكتبات وأداة CLI إلى ضبطه في كل مرة تتصل فيها.

ما الذي تفعله ON DELETE CASCADE في SQLite؟

تُخبر ON DELETE CASCADE محرّك SQLite بأن يحذف الصفوف التابعة تلقائيًا عند حذف الصف الأب. تتوفّر أيضًا خيارات أخرى: RESTRICT لمنع عملية الحذف، وSET NULL لتفريغ قيمة المفتاح الأجنبي، وSET DEFAULT، وNO ACTION (الافتراضي، ويعمل عمليًا مثل RESTRICT). اختر ما يناسبك بناءً على ما إذا كانت الصفوف التابعة تبقى ذات معنى بدون الصف الأب أم لا.

كيف أُصلح خطأ foreign key constraint failed في SQLite؟

يعني هذا الخطأ أنّك حاولت إدراج أو تعديل صف بقيمة مفتاح أجنبي لا تطابق أي صف في الجدول المُشار إليه، أو حاولت حذف صف أب لا يزال له صفوف تابعة. تأكّد أوّلًا من وجود الصف المُشار إليه قبل الإدراج، أو فعّل ON DELETE CASCADE إذا أردت حذف الصفوف التابعة تلقائيًا مع الأب.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن