عندما تحتاج قاعدة البيانات إلى التحكم في الإصدارات أيضًا
تخيل هذا السيناريو: فريقك لديه خط أنابيب CI/CD متين لتطبيقك. كل طلب سحب (pull request) يشغل اختبارات آلية، يبني حاوية (container image)، وينشر إلى بيئة الاختبار (staging). ثم يأتي النشر إلى الإنتاج. يعمل خط الأنابيب، يبدأ التطبيق، ثم ينهار فورًا مع خطأ "العمود غير موجود" (column-not-found error). نسي أحدهم تشغيل ترحيل قاعدة البيانات (migration) الذي يضيف عمود phone_number. يفشل النشر، يرى المستخدمون أخطاء، ويحاول الفريق فهم ما حدث.
هذا السيناريو يحدث يوميًا في فرق عديدة. كود التطبيق مُدار بالإصدارات، مُختبر، ومنشور عبر خط أنابيب. لكن تغييرات هيكل قاعدة البيانات تُعامل كشيء ثانوي، شيء يقوم به شخص ما يدويًا قبل أو بعد النشر. الفجوة بين نشر الكود وتغييرات الهيكل تخلق ثغرة تتسرب من خلالها الأخطاء.
المشكلة: كيف يعرف خط الأنابيب ما تم تشغيله؟
عندما يكون لديك دليل من نصوص الترحيل مثل V001_create_users.sql و V002_add_phone.sql و V003_add_index.sql، يحتاج خط الأنابيب إلى معرفة أي منها تم تطبيقه بالفعل على قاعدة البيانات. لا يمكنك تشغيل جميع الملفات من البداية في كل مرة تنشر فيها. قاعدة بيانات الإنتاج تحتوي بالفعل على بيانات حقيقية. تشغيل V001 مرة أخرى سيفشل لأن الجدول موجود بالفعل، أو الأسوأ، سيحذف الجداول ويعيد إنشائها، مما يدمر بيانات العملاء.
بدون آلية تتبع، تلجأ الفرق إلى الفحوصات اليدوية. يقوم شخص ما بتسجيل الدخول إلى قاعدة البيانات، يشغل \dt أو SHOW TABLES، ويحاول تذكر ما تم نشره الأسبوع الماضي. أو يعتمدون على جدول بيانات مشترك لا يقوم أحد بتحديثه. أو ببساطة يشغلون الترحيل ويأملون الأفضل.
لا شيء من هذه الأساليب قابل للتوسع. إنها تُدخل أخطاء بشرية، تُبطئ عمليات النشر، وتخلق خوفًا حول كل تغيير في قاعدة البيانات.
الحل: جدول ترحيل في قاعدة البيانات
الإجابة بسيطة بشكل مدهش: دع قاعدة البيانات تتبع تاريخ ترحيلاتها بنفسها. أنشئ جدولًا خاصًا، يُسمى عادةً schema_migrations أو migration_history، يسجل كل نص ترحيل تم تنفيذه.
إليك كيف يعمل عمليًا:
- في المرة الأولى التي تعمل فيها أداة الترحيل على قاعدة بيانات فارغة، تنشئ جدول الترحيل.
- بعد تنفيذ كل نص ترحيل بنجاح، تُدرج الأداة صفًا يحتوي على اسم النص وطابع زمني للتنفيذ.
- في عمليات النشر اللاحقة، يقرأ خط الأنابيب جدول الترحيل، ويقارنه بقائمة ملفات الترحيل المتاحة، ويشغل فقط النصوص غير المسجلة بعد.
على سبيل المثال، بعد تشغيل V001_create_users.sql، يحتوي جدول الترحيل على صف واحد: V001_create_users.sql. عندما يتضمن النشر التالي V002_add_phone.sql، يتحقق خط الأنابيب من الجدول، ويرى أن V002 مفقودة، ويشغلها. بعد النجاح، يُضيف صفًا جديدًا. تصبح قاعدة البيانات نفسها المصدر الوحيد للحقيقة حول إصدار الهيكل الحالي.
لماذا هذا مهم لخط الأنابيب الخاص بك
هذه الآلية تسمى "قفل الإصدار" (version locking). قاعدة البيانات تحمل السجل الموثوق لحالتها الخاصة. لا حاجة لملف تكوين منفصل، أو متغير بيئة قد يصبح غير متزامن، أو قائمة مراجعة يدوية قد ينساها أحد.
بالنسبة لخط أنابيب CI/CD، هذا أمر بالغ الأهمية. يمكن لخط الأنابيب الآن اتخاذ قرار موضوعي: "بناءً على ما تخبرني به قاعدة البيانات، أحتاج إلى تشغيل ملفات الترحيل الثلاثة هذه." لا تخمين، لا فحوصات يدوية، لا خوف من تشغيل نفس الترحيل مرتين.
الرسم البياني التالي يوضح هذا التدفق:
تطبق أدوات الترحيل المختلفة هذا بشكل مختلف قليلاً. بعضها يسجل المجموع الاختباري (checksum) لكل ملف ترحيل لاكتشاف ما إذا قام شخص ما بتعديل نص تم تشغيله بالفعل. البعض الآخر يستخدم أرقام إصدار متسلسلة بدلاً من أسماء الملفات. بعض الأدوات تخزن تاريخ الترحيل في مخطط (schema) أو قاعدة بيانات منفصلة. لكن المبدأ الأساسي يبقى كما هو: قاعدة البيانات تتبع تاريخها الخاص، ويقرأ خط الأنابيب هذا التاريخ لتحديد الخطوات التالية.
التمهيد: الترحيل الأول
هناك مشكلة "الدجاجة والبيضة" هنا. جدول الترحيل نفسه يحتاج إلى الوجود قبل أن يتم تسجيل أي ترحيل آخر. كيف تنشئه؟
معظم أدوات الترحيل تتعامل مع هذا تلقائيًا. عندما تشغل الأداة على قاعدة بيانات فارغة لأول مرة، تنشئ جدول الترحيل كجزء من عملية التمهيد. بعض الأدوات تسجل هذا الإجراء التمهيدي كأول إدخال في تاريخ الترحيل.
إذا كنت تتبنى نصوص ترحيل لقاعدة بيانات موجودة بالفعل تحتوي على جداول وبيانات، فأنت بحاجة إلى نهج مختلف. هنا يأتي دور "ترحيل الأساس" (baseline migration). بدلاً من محاولة إعادة إنشاء كل تغيير تاريخي، تنشئ نص ترحيل واحد يلتقط الحالة الحالية لهيكل قاعدة البيانات. تضع علامة عليه كأساس، وتسجله أداة الترحيل كمنفذ بالفعل. من تلك النقطة فصاعدًا، تضيف فقط نصوص ترحيل جديدة للتغييرات المستقبلية.
ترحيل الأساس هو حل عملي. يعترف بأنه لا يمكنك إعادة كتابة التاريخ، لكن يمكنك البدء في تتبع التغييرات من اليوم. البديل سيكون إعادة هندسة كل تغيير في الهيكل تم إجراؤه على الإطلاق، وهو أمر غير عملي لمعظم الفرق.
ما لا يحله جدول الترحيل
جدول الترحيل يحل مشكلة واحدة محددة: معرفة أي النصوص تم تشغيلها بالفعل. يعطي خط الأنابيب طريقة موثوقة لتحديد إصدار الهيكل الحالي وتطبيق التغييرات المعلقة.
لكنه لا يحل كل شيء. يعمل جدول الترحيل بشكل جيد مع التغييرات الإضافية مثل إضافة جداول أو أعمدة أو فهارس. هذه التغييرات لا تكسر كود التطبيق الحالي الذي كُتب للهيكل القديم. تبدأ المشاكل عندما تحتاج إلى إزالة أو إعادة تسمية أعمدة، تغيير أنواع البيانات، أو إعادة هيكلة الجداول. هذه التغييرات التدميرية أو التحويلية يمكن أن تكسر التطبيقات العاملة، تسبب توقفًا، أو تفسد البيانات.
جدول الترحيل يخبرك بما تم تشغيله، لكنه لا يخبرك ما إذا كان التطبيق الحالي متوافقًا مع الهيكل الجديد. هذا يتطلب مجموعة مختلفة من الممارسات حول الترحيلات المتوافقة مع الإصدارات السابقة، النشر المرحلي، والتنسيق الدقيق بين نشر الكود وتغييرات الهيكل.
قائمة مراجعة عملية لتتبع الترحيل
- اختر أداة ترحيل تدعم التتبع التلقائي عبر جدول ترحيل.
- تأكد من إنشاء جدول الترحيل أثناء النشر الأول، وليس يدويًا.
- لا تقم أبدًا بتعديل نص ترحيل تم تطبيقه بالفعل على الإنتاج.
- إذا كنت تتبنى الترحيلات لقاعدة بيانات موجودة، أنشئ ترحيل أساس أولاً.
- أدرج تنفيذ الترحيل كخطوة في خط أنابيب النشر الخاص بك، وليس عملية يدوية.
- اختبر الترحيلات في بيئة اختبار تعكس حجم بيانات الإنتاج.
الخلاصة الملموسة
هيكل قاعدة البيانات الخاص بك لا يقل أهمية عن كود التطبيق، ويستحق نفس المستوى من التحكم في الإصدارات والأتمتة. جدول الترحيل يعطي خط الأنابيب الخاص بك طريقة موثوقة مدعومة بقاعدة البيانات لمعرفة ما تم تطبيقه وما زال يحتاج إلى التشغيل. بدونه، أنت تخمن. معه، لديك مصدر واحد للحقيقة يزيل السبب الأكثر شيوعًا لفشل النشر المتعلق بتغييرات قاعدة البيانات. ابدأ في تتبع إصدارات هيكلك اليوم، وسيشكرك مستقبلك أثناء النشر القادم.