أصرف تطبيقات مولَّدة بالذكاء الاصطناعي بأمان أكبر بوضع قواعد PostgreSQL مثل NOT NULL وCHECK وUNIQUE وFOREIGN KEY قبل الشيفرة والاختبارات.

غالبًا ما تبدو الشيفرة المولَّدة بالذكاء الاصطناعي صحيحة لأنها تتعامل مع السيناريوهات المثالية. التطبيقات الحقيقية تفشل في المنتصف الفوضوي: نموذج يرسل سلسلة فارغة بدلًا من null، مهمة خلفية تعيد المحاولة فتُنشئ نفس السجل مرتين، أو حذف يزيل صف أصل ويترك أطفالًا بلا مرجع. هذه ليست أخطاء غريبة؛ تظهر كحقول مطلوبة فارغة، وقيم "فريدة" مكررة، وصفوف يتيمة تشير إلى لا شيء.
تمر هذه الأخطاء أيضًا عبر مراجعة الشيفرة والاختبارات الأساسية لسبب بسيط: المراجعون يقرؤون النية، وليس كل حالة حافة. عادةً ما تغطي الاختبارات بعض الأمثلة النموذجية، وليس أسابيع من سلوك المستخدم الحقيقي، أو استيراد CSV، أو محاولات الشبكة المتقلبة، أو الطلبات المتزامنة. إذا ولّد مساعد الشيفرة الكود، فقد يفوته تحقق بسيط لكنه حاسم مثل إزالة الفراغات المحيطة، التحقق من النطاقات، أو الحماية من ظروف التنافس.
"القيود أولًا، الشيفرة ثانيًا" يعني أن تضع قواعد غير قابلة للتفاوض في قاعدة البيانات حتى لا يُحفَظ بيانات خاطئة مهما كان مسار الكتابة. يجب أن يستمر التطبيق في التحقق من المدخلات لتحسين رسائل الخطأ، لكن قاعدة البيانات هي التي تفرض الحقيقة. هنا تتألق قيود PostgreSQL: تحمِيك من فئات كاملة من الأخطاء.
مثال سريع: تخيّل CRM صغير. سكربت استيراد مولَّد يخلق جهات اتصال. أحد الصفوف له بريد إلكتروني "" (فارغ)، صفّان يتكرران بنفس البريد مع اختلاف حالة الحروف، واتصال واحد يشير إلى account_id غير موجود لأن الحساب حُذف في عملية أخرى. بدون قيود، كل ذلك قد يصل للإنتاج ويكسر التقارير لاحقًا.
مع قواعد قاعدة بيانات مناسبة، تفشل تلك الكتابات فورًا، بالقرب من المصدر. لا يمكن أن تكون الحقول المطلوبة مفقودة، ولا تتسلل التكرارات أثناء المحاولات المتكررة، ولا تشير العلاقات إلى سجلات محذوفة أو غير موجودة، ولا تقع القيم خارج النطاق المسموح.
القيود لا تمنع كل خطأ. لن تُصلح واجهة مستخدم مربكة، أو حساب خصم خاطئ، أو استعلام بطيء. لكنها توقف تراكم البيانات السيئة بصمت، وهو المكان الذي تصبح فيه "أخطاء حالات الحافة المولَّدة بالذكاء الاصطناعي" مكلفة في الغالب.
تطبيقك نادراً ما يكون قاعدة شيفرة واحدة تتحدث مع مستخدم واحد. المنتج النموذجي يحتوي واجهة ويب، تطبيق محمول، شاشات إدارة، مهام خلفية، استيراد من CSV، وأحيانًا تكاملات من طرف ثالث. كل مسار يمكنه إنشاء أو تغيير البيانات. إذا اضطر كل مسار لتذكّر نفس القواعد، فواحدٌ منها سيتجاوز النسيان.
قاعدة البيانات هي المكان المشترك الذي يرونه جميعًا. عندما تعاملها كبوابة نهائية، تُطبق القواعد على كل شيء تلقائيًا. تُحوّل قيود PostgreSQL عبارة "نفترض أن هذا صحيح" إلى "هذا يجب أن يكون صحيحًا، وإلا تفشل عملية الكتابة".
الشيفرة المولَّدة بالذكاء الاصطناعي تجعل هذا أكثر أهمية. قد يضيف النموذج تحققًا في واجهة React لكنه يغفل حالة طرفية في مهمة خلفية. أو يتعامل جيدًا مع بيانات المسار السعيد، ثم ينكسر عندما يُدخل زبون حقيقي شيئًا غير متوقع. تلتقط القيود المشاكل في اللحظة التي تحاول فيها البيانات السيئة الدخول، لا بعد أسابيع عندما تكون منشغلًا بتصحيح تقارير غامضة.
عندما تتخطى القيود، تكون البيانات السيئة غالبًا صامتة. تُنجز الحفظ، يستمر التطبيق، وتظهر المشكلة لاحقًا كتذكرة دعم، أو اختلاف في الفواتير، أو لوحة بيانات لا يثق بها أحد. التنظيف مكلف لأنك تصلح التاريخ، لا طلبًا واحدًا.
عادةً ما تتسلل البيانات السيئة من مواقف يومية: إصدار عميل جديد يرسل حقلًا فارغًا بدلًا من مفقود، محاولة إعادة تخلق التكرارات، تعديل إداري يتجاوز تحقق الواجهة، ملف استيراد بتنسيق غير متسق، أو اثنان من المستخدمين يحدثان سجلات مرتبطة في نفس الوقت.
نموذج ذهني مفيد: اقبل البيانات فقط إذا كانت صالحة عند الحدود. عمليًا، يجب أن تشمل هذه الحدود قاعدة البيانات، لأن قاعدة البيانات ترى كل عمليات الكتابة.
NOT NULL هو أبسط قيد في PostgreSQL، ويمنع فئة مفاجئة وكبيرة من الأخطاء. إذا كانت القيمة يجب أن توجد لكي يكون الصف منطقيًا، اجعل قاعدة البيانات تفرض ذلك.
غالبًا ما يكون NOT NULL مناسبًا للمُعرفات، الأسماء المطلوبة، والطوابع الزمنية. إذا لم تستطع إنشاء سجل صالح بدونه، فلا تسمح بأن يكون فارغًا. في CRM صغير، عميل محتمل بدون مالك أو وقت إنشاء ليس "عميلًا جزئيًا"؛ إنه بيانات معطوبة ستسبب سلوكًا غريبًا لاحقًا.
يظهر NULL أكثر مع الشيفرة المولَّدة بالذكاء الاصطناعي لأن من السهل إنشاء مسارات "اختيارية" دون ملاحظة. قد يكون حقل النموذج اختياريًا في الواجهة، قد يقبل API مفتاحًا مفقودًا، وقد يتخطى فرع في دالة الإنشاء تعيين قيمة. كل شيء ما زال يترجم وتنجح اختبارات السيناريو السعيد. ثم يستورد المستخدمون ملف CSV بخلايا فارغة، أو يرسل عميل محمول حمولة مختلفة، ويهبط NULL في قاعدة البيانات.
نمط جيد هو الجمع بين NOT NULL وقيمة افتراضية معقولة للحقول التي تمتلكها النظام:
created_at TIMESTAMP NOT NULL DEFAULT now()status TEXT NOT NULL DEFAULT 'new'is_active BOOLEAN NOT NULL DEFAULT trueالافتراضات ليست دائمًا مفيدة. لا تضع افتراضًا لحقول يُدخلها المستخدم مثل email أو company_name لمجرد إرضاء NOT NULL. السلسلة الفارغة ليست "أكثر صحة" من NULL؛ إنها تخفي المشكلة.
عندما تكون غير متأكد، قرر هل القيمة مجهولة حقًا، أم أنها تمثل حالة مختلفة. إذا كانت "غير مقدمة بعد" ذات معنى، ففكر في عمود حالة منفصل بدلًا من السماح بـ NULL في كل مكان. على سبيل المثال، اترك phone قابلاً لأن يكون NULL، لكن أضف phone_status مثل missing أو requested أو verified. هذا يحافظ على المعنى متسقًا عبر الشيفرة.
قيد CHECK هو وعد يقطعه جدولك: كل صف يجب أن يفي بقيد، في كل مرة. إنه أحد أسهل الطرق لمنع حالات الحافة من إنشاء سجلات تبدو صحيحة في الشيفرة لكنها غير منطقية في الواقع.
تعمل قيود CHECK بشكل أفضل للقواعد التي تعتمد فقط على قيم في نفس الصف: نطاقات رقمية، قيم مسموح بها، وعلاقات بسيطة بين الأعمدة.
-- 1) Totals should never be negative
ALTER TABLE invoices
ADD CONSTRAINT invoices_total_nonnegative
CHECK (total_cents >= 0);
-- 2) Enum-like allowed values without adding a custom type
ALTER TABLE tickets
ADD CONSTRAINT tickets_status_allowed
CHECK (status IN ('new', 'open', 'waiting', 'closed'));
-- 3) Date order rules
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_date_order
CHECK (end_date IS NULL OR end_date >= start_date);
قيد CHECK الجيد يكون قابلًا للقراءة بسرعة. عاملْه كوثيقة لبياناتك. فضّل التعبيرات القصيرة، أسماء قيود واضحة، وأنماط متوقعة.
CHECK ليس الأداة المناسبة لكل شيء. إذا احتاجت قاعدة أن تبحث عن صفوف أخرى، أو تجمع بيانات، أو تقارن بين جداول (مثل "حساب لا يمكن أن يتجاوز حد خطته"), احتفظ بالمنطق في شيفرة التطبيق، أو مشغلات، أو مهمة خلفية مُتحكم بها.
قاعدة UNIQUE بسيطة: ترفض قاعدة البيانات تخزين صفين لهما نفس القيمة في العمود المقيد (أو نفس تركيبة القيم عبر أعمدة متعددة). هذا يقضي على فئة كاملة من الأخطاء حيث يُنفّذ مسار "إنشاء" مرتين، أو تحدث محاولة إعادة، أو يقدم مستخدمان نفس الشيء في الوقت نفسه.
UNIQUE يضمن عدم التكرار للقيم التي تعرّفها تمامًا. لا يضمن أن القيمة موجودة (NOT NULL)، أو أنها تتبع صيغة (CHECK)، أو أنها تطابق فكرتك عن المساواة (حالة الحروف، الفراغات، علامات الترقيم) ما لم تُعرّف ذلك.
أماكن شائعة تريد فيها التفرد تشمل البريد الإلكتروني في جدول المستخدمين، external_id من نظام آخر، أو اسم يجب أن يكون فريدًا داخل حساب مثل (account_id, name).
تحذير شائع: NULL و UNIQUE. في PostgreSQL، يُعامل NULL على أنه "مجهول"، لذا تُسمح قيم NULL المتعددة تحت قيد UNIQUE. إذا كنت تقصد "يجب أن توجد القيمة وتكون فريدة"، اجمع UNIQUE مع NOT NULL.
نمط عملي لمعرفات المستخدمين هو التفرد بغض النظر عن حالة الحروف. سيكتب الناس "[email protected]" ثم لاحقًا "[email protected]" ويتوقعون أنهما نفس الشيء.
-- Case-insensitive unique email
CREATE UNIQUE INDEX users_email_unique_ci
ON users (lower(email));
-- Unique contact name per account
ALTER TABLE contacts
ADD CONSTRAINT contacts_account_name_unique UNIQUE (account_id, name);
عرّف ما يعنيه "التكرار" لمستخدميك (حالة الحروف، الفراغات، لكل حساب مقابل عام)، ثم شفّره مرة واحدة حتى تتبع كل مسارات الشيفرة نفس القاعدة.
المفتاح الأجنبي يقول: "هذا الصف يجب أن يشير إلى صف حقيقي هناك." بدون ذلك، قد تنشئ الشيفرة سجلات يتيمة تبدو صحيحة بمعزل عن غيرها لكنها تكسر التطبيق لاحقًا. مثلًا: ملاحظة تشير إلى عميل حُذف، أو فاتورة تُشير إلى معرف مستخدم لم يوجد قط.
المفاتيح الأجنبية مهمة خصوصًا عندما يحدث عملان قريبان من بعضهما: حذف وإنشاء، محاولة إعادة بعد انتهاء المهلة، أو مهمة خلفية تعمل ببيانات قديمة. قاعدة البيانات أفضل في فرض الاتساق من أن يتذكر كل مسار في التطبيق الفحص.
خيار ON DELETE يجب أن يتطابق مع المعنى الواقعي للعلاقة. اسأل: "إذا اختفى الأصل، هل يجب أن يستمر وجود الطفل؟"
RESTRICT (أو NO ACTION): يمنع حذف الأصل إذا كانت هناك أطفال موجودة.CASCADE: حذف الأصل يحذف الأطفال أيضًا.SET NULL: إبقاء الطفل لكن إزالة العلاقة.كن حذرًا مع CASCADE. قد يكون صحيحًا، لكنه قد يمحو أكثر مما توقعت عندما يحذف خطأ أو حذف إداري صفًا أصلًا.
في التطبيقات متعددة المستأجرين، المفاتيح الأجنبية ليست مجرد صحة. إنها تمنع تسرب البيانات عبر الحسابات. نمط شائع هو تضمين account_id على كل جدول مملوك للمستأجر وربط العلاقات عبره.
CREATE TABLE contacts (
account_id bigint NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
PRIMARY KEY (account_id, id)
);
CREATE TABLE notes (
account_id bigint NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
contact_id bigint NOT NULL,
body text NOT NULL,
PRIMARY KEY (account_id, id),
FOREIGN KEY (account_id, contact_id)
REFERENCES contacts (account_id, id)
ON DELETE RESTRICT
);
هذا يفرض "من يملك ماذا" في المخطط: لا يمكن لملاحظة أن تشير إلى جهة اتصال في حساب مختلف، حتى لو حاولت الشيفرة (أو استعلام مولَّد بواسطة LLM) ذلك.
ابدأ بكتابة قائمة قصيرة من الثوابت: حقائق يجب أن تكون صحيحة دائمًا. اجعلها بسيطة. "كل جهة اتصال تحتاج إلى بريد إلكتروني." "يجب أن تكون الحالة واحدة من قيم مسموح بها." "يجب أن تنتمي الفاتورة إلى عميل حقيقي." هذه هي القواعد التي تريد قاعدة البيانات أن تفرضها في كل مرة.
نَفِّذ التغييرات في ترحيلات صغيرة حتى لا تُفاجأ بيئة الإنتاج:
NOT NULL, UNIQUE, CHECK, FOREIGN KEY).الجزء الفوضوي هو البيانات السيئة الموجودة أصلًا. خطط لذلك. بالنسبة للتكرارات، اختر صفًا فائزًا وادمج الباقي مع ملاحظة تدقيقية صغيرة. بالنسبة للحقول المطلوبة المفقودة، اختر قيمة افتراضية آمنة فقط إذا كانت آمنة حقًا؛ وإلا ضعها في حجر صحي. بالنسبة للعلاقات المكسورة، أعد تعيين صفوف الأطفال إلى الأصل الصحيح أو احذف الصفوف السيئة.
بعد كل ترحيل، اختبر ببعض عمليات كتابة يفترض أن تفشل: أدرج صفًا بقيمة مطلوبة مفقودة، أدخل مفتاح مكرر، أدخل قيمة خارج النطاق، واحرج مرجع أصل مفقود. عمليات الكتابة الفاشلة إشارة مفيدة؛ تظهر أين اعتمد التطبيق بصمت على سلوك "المحاولة الأفضل".
تخيل CRM صغير: حسابات (كل عميل لخدمة SaaS الخاصة بك)، شركات يتعاملون معها، جهات اتصال في تلك الشركات، وصفقات مرتبطة بشركة.
هذا بالضبط نوع التطبيق الذي يولده الناس بسرعة باستخدام أداة دردشة. يبدو جيدًا في العروض التوضيحية، لكن البيانات الحقيقية تتسخ سريعًا. يظهر خطآن مبكرًا عادةً: تكرار جهات الاتصال (نفس البريد مدخَل مرتين بطرق مختلفة قليلاً)، وصفقات خُلِقت بدون company_id لأن مسار إنشائي نسي تعيينه. آخر كلاسيكي هو قيمة صفقة سالبة بعد إعادة هيكلة أو خطأ في التحليل.
الحل ليس المزيد من عبارات if. إنه بعض القيود المختارة جيدًا التي تجعل تخزين البيانات السيئة مستحيلًا.
-- Contacts: prevent duplicates per account
ALTER TABLE contacts
ADD CONSTRAINT contacts_account_email_uniq UNIQUE (account_id, email);
-- Deals: require a company and keep the relationship valid
ALTER TABLE deals
ALTER COLUMN company_id SET NOT NULL,
ADD CONSTRAINT deals_company_fk
FOREIGN KEY (company_id) REFERENCES companies(id);
-- Deals: deal value cannot be negative
ALTER TABLE deals
ADD CONSTRAINT deals_value_nonneg CHECK (deal_value >= 0);
-- A few obvious required fields
ALTER TABLE companies
ALTER COLUMN name SET NOT NULL;
ALTER TABLE contacts
ALTER COLUMN email SET NOT NULL;
هذا ليس عن التشدد لمجرد التشدد. أنت تحوّل توقعات غامضة إلى قواعد تفرضها قاعدة البيانات في كل مرة، بغض النظر عن أي جزء من التطبيق يكتب البيانات.
بعد وجود هذه القيود، يصبح التطبيق أبسط. يمكنك إزالة الكثير من الفحوص الدفاعية التي تحاول اكتشاف التكرارات بعد وقوعها. تصبح الفشلات واضحة وقابلة للإجراء (مثل "البريد الإلكتروني موجود بالفعل لهذا الحساب" بدلًا من سلوك غريب لاحقًا). وعندما ينسى مسار API المولَّد حقلًا أو يسيء التعامل مع قيمة، تفشل عملية الكتابة فورًا بدلًا من إفساد قاعدة البيانات بصمت.
تعمل القيود بشكل أفضل عندما تطابق كيفية عمل العمل فعليًا. معظم المصاعب تأتي من إضافة قواعد تبدو "آمنة" الآن لكنها تتحول إلى مفاجآت لاحقًا.
فخ شائع هو استخدام ON DELETE CASCADE في كل مكان. يبدو أنيقًا حتى يحذف شخص ما صفًا أصلًا فتُمحى نصف النظام. قد تكون الـ Cascades صحيحة للبيانات المملوكة حقًا (مثل عناصر سطر مسودة لا يجب أن توجد بمفردها)، لكنها محفوفة بالمخاطر للسجلات التي يعتبرها الناس مهمة (العملاء، الفواتير، التذاكر). إذا لم تكن متأكدًا، فضّل RESTRICT وتعامل مع الحذف عمدًا.
مشكلة أخرى هي كتابة قيود CHECK ضيقة للغاية. "يجب أن تكون الحالة 'new' أو 'won' أو 'lost'" تبدو جيدة حتى تحتاج "paused" أو "archived". قيد CHECK الجيد يصف حقيقة مستقرة، لا خيار واجهة مؤقت. "amount >= 0" يبلى جيدًا. "country in (...)" غالبًا لا يبقى مناسبًا.
قليل من المشاكل المتكررة عندما تضيف فرق قيودًا بعد أن تكون الشيفرة المولَّدة تعمل في الإنتاج:
CASCADE كأداة تنظيف ثم حذف بيانات أكثر مما قصدت.عن الأداء: PostgreSQL ينشئ فهرسًا تلقائيًا لـ UNIQUE، لكن المفاتيح الأجنبية لا تفهرس عمود الإشارة تلقائيًا. بدون هذا الفهرس، قد تصبح التحديثات والحذف على الأصل بطيئة لأن Postgres يجب أن يمسح جدول الأطفال للتحقق من المراجع.
قبل تشديد قاعدة، جد الصفوف الحالية التي ستفشلها، قرر إصلاحها أم حجرها صحيًا، وانفذ التغيير على خطوات.
قبل أن تنشر، خذ خمس دقائق لكل جدول واكتب ما يجب أن يكون صحيحًا دائمًا. إذا استطعت قوله بالإنجليزية البسيط، فعادةً ما يمكنك تطبيقه بقيد.
اطرح هذه الأسئلة لكل جدول:
إذا كنت تستخدم أداة بناء مدفوعة بالدردشة، عامل تلك الثوابت كمعايير قبول للبيانات، لا كملاحظات اختيارية. مثال: "مبلغ الصفقة يجب أن يكون غير سالب"، "بريد جهة الاتصال فريد لكل مساحة عمل"، "مهمة يجب أن تشير إلى جهة اتصال حقيقية". كلما كانت القواعد أكثر وضوحًا، قل مجال الحالات العرضية.
Koder.ai (koder.ai) يتضمن ميزات مثل وضع التخطيط، اللقطات والتراجع، وتصدير الشيفرة المصدرية، والتي قد تجعل تكرار تغييرات المخطط أسهل أثناء تشديد القيود تدريجيًا.
نمط نشر بسيط يعمل في فرق حقيقية: اختر جدولًا ذا قيمة عالية (المستخدمون، الطلبات، الفواتير، جهات الاتصال)، أضف 1-2 قيد يمنعان أسوأ الفشلات (غالبًا NOT NULL و UNIQUE)، أصلح عمليات الكتابة التي تفشل، ثم كرر. تشديد القواعد على مراحل يفوق ترحيلًا كبيرًا ومحفوفًا بالمخاطر.