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

ترحيل قواعد بيانات SQLite باستخدام user_version

كيف تطوّر مخطط قاعدة بيانات SQLite بأمان عبر الزمن باستخدام PRAGMA user_version، وسكربتات ترحيل مرتّبة، ومعاملات قابلة للتراجع.

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

المخططات تتغيّر، فاستعدّ لذلك

النسخة الأولى من مخطط قاعدة بياناتك لن تكون الأخيرة أبداً. ستضاف أعمدة جديدة، وستُقسَّم جداول، وستُعاد صياغة الفهارس من الصفر. السؤال ليس هل سيتغيّر مخططك، بل هل سيصل التغيير سليماً إلى كل جهاز محمول وخادم وجهاز مستخدم يحمل بالفعل نسخة أقدم من قاعدة البيانات.

هنا يأتي دور ترحيل قاعدة بيانات SQLite: سلسلة من السكربتات الصغيرة المرتّبة التي تنقل قاعدة البيانات من الإصدار N إلى الإصدار N+1. شغّلها بالترتيب، فتلحق أي قاعدة بيانات بالنسخة الحالية. أهمل هذا الانضباط، وستجد نفسك أمام أخطاء من نوع "تعمل عندي فقط" تأكل ساعات من يومك دون فائدة.

لا يقدّم لك SQLite سوى أداة مدمجة واحدة لهذا الغرض: PRAGMA user_version. إنه عدد صحيح بحجم 32 بت تحتفظ به قاعدة البيانات نيابةً عنك، ولا يلمسه SQLite بنفسه. أنت من يقرّر ماذا يعني هذا الرقم.

تبدأ أي قاعدة بيانات جديدة بالقيمة 0. اضبطها على رقم آخر ترحيل قمت بتطبيقه، واقرأها عند بدء تشغيل التطبيق لتعرف عند أي نقطة أنت واقف.

حلقة ترحيل بسيطة لقاعدة بيانات SQLite

الفكرة باختصار: كل عملية ترحيل عبارة عن سكربت SQL مُرقَّم. يقرأ تطبيقك قيمة user_version الحالية، ثم ينفّذ كل السكربتات ذات الأرقام الأعلى بالترتيب، ويحدّث user_version بعد كل واحد منها.

هذا هو الترحيل رقم 1، وفيه ننشئ مخطط قاعدة البيانات الأولي:

هناك أمران يستحقان الانتباه. الكتلة كاملة ملفوفة بـ BEGIN; ... COMMIT; لتكون عملية ذرّية — فلو فشل CREATE TABLE، لن تتغيّر قيمة user_version، ويمكنك تصحيح المشكلة وإعادة المحاولة. كما أنّ PRAGMA user_version = 1 هي آخر تعليمة قبل الـ commit، فلا يتبدّل رقم الإصدار إلا إذا نجح كل ما قبله.

لنفترض الآن أنك تريد إضافة عمود created_at. هذا هو الترحيل رقم 2:

قاعدة البيانات عند الإصدار 0 تُنفِّذ السكربتين معًا. وعند الإصدار 1 تُنفِّذ السكربت الثاني فقط. أما عند الإصدار 2 فلا تُنفِّذ شيئًا. الترتيب هنا هو العقد الذي لا يُكسر.

ماذا يستطيع ALTER TABLE فعله وما لا يستطيع؟

أمر ALTER TABLE في SQLite محدود عمدًا، وهو يدعم العمليات التالية فقط:

  • ADD COLUMN — لإضافة عمود جديد مع قيمة افتراضية اختيارية.
  • DROP COLUMN — لحذف عمود (متاح منذ الإصدار 3.35).
  • RENAME COLUMN — لإعادة تسمية عمود (متاح منذ الإصدار 3.25).
  • RENAME TO — لإعادة تسمية الجدول نفسه.

هذا كل شيء. لا يمكنك تغيير نوع عمود، ولا تعديل قيد NOT NULL، ولا تغيير قيد CHECK، ولا إضافة FOREIGN KEY إلى عمود موجود مسبقًا.

-- غير مدعوم:
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(255);
ALTER TABLE users ADD CONSTRAINT users_email_check CHECK (email LIKE '%@%');

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

إعادة بناء الجدول للتعديلات الكبيرة

الفكرة بسيطة: تُنشئ جدولاً جديداً بالشكل الذي تريده، ثم تنسخ البيانات إليه، وتحذف الجدول القديم، وأخيراً تُعيد تسمية الجديد ليأخذ مكانه. كل ذلك يجري داخل معاملة (transaction) واحدة.

في توثيق SQLite الرسمي يُطلق على هذه العملية اسم "وصفة الخطوات الـ12"، وتُضاف إليها بعض التحذيرات الإضافية المتعلقة بالـ triggers والـ views ومراجع المفاتيح الأجنبية، ويستحق الأمر قراءتها مرة واحدة قبل تطبيقها على مخطط قاعدة بيانات في بيئة الإنتاج. أما في معظم الحالات فإن النسخة المختصرة ذات الأربع خطوات أعلاه تفي بالغرض.

تنبيه مهم: إذا كانت لديك مفاتيح أجنبية تشير إلى الجدول الذي تعيد بناءه، فشغّل PRAGMA foreign_keys = OFF قبل عملية الترحيل ثم PRAGMA foreign_keys = ON بعدها. وإلا فقد يكسر أمر DROP TABLE السلامة المرجعية في منتصف العملية.

إدارة ترحيل قاعدة بيانات SQLite من داخل تطبيقك

العمليات المطلوبة بسيطة بما يكفي لتكتبها بنفسك. إليك مثالاً بلغة Python باستخدام المكتبة القياسية فقط:

القواعد الأساسية التي يجب الالتزام بها:

  • ترقيم الترحيلات يكون متسلسلاً ومتتابعاً بدءاً من 1، دون فجوات ودون إعادة ترتيب.
  • كل عملية ترحيل تُغلَّف داخل معاملة (transaction) مع رفع قيمة PRAGMA user_version = N.
  • بمجرد تثبيت ترحيل ونشره، لا تعدّله أبداً. أي تغيير جديد يذهب في ترحيل جديد.

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

تسجيل سجل تدقيق للترحيلات

قيمة user_version تخبرك أين وصلت قاعدة البيانات، لكنها لا تخبرك متى نُفِّذت كل خطوة ولا ماذا فعلت. جدول صغير للتوثيق يحلّ هذه المشكلة:

الآن لديك صف لكل عملية ترحيل يحمل اسمًا وطابعًا زمنيًا — وهذا مفيد جدًا عند تصحيح الأخطاء وتساؤلك: "لماذا تحتوي قاعدة البيانات على عمود لا يتوقعه الكود؟"

يبقى PRAGMA user_version هو المرجع الأساسي للحلقة؛ أما الجدول فهو للبشر.

التراجع عن الترحيل: ما الذي توفره المعاملات وما الذي لا توفره

أوامر DDL في SQLite قابلة للتنفيذ ضمن معاملة (transactional). إذا بدأ الترحيل رقم 5 بإنشاء جدول ونسخ بيانات إليه ورفع قيمة user_version، ثم فشلت عملية النسخ في منتصف الطريق، فإن ROLLBACK يتراجع عن كل شيء — بما في ذلك أمر CREATE TABLE نفسه. تعود قاعدة البيانات إلى حالتها تمامًا كما كانت قبل BEGIN.

ما سبق يغطي حالة الترحيلات التي تفشل أثناء التنفيذ، لكنه لا يغطي الترحيلات التي نُفِّذت بنجاح ثم ندمت عليها لاحقًا. لهذه الحالة، تكتب ما يُسمى بـ ترحيل عكسي (down-migration) — أي سكربت منفصل يتراجع عن التغيير. SQLite لا يوفّر أي تراجع تلقائي. فإذا كان الترحيل رقم 7 قد أضاف عمودًا، فالنسخة العكسية تحذفه. أما إذا كان الترحيل رقم 7 قد حذف عمودًا، فلا يمكن للنسخة العكسية أن تستعيد البيانات؛ أقصى ما تستطيعه هو إعادة إنشاء العمود فارغًا.

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

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

  • ترحيل واحد لكل تغيير منطقي. الترحيل الذي يضيف ثلاثة أعمدة لا علاقة بينها أصعب في المراجعة وأصعب في التراجع من ثلاثة ترحيلات منفصلة.
  • اختبر الترحيلات على نسخة من قاعدة الإنتاج. تعديلات المخطط قد تكون بطيئة على الجداول الكبيرة، واكتشاف ذلك في الإنتاج تجربة غير ممتعة أبدًا.
  • لا تعدّل أبدًا على ترحيل سبق نشره. أضف ترحيلًا جديدًا بدلًا من ذلك.
  • خذ نسخة احتياطية أولًا. أمر .backup السريع في سطر الأوامر، أو نسخ الملف عندما تكون قاعدة البيانات مغلقة، تأمين رخيص قبل أي ترحيل غير تافه.
  • انتبه إلى PRAGMA foreign_keys. أوقفه أثناء إعادة بناء الجداول، ثم أعِد تشغيله بعد الانتهاء.

في المشاريع الأكبر، استعِن بأداة مخصصة — مثل Alembic مع SQLAlchemy، أو golang-migrate، أو Knex، أو Flyway. هذه الأدوات تتكفّل بترتيب الترحيلات، والتعامل مع تشغيلها من أكثر من جهة في نفس الوقت، وأعراف العمل الجماعي التي ستضطر لإعادة اختراعها يدويًا. المبادئ هي نفسها التي رأيناها في الحلقة بالأعلى؛ الأداة فقط تخلّصك من الكود المتكرر.

التالي: وضع WAL والتزامن

الترحيلات عادةً تُنفَّذ والتطبيق متوقف، أو وهي ممسكة بقفل حصري على القاعدة. أما بقية الوقت، فقاعدة بياناتك تخدم قراءات وكتابات من اتصالات متعددة في وقت واحد — ووضع journal الافتراضي في SQLite ليس دائمًا الخيار الأنسب لذلك. الصفحة التالية تشرح وضع WAL، وما الذي يتغيّر عند تفعيله، ومتى يجدر بك التحوّل إليه.

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

كيف أضع رقم إصدار لمخطط قاعدة بيانات SQLite؟

تحتفظ SQLite بخانة عدد صحيح 32-bit مدمجة لكل قاعدة بيانات اسمها user_version، وتقرأها وتكتبها عبر PRAGMA user_version. عند بدء التشغيل اقرأ القيمة، قارنها برقم آخر ترحيل يعرفه كودك، ثم نفّذ الترحيلات الناقصة بالترتيب. لا تحتاج إلى جدول إضافي، رغم أن كثيرًا من التطبيقات تضيف جدولًا خاصًا لأغراض التتبّع والتدقيق.

هل يمكن التراجع عن ترحيل في SQLite؟

لفّ كل ترحيل داخل BEGIN; ... COMMIT;. إذا فشل أي شيء في المنتصف، يقوم ROLLBACK بإلغاء الخطوة بأكملها — تغييرات المخطط وتغييرات البيانات معًا، لأن أوامر DDL في SQLite تعمل ضمن المعاملات. أما التراجع عن ترحيل سبق تنفيذه وحفظه فيتطلّب سكربت تراجع (down-script) تكتبه أنت بنفسك؛ SQLite لن تولّده تلقائيًا.

لماذا أوامر ALTER TABLE محدودة في SQLite؟

تدعم SQLite فقط ALTER TABLE ADD COLUMN و RENAME TABLE و RENAME COLUMN و DROP COLUMN، ولا تدعم تغييرات اعتباطية مثل تعديل نوع عمود أو قيوده. الحل المعتمد هو وصفة الـ 12 خطوة: أنشئ جدولًا جديدًا بالشكل المطلوب، ثم INSERT INTO new_table SELECT ... FROM old_table، ثم احذف الجدول القديم، وأعِد تسمية الجديد ليأخذ مكانه.

هل أستخدم أداة ترحيل جاهزة أم أكتب الحل بنفسي؟

للتطبيقات الصغيرة، حلقة بسيطة على ملفات .sql مرقّمة مدفوعة بـ PRAGMA user_version لا تتجاوز 30 سطرًا من الكود وتؤدي الغرض تمامًا. أما للمشاريع الكبيرة، فأدوات مثل Alembic (في بايثون) أو golang-migrate (في Go) أو Knex (في Node) تتكفّل بالترتيب والقفل وسير العمل الجماعي، وهي أمور ستضطر لإعادة اختراعها لو كتبتها يدويًا.

Coddy programming languages illustration

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

ابدأ الآن