Featured image of post لا تنخدع بعفوية SQLite! ما هي فخاخ الأنواع الديناميكية؟ ولماذا ALTER TABLE غير مكتمل؟ وكيف تبني بنية برمجية دفاعية في Node.js لترقية مخطط الجدول بدون ألم؟

لا تنخدع بعفوية SQLite! ما هي فخاخ الأنواع الديناميكية؟ ولماذا ALTER TABLE غير مكتمل؟ وكيف تبني بنية برمجية دفاعية في Node.js لترقية مخطط الجدول بدون ألم؟

تعتمد SQLite نظام أنواع ديناميكي وضعيف؛ حيث إن إدخال سلسلة نصية في عمود INTEGER لن يؤدي للمفاجأة إلى حدوث أي خطأ. تعرف على فخاخ أفيونيتي النوع (Type Affinity) في SQLite، وتأثير غياب أنواع Boolean و Date الأصلية، وقيود ALTER TABLE إلى جانب استراتيجية الترقية الآمنة 'أربع خطوات لإعادة الإنشاء والنقل'، وكيفية بناء هندسة برمجية دفاعية باستخدام أدوات مثل TypeScript و Zod و Prisma.

تخيل سلة إعادة تدوير وضعت عليها بوضوح ملصقًا يقول "للزجاجات البلاستيكية فقط"، ولكن عندما يلقي شخص ما قطعة ورق داخلها، فإنها تقبلها بصمت دون كلمة احتجاج واحدة؟

هذه هي التجربة المثيرة للقشعريرة التي يمر بها المطورون عندما يقابلون نظام أنواع SQLite لأول مرة.

إذا كنت معتادًا على أسلوب موظف الجمارك الصارم في PostgreSQL (حيث يتم رفض الأنواع غير الصحيحة مباشرة من الدخول)، فإن عفوية وسهولة SQLite قد تجعلك تشك في حياتك.

والأكثر رعبًا من ذلك، عندما تريد تعديل بنية جدول، ستخبرك قاعدة البيانات بالتالي:

"لا يمكن تعديل الجدول مباشرة. يرجى بناء منزل جديد، ونقل الأثاث إليه، ثم تفجير المنزل القديم."

تحت الغطاء، تمتلك SQLite 5 فئات تخزين فقط

بغض النظر عن أسماء الأنواع الفخمة التي تعلن عنها في CREATE TABLE مثل (VARCHAR(255)، BIGINT، DECIMAL)، فإن SQLite تحت الغطاء لا تتعرف إلا على فئات التخزين الخمس التالية:

فئة التخزين الوصف
NULL قيمة فارغة
INTEGER عدد صحيح (يشغل تلقائيًا من 1 إلى 8 بايت اعتمادًا على حجم القيمة)
REAL رقم عشري عائم (ثابت 8 بايت)
TEXT سلسلة نصية (ترميز UTF-8 افتراضي)
BLOB كائن ثنائي كبير (يتم تخزينه تمامًا كما تم إدخاله)

الأنواع التي تحددها في الأعمدة هي مجرد "توصية" لـ SQLite وليست "قواعد إلزامية".

يطلق على هذا اسم "Type Affinity" (أفيونيتي النوع). ستحاول SQLite تحويل بياناتك إلى النوع الموصى به، ولكن إذا لم تتمكن من ذلك، فستقوم بإدخال البيانات الأصلية على أي حال دون إظهار أي خطأ.

يمكنك الإعلان عن عمود age INTEGER في SQLite ثم إدخال السلسلة النصية 'ثمانية عشر عامًا للأبد'؛ وسوف تقبله بكل سرور.

ثلاثة فخاخ أنواع هي الأسهل في الوقوع بها

الفخ 1: غياب نوع Boolean الأصلي

لا تحتوي SQLite على نوع boolean أصلي. يمكن تمثيل قيمتي True و False بالأعداد الصحيحة 1 و 0 فقط.

عندما تسترجع البيانات من SQLite باستخدام Node.js، ستحصل على الرقم 1 أو 0 وليس true أو false.

إذا قمت مباشرة بإجراء فحص مثل if (user.is_admin === true)، فلن يكون صحيحًا أبدًا.

الفخ 2: غياب نوع Date/Time

لا تحتوي SQLite على نوع للتاريخ/الوقت. لا يمكنك تخزين الوقت إلا بالطرق التالية:

طريقة التخزين مثال الميزات والعيوب
TEXT (سلسلة ISO-8601) '2026-05-19T18:00:00Z' الأكثر توصية، وضوح عالٍ، وتحويل سلس عند الانتقال إلى PostgreSQL مستقبلاً
INTEGER (طابع Unix الزمني) 1747656000 يشغل مساحة صغيرة، ولكنه غير مقروء للبشر

لا تخزن التواريخ مطلقًا بتنسيقات مخصصة عشوائية مثل 19/5/2026 6:00 مساءً، وإلا فسيكون ترحيل البيانات مستقبلاً كارثة حقيقية.

الفخ 3: إدخال سلسلة نصية في عمود INTEGER لن يؤدي لحدوث خطأ

في PostgreSQL يؤدي إدخال سلسلة نصية في عمود INTEGER إلى حدوث خطأ على الفور. ولكن SQLite ستحاول التحويل بصمت فقط، وعند الفشل، ستقبل البيانات كما هي.

هذا يعني أن البيانات الملوثة قد تتسلل بصمت إلى قاعدة بياناتك حتى يأتي يوم يتوقف فيه برنامجك فجأة بسبب تلقي نوع غير متوقع، وعندها فقط تكتشف المشكلة.

البرمجة الدفاعية: مواجهة قاعدة بيانات عفوية بموقف صارم

في مواجهة عفوية وسهولة SQLite، يجب عليك بناء آليات دفاع صارمة في تطوير تطبيقات Node.js:

مستوى الدفاع الأداة الدور
حارس وقت الترجمة TypeScript التقاط الأنواع غير الصحيحة في مرحلة كتابة الكود
التحقق من مدخلات API Zod التحقق من صحة البيانات الواردة بصرامة (ضمان أن يكون age رقمًا دائمًا)
تحويل الأنواع الداخلي Prisma / Drizzle ORM معالجة فروق الأنواع تلقائيًا بين SQLite و PostgreSQL

إن نقل "حارس بوابة التحقق من البيانات" من مستوى قاعدة البيانات إلى مستوى تطبيق Node.js هو استراتيجية رئيسية للاستفادة من سرعة تطوير SQLite مع ضمان قابلية التوسع مستقبلاً.

عند استخدام ORM، طالما أنك تعلن عن type: 'boolean' في كودك، فإن الـ ORM يقوم تلقائيًا بتحويله إلى 1/0 عند الحفظ في SQLite ويحوله مجددًا إلى true/false عند القراءة، مما يخفي فروق الأنواع الأساسية تمامًا.

ALTER TABLE غير مكتمل: ما يمكن وما لا يمكن تعديله

دعم SQLite لتعديل بنية الجداول محدود للغاية:

العملية مدعومة
إضافة عمود (ADD COLUMN) نعم
إعادة تسمية عمود (RENAME COLUMN) نعم
حذف عمود (DROP COLUMN) نعم (في الإصدارات الحديثة)
إعادة تسمية جدول (RENAME TO) نعم
تغيير نوع العمود لا
إضافة/إزالة قيود UNIQUE و NOT NULL لا
تعديل المفتاح الأساسي لا
تعديل المفتاح الأجنبي لا

بمجرد أن تحتاج إلى إجراء أي من التعديلات "غير المدعومة"، تتطلب منك SQLite تنفيذ استراتيجية "إعادة الإنشاء والنقل".

أربع خطوات لإعادة الإنشاء والنقل: طريقة ترقية جداول SQLite

نظرًا لعدم إمكانية التعديل المباشر، فإن الطريقة القياسية الموصى بها في الوثائق الرسمية هي بناء منزل جديد، ونقل الأثاث، وتفجير المنزل القديم، وتعليق اللوحة الجديدة:

الخطوة الإجراء الوصف
1 إنشاء جدول جديد إنشاء CREATE TABLE users_new (...) بالبنية الصحيحة
2 نسخ البيانات INSERT INTO users_new SELECT ... FROM users
3 حذف الجدول القديم DROP TABLE users
4 إعادة تسمية الجدول ALTER TABLE users_new RENAME TO users

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

ضمان عدم فقدان البيانات أثناء الترقية: خطان للدفاع عن الأمان

خط الدفاع 1: الدفاع المادي، نسخ الملف مباشرة

تعتبر SQLite في جوهرها مجرد ملف واحد. قبل تنفيذ أي تغييرات في المخطط، يكفي ببساطة نسخ ملف .db ليكون نسخة احتياطية.

const fs = require('fs');
fs.copyFileSync('my_project.db', 'my_project_backup.db');

إذا سارت الأمور بشكل سيء، فإن استبدال الملف يستعيد كل شيء في لمحة بصر. هذه ميزة لا توفرها قواعد البيانات الكبيرة الأخرى.

خط الدفاع 2: تغليف المعاملة (Transaction), آلة الزمن لقاعدة البيانات

قم بتغليف جميع خطوات الترحيل داخل معاملة Transaction واحدة؛ وإذا فشلت أي خطوة، فسيتم التراجع تلقائيًا (Rollback) عن العملية بأكملها وكأن شيئًا لم يكن.

const Database = require('better-sqlite3');
const db = new Database('my_project.db');

const migrateData = db.transaction(() => {
  db.prepare(`
    CREATE TABLE users_new (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      age INTEGER NOT NULL DEFAULT 18
    )
  `).run();

  db.prepare(`
    INSERT INTO users_new (id, name, age)
    SELECT id, name, COALESCE(age, 18) FROM users
  `).run();

  db.prepare('DROP TABLE users').run();
  db.prepare('ALTER TABLE users_new RENAME TO users').run();
});

try {
  migrateData();
  console.log('اكتملت ترقية الجدول');
} catch (error) {
  console.error('فشلت الترقية، تم استعادة البيانات بأمان:', error.message);
}

"ملف النسخ الاحتياطي + ربط المعاملة" هو الوسادة الهوائية لترحيل قاعدة بياناتك.

التحكم في عفوية SQLite بموقف صارم

تحكم في عفوية وسهولة SQLite باستخدام بنية صارمة لطبقة التطبيق للاستمتاع بميزة سرعة التطوير الفائقة مع تجنب الديون التقنية مستقبلاً.

يعتبر نظام الأنواع في SQLite مرنًا وعفويًا للغاية، كما أن ALTER TABLE لديه العديد من القيود.

ولكن طالما أنك تقوم بتطبيق فحص أنواع TypeScript + تحقق Zod + تجريد الـ ORM في جهة Node.js بجدية، وتدمج ذلك مع استراتيجية الأمان المتمثلة في النسخ الاحتياطي المادي + المعاملات الصارمة، فيمكنك الاستمتاع بكفاءة التطوير الرائعة التي توفرها SQLite مع تمهيد الطريق للانتقال السلس إلى PostgreSQL مستقبلاً.

Reference

All rights reserved,未經允許不得隨意轉載
مبني بستخدام Hugo
قالب Stack مصمم من Jimmy