كتابة ترحيلات قاعدة بيانات لا تتعطل عند تشغيلها مرتين

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

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

ما الذي يجعل الترحيل آمنًا

المبدأ الأساسي هو التسامح مع التكرار (Idempotency). العملية تكون متسامحة مع التكرار عندما ينتج عن تشغيلها مرة واحدة أو مائة مرة نفس الحالة النهائية. هذا لا يتعلق بمنع تشغيل السكريبت مرتين، بل يتعلق بضمان أن تشغيله مرتين لا يسبب ضررًا.

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

النمط البسيط: تحقق قبل أن تفعل

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

إليك مثال ملموس. افترض أنك بحاجة إلى إضافة عمود last_login_at لتتبع نشاط المستخدم:

-- غير متسامح مع التكرار: يفشل إذا كان العمود موجودًا بالفعل
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;

-- متسامح مع التكرار: ينجح سواء كان العمود موجودًا أم لا
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMP;

النسخة الأولى ستلقي خطأ إذا كان العمود موجودًا بالفعل. النسخة الثانية تعمل بأمان في كل مرة.

بدلاً من كتابة:

ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);

اكتب:

ALTER TABLE users ADD COLUMN IF NOT EXISTS phone_number VARCHAR(20);

جملة IF NOT EXISTS تعني أن السكريبت ينجح سواء كان العمود موجودًا بالفعل أم لا. نفس النهج ينطبق على الفهارس (indexes)، القيود (constraints)، والجداول الجديدة. PostgreSQL و MySQL ومعظم قواعد البيانات الحديثة تدعم هذه الإنشاءات الشرطية.

لإزالة الأشياء، ينطبق نفس المنطق. حذف عمود غير موجود سيفشل. استخدم IF EXISTS لجعل العملية آمنة:

ALTER TABLE users DROP COLUMN IF EXISTS old_phone_number;

التعامل مع ترحيلات البيانات

إضافة وإزالة الأعمدة هي الحالات السهلة. التعقيد الحقيقي يأتي عندما تحتاج إلى نقل أو تحويل بيانات موجودة. افترض أنك تقوم بتقسيم عمود full_name إلى first_name و last_name. الترحيل يحتاج إلى:

  1. إضافة العمودين الجديدين
  2. تعبئتهما من البيانات الموجودة
  3. التعامل مع حالة تشغيل السكريبت مرة أخرى

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

-- إضافة الأعمدة إذا لم تكن موجودة
ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100);

-- التعبئة فقط إذا كانت الأعمدة الهدف فارغة
UPDATE users 
SET first_name = SPLIT_PART(full_name, ' ', 1),
    last_name = SPLIT_PART(full_name, ' ', 2)
WHERE first_name IS NULL AND last_name IS NULL;

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

للسيناريوهات الأكثر تعقيدًا، قد تحتاج إلى مقارنة بيانات المصدر والهدف، أو استخدام checksum للتحقق من أن التحويل أنتج النتيجة الصحيحة. المبدأ يظل كما هو: لا تفترض أبدًا أن البيانات في حالتها الأصلية.

الحذف التدريجي يقلل المخاطر

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

  1. الترحيل الأول: إعادة تسمية العمود إلى column_name_deprecated
  2. الانتظار لبضع دورات إصدار للتأكد من عدم كسر أي شيء
  3. الترحيل الثاني: حذف العمود المهمل

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

احتفظ بسجل لما تم تشغيله

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

معظم أطر عمل الترحيل مثل Flyway أو Liquibase تتعامل مع هذا تلقائيًا. إنها تنشئ جدولًا يتتبع كل سكريبت ترحيل بالاسم و checksum وطابع زمني للتنفيذ. إذا كنت تكتب سكريبتات SQL خام بدون إطار عمل، فأنشئ جدول التتبع الخاص بك:

CREATE TABLE IF NOT EXISTS migration_log (
    script_name VARCHAR(255) PRIMARY KEY,
    started_at TIMESTAMP,
    completed_at TIMESTAMP,
    status VARCHAR(20),
    script_hash VARCHAR(64)
);

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

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

قائمة تحقق عملية لكتابة سكريبتات الترحيل

قبل تشغيل أي ترحيل في الإنتاج، تحقق من هذه النقاط:

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

الخلاصة

سكريبت الترحيل الذي يفشل عند تشغيله مرتين ليس سكريبت ترحيل. إنه قنبلة موقوتة تنتظر شخصًا ما لإعادة تشغيل pipeline. اكتب كل ترحيل كما لو أنه سيتم تنفيذه عدة مرات، لأنه عمليًا، من المحتمل أن يحدث ذلك. تحقق قبل الإنشاء، وتحقق قبل التحويل، وسجل كل شيء. ذاتك المستقبلية، التي تتصحيح نشرًا في الساعة 2 صباحًا، ستشكرك.