Menu
flag Ar iconالعربيةdown icon

قيود NOT NULL و DEFAULT في SQLite بشكل عملي

شرح عملي لقيدَي NOT NULL و DEFAULT في SQLite: ما الذي يفرضانه فعلياً، حيلة CURRENT_TIMESTAMP، ومشاكل إضافتهما إلى جدول موجود مسبقاً.

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

قيدان يستحقان وجودهما في كل جدول

معظم الأخطاء التي تظهر بسبب تصميم سيء للجداول ترجع في الغالب إلى أحد سببين: عمود تكون قيمته NULL بينما لم يكن أحد يتوقع ذلك، أو عمود تنقصه قيمة افترض التطبيق وجودها. هنا يأتي دور قيدَي NOT NULL وDEFAULT في sqlite لحل المشكلتين معًا، وإضافتهما لا تكلفك شيئًا تقريبًا.

عمود واحد إلزامي وبدون قيمة احتياطية، واثنان لهما قيم افتراضية. عملية INSERT كانت تحتاج فقط لتمرير email، وSQLite تكفّل بالباقي. هذه هي الفكرة كلها في مثال واحد، وبقية هذه الصفحة تتناول الحالات الطرفية فقط.

قيد NOT NULL في sqlite: ارفض NULL بدون استثناءات

قيد NOT NULL يفعل تماماً ما يوحي به اسمه. أي محاولة لإدخال NULL في العمود — سواء بإغفاله في جملة INSERT دون وجود قيمة افتراضية، أو بكتابة NULL صراحةً — ستفشل:

الرسالة الخطأ تظهر كالتالي:

خطأ في وقت التشغيل: فشل قيد NOT NULL: posts.title

النتيجة نفسها لو مرّرت NULL بشكل صريح:

INSERT INTO posts (id, title) VALUES (1, NULL);
-- خطأ وقت التشغيل: فشل قيد NOT NULL: posts.title

هذا هو العقد ببساطة. إذا كان العمود مطلوباً منطقياً، ضع عليه NOT NULL وستكون قد أغلقت الباب أمام فئة كاملة من الأخطاء — فلن يستطيع أي كود في التطبيق أن يُمرّر NULL إلى قاعدة البيانات خِلسةً.

القيمة الافتراضية DEFAULT في sqlite تُستخدم حين لا يُمرّر المستدعي قيمة

تعمل DEFAULT فقط عندما لا يذكر INSERT العمود إطلاقاً. أما إذا مرّرت NULL صراحةً، فلن تُنقذك أبداً:

عملية الإدخال الأولى تعتمد على القيمة الافتراضية، والثانية تتجاوزها. أما لو كتبت INSERT INTO tasks (title, status) VALUES ('x', NULL)، فسيظهر لك خطأ NOT NULL constraint failed — لأنك ذكرت اسم العمود صراحةً، وبالتالي لن تُفعَّل القيمة الافتراضية أصلاً.

هذا هو النموذج الذهني الذي يستحق أن تستوعبه جيداً للتفريق بين NOT NULL و DEFAULT في sqlite: الكلمة DEFAULT تملأ الفراغ في الأعمدة الغائبة فقط، بينما NOT NULL ترفض القيم الفارغة مهما كان مصدرها. هما خاصيتان مستقلتان تتكاملان بشكل جميل معاً.

القيم الافتراضية يمكن أن تكون تعبيرات (DEFAULT expression)

في الغالب تكون القيمة الافتراضية قيمة حرفية بسيطة (DEFAULT 0 أو DEFAULT '' أو DEFAULT 'pending')، لكن SQLite يسمح لك أيضاً بتمرير تعبير داخل أقواس. وهذه الطريقة المعتادة لتسجيل وقت إنشاء الصف تلقائياً، أو لتوليد معرّف عشوائي:

بعض الأمور المهمة التي ينبغي معرفتها:

  • يُحسب التعبير مع كل عملية إدراج، وليس مرة واحدة عند إنشاء الجدول. أي أن كل صف يحصل على طابعه الزمني الخاص ورمزه الخاص.
  • CURRENT_TIMESTAMP وCURRENT_DATE وCURRENT_TIME هي الكلمات المفتاحية الثلاث الوحيدة التي لا تحتاج إلى أقواس. أما ما عداها فيلزمه الأقواس.
  • لا يمكن للتعبير أن يشير إلى أعمدة أخرى أو استعلامات فرعية، بل يجب أن يكون مكتفياً بذاته.

إذا أردت أن يكون العمود اختيارياً لكن يُختم تلقائياً عند تمريره، احذف NOT NULL واحتفظ بالقيمة الافتراضية. أما إذا أردته إلزامياً ومختوماً تلقائياً في الوقت نفسه، فاستخدم الاثنين معاً.

استخدام DEFAULT NULL مسموح (وأحياناً يكون هو المقصود)

كتابة DEFAULT NULL تعادل عدم وضع قيمة افتراضية أصلاً، فالعمود يأخذ القيمة NULL حين لا تُمرّر له قيمة. لكن استخدامها مفيد حين تريد أن توضّح صراحةً في تعريف الجدول أن "لا قيمة" هي الحالة الابتدائية المقصودة:

يتصرّف العمودان bio و avatar بنفس الطريقة هنا. أمّا DEFAULT NULL المكتوبة على bio فهي تعليق على هيئة كود — تُخبر كل من يقرأ مخطط الجدول أنّ غياب السيرة الذاتية حالة طبيعية متوقّعة، لا سهو من المطوّر.

إضافة NOT NULL إلى جدول موجود مسبقًا

هنا تبدأ الأمور تصبح مزعجة قليلًا. الأمر ALTER TABLE في sqlite محدود عمدًا — فلا يمكنك تنفيذ ALTER COLUMN ... SET NOT NULL كما تفعل في Postgres مثلًا. ما يمكنك فعله فعلًا يعتمد على ما إذا كان العمود موجودًا بالفعل أم لا.

بالنسبة لعمود جديد كليًّا، فإنّ ADD COLUMN ... NOT NULL يعمل بلا مشاكل، لكن يجب عليك توفير قيمة افتراضية — وإلّا فإنّ الصفوف الموجودة ستحتوي فجأةً على NULL داخل عمود NOT NULL، وهذا أمر مستحيل:

جرّب نفس الأمر لكن بدون قيمة DEFAULT، وسترى أنك ستحصل على خطأ:

ALTER TABLE products ADD COLUMN sku TEXT NOT NULL;
-- خطأ وقت التشغيل: لا يمكن إضافة عمود NOT NULL بقيمة افتراضية NULL

بالنسبة لعمود موجود مسبقاً، لا يوجد تعديل مباشر في مكانه. الحيلة المتعارف عليها هي ما يُسمى بـ"رقصة إعادة البناء": تُنشئ جدولاً جديداً بالقيد الذي تريده، ثم تنسخ البيانات إليه، تحذف الجدول القديم، وأخيراً تُعيد تسمية الجديد. سنتناول هذه الخطوات بالتفصيل في صفحة drop-and-alter-table — يكفي الآن أن تعرف أن هذا القيد حقيقي، وعليك أن تأخذه بعين الاعتبار عند تصميم مخططك من البداية.

مثال واقعي يجمع بين القيدين

معظم الجداول في بيئات الإنتاج تستخدم القيدين معاً لتُعبّر عن "ما يفترض التطبيق أنه صحيح دائماً":

اقرأ هذا السكيما من أوله لآخره، وستعرف ما الذي يفعله التطبيق دون أن تطّلع على سطر واحد من الكود. حقل customer إلزامي ولا يملك قيمة احتياطية — على من يستدعي الإدراج أن يعرف صاحب الطلب. أما المبلغ والعملة والحالة، فلكلٍّ منها قيمة افتراضية منطقية، بحيث ينتج عن أبسط عملية INSERT صفٌّ متكامل ومتسق. حقل notes اختياري. وحقل created_at تملؤه قاعدة البيانات نفسها، وهذا هو المكان الوحيد الذي يجب أن يُملأ فيه.

هذه هي الفائدة الحقيقية من قيود الأعمدة في sqlite — فهي تحوّل الافتراضات الذهنية إلى قواعد تفرضها قاعدة البيانات بنفسها.

أخطاء شائعة احذر منها

إليك قائمة قصيرة بأمور توقع كثيرًا من المطورين:

  • تمرير NULL صراحةً يُلغي عمل DEFAULT. الجملة INSERT INTO t (col) VALUES (NULL) لن تستخدم القيمة الافتراضية. لا بدّ أن يكون العمود غائبًا كليًّا عن قائمة الأعمدة.
  • القيم الافتراضية المبنية على تعابير تحتاج إلى أقواس. الصيغة DEFAULT CURRENT_TIMESTAMP تعمل (لأنها واحدة من ثلاث كلمات مفتاحية خاصة)، أما DEFAULT lower(hex(randomblob(8))) فلا تعمل — لا بدّ من تغليفها هكذا: DEFAULT (lower(hex(randomblob(8)))).
  • NOT NULL لا يساوي رفض السلسلة الفارغة. القيمة '' نص صالح من نوع TEXT ولن تُطلق رسالة NOT NULL constraint failed. إن أردت منع السلاسل الفارغة أيضًا، فهذه مهمة قيد CHECK (في الصفحة التالية).
  • ADD COLUMN ... NOT NULL يستلزم وجود DEFAULT بقيمة غير NULL. وبدون ذلك، يرفض SQLite تنفيذ التعديل.

الخطوة التالية: قيود CHECK

يغطّي NOT NULL وDEFAULT حالتَي "يجب أن تكون القيمة موجودة" و"املأها إن غابت". أما الطبقة التالية من التحقق — مثل "يجب أن تكون موجبة" أو "يجب أن تكون ضمن قائمة محددة" أو "تاريخ الانتهاء يجب أن يأتي بعد تاريخ البدء" — فيوفّرها SQLite عبر قيود CHECK، التي تتيح لك كتابة أي تعبير منطقي عشوائي يجب أن يحقّقه كل صف. هذا موضوع الصفحة التالية.

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

كيف أجعل عموداً إلزامياً في SQLite؟

أضف NOT NULL عند تعريف العمود، هكذا: email TEXT NOT NULL. أي عملية INSERT أو UPDATE تحاول ترك هذا العمود فارغاً (NULL) ستفشل برسالة NOT NULL constraint failed. ومن الأفضل أن تقرنه بـ DEFAULT إذا كنت تريد قيمة احتياطية في حال لم يمررها المستدعي.

كيف تعمل القيم الافتراضية في SQLite؟

DEFAULT <value> يعطي العمود قيمة تُستخدم عندما لا يحدد INSERT قيمة له. القيمة الافتراضية يمكن أن تكون قيمة حرفية مثل DEFAULT 0 أو DEFAULT 'pending'، أو NULL، أو تعبيراً بين قوسين مثل DEFAULT (CURRENT_TIMESTAMP) أو DEFAULT (lower(hex(randomblob(8)))). التعابير يُعاد تقييمها مع كل عملية إدراج جديدة.

لماذا تظهر لي رسالة 'NOT NULL constraint failed' عند الإدراج؟

السبب أنك تدرج صفاً دون تمرير قيمة لعمود معرّف بـ NOT NULL وليس له DEFAULT. الحل: إما أن تضمّن العمود في عبارة INSERT، أو تعطيه DEFAULT، أو تخفف القيد. وانتبه: تمرير NULL صراحةً يطلق نفس الخطأ — لأن NOT NULL يرفض أي قيمة فارغة بغض النظر عن مصدرها.

هل يمكنني إضافة قيد NOT NULL إلى عمود موجود في SQLite؟

لا يمكن مباشرة، لأن ALTER TABLE ... ALTER COLUMN غير مدعوم في SQLite أصلاً. أمامك خياران: إما إضافة عمود جديد بصيغة NOT NULL DEFAULT <value> (وهنا DEFAULT ضروري لأن هناك صفوفاً موجودة)، أو إعادة بناء الجدول كاملاً: تنشئ جدولاً جديداً بالقيد المطلوب، تنسخ البيانات إليه، تحذف القديم، ثم تعيد تسمية الجديد.

Coddy programming languages illustration

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

ابدأ الآن