وداعاً للتوقف الليلي: كيف أنقذتنا هجرات قواعد البيانات المتوافقة من جحيم الصيانة؟

يا جماعة الخير، بالصلاة عالنبي. خلوني أحكيلكم قصة صارت معي زمان، أيام ما كان الشعر الأسود أكثر من الأبيض براسي. كنا فريق صغير، شغالين على مشروع ناشئ، وكان الحماس “واصل للسما”. في ليلة من الليالي، حوالي الساعة 2 بعد منتصف الليل، كنا مجتمعين بالمكتب، وريحة القهوة المغلية معبّية الجو. ليش سهرانين؟ لأنه كان لازم نعمل تحديث “بسيط” على قاعدة البيانات: نضيف حقل جديد عشان ميزة طلبها قسم التسويق “لأهميتها القصوى”.

قلنا بسيطة، “شغل ربع ساعة بالكثير”. أعلنا للمستخدمين عن صيانة مجدولة، وقفنا السيرفرات، وشغّلنا سكربت الهجرة (Migration). وهنا بلشت المصايب. السكربت علّق بنص الطريق بسبب حجم البيانات الكبير. حاولنا نرجع بالـ Rollback، بس الأمور تعقدت أكثر. قضينا الليلة كلها، وإحنا بنحاول نصلح اللي خرب، والتلفونات من المدير ما وقفت. طلع علينا الصبح وإحنا يادوب مصلحين الوضع، والخدمة رجعت بعد توقف 8 ساعات بدل ربع ساعة. يومها حلفت إنه لازم نلاقي طريقة أفضل، طريقة ما تخلّي قلوبنا بإيدينا مع كل عملية نشر. وهيك كانت بداية رحلتي مع ما يسمى بـ “الهجرات المتوافقة مع الإصدارات السابقة”.

ما هي “هجرة قواعد البيانات” (Schema Migration) أصلاً؟

قبل ما نغوص بالتفاصيل، خلينا نوحّد المصطلحات. تخيل قاعدة بياناتك زي المبنى اللي بتشتغل فيه. مع الوقت، بتحتاج تضيف غرفة جديدة، أو توسّع مكتب، أو حتى تهد حيط. “هجرة قواعد البيانات” أو الـ Schema Migration هي ببساطة مجموعة التعليمات المنظمة والمُدارة اللي بتنفذ هاي التغييرات على هيكل قاعدة بياناتك (الـ Schema).

بدل ما يفوت المبرمج ويكتب أوامر SQL مباشرة على قاعدة البيانات الإنتاجية (وهي وصفة للكوارث)، بنكتب هاي التغييرات بملفات بنسميها “ملفات الهجرة”. كل ملف بكون اله رقم إصدار، وبحتوي على قسمين:

  • Up: التعليمات اللي بتنفذ التغيير المطلوب (مثلاً، إضافة جدول جديد).
  • Down: التعليمات اللي بتلغي التغيير اللي عمله الـ Up (مثلاً، حذف الجدول اللي أضفته)، عشان لو صار مشكلة نقدر نتراجع.

هذا الأسلوب بشبه الـ Version Control (زي Git) بس لقاعدة بياناتك. بخلي كل التغييرات موثقة، قابلة للتتبع، وقابلة للتراجع.

الطريقة التقليدية (واللي كانت مغلّبتنا): جحيم التوقف المجدول

القصّة اللي حكيتها بالبداية هي مثال حي على الطريقة التقليدية اللي معظم الفرق (للأسف) بتبدأ فيها. السيناريو الكلاسيكي للتحديث كان يمشي بالخطوات التالية:

  1. إعلان الصيانة: نبعت إيميلات ونحط بنرات للمستخدمين “عذراً، الموقع تحت الصيانة من الساعة 2:00 صباحاً حتى 2:30”.
  2. إيقاف التطبيق: نحول الموقع لوضع الصيانة (Maintenance Mode) عشان ما حدا يقدر يستخدمه.
  3. تشغيل الهجرة: نشغّل سكربت الهجرة على قاعدة البيانات.
  4. نشر الكود الجديد: نرفع الكود الجديد اللي بفهم التغيير اللي صار على قاعدة البيانات.
  5. إعادة التشغيل: نرجع نشغّل السيرفرات والتطبيق.
  6. الدعاء والصلاة: أهم خطوة، إنه كل شي يشتغل تمام وما تطلع مصيبة جديدة.

ليش هاي الطريقة سيئة؟

  • التوقف عن الخدمة (Downtime): أسوأ شي ممكن تقدمه للمستخدم هو خدمة متوقفة. بتخسر ثقة، وممكن تخسر فلوس مباشرة.
  • مخاطرة عالية: أنت بتعمل تغيير كبير وخطير دفعة واحدة (Big Bang). لو فشلت أي خطوة، عملية الإصلاح بتكون معقدة ومرهقة.
  • ضغط نفسي على الفريق: الشغل تحت ضغط الوقت بالليل المتأخر هو وصفة للأخطاء البشرية.

الحل السحري: الهجرات المتوافقة مع الإصدارات السابقة (Backwards-Compatible Migrations)

هنا يكمن “السر” اللي غير طريقة عملنا تماماً. الفكرة الأساسية بسيطة لكن عبقرية: “يجب أن تجري تغييرات على قاعدة البيانات بطريقة لا تكسر الإصدار الحالي (القديم) من الكود الذي يعمل على الخوادم الآن”.

بمعنى آخر، بدل ما نوقف كل شي، بنخلي عملية التغيير تتم على مراحل، وفي كل مرحلة، بتكون قاعدة البيانات متوافقة مع الإصدار القديم والجديد من الكود بنفس الوقت (أو على الأقل لا تتعارض مع القديم). هذا المبدأ يفتح الباب أمام ما يسمى بـ “النشر بدون توقف” (Zero-Downtime Deployment).

استراتيجيات عملية لتطبيق الهجرات المتوافقة (شغل مرتب)

الكلام النظري حلو، بس كيف بنطبق هالحكي على أرض الواقع؟ خلينا ناخذ أشهر الحالات العملية ونشوف كيف نتعامل معها.

الحالة الأولى: إضافة حقل جديد (الأسهل)

هاي أبسط حالة. لنفرض بدنا نضيف حقل last_login_ip لجدول المستخدمين users.

الطريقة الخطأ (تسبب مشاكل):


-- هذا الأمر سيفشل على قواعد البيانات التي لا تسمح بذلك افتراضياً
-- لأن الحقل الجديد ليس له قيمة للاسطر الموجودة مسبقاً
ALTER TABLE users ADD COLUMN last_login_ip VARCHAR(45) NOT NULL;

الكود القديم ما بعرف بوجود هذا الحقل، فلو حاول يعمل عملية INSERT، راح تفشل العملية لأنه ما أرسل قيمة للحقل الجديد اللي هو NOT NULL.

الطريقة الصح (المتوافقة):

الحل هو إنك تخلي الحقل الجديد “اختياري” في البداية.


-- الطريقة الأولى: اجعله يقبل القيمة الفارغة (NULL)
ALTER TABLE users ADD COLUMN last_login_ip VARCHAR(45) NULL;

-- الطريقة الثانية: أعطه قيمة افتراضية
ALTER TABLE users ADD COLUMN login_attempts INT DEFAULT 0;

بهذه الطريقة، الكود القديم بضل شغال زي ما هو. لما يعمل INSERT، قاعدة البيانات راح تحط NULL أو القيمة الافتراضية 0 في الحقل الجديد تلقائياً. يا سلام! هيك بنقدر ننفذ الهجرة بأي وقت، وبعدها بننشر الكود الجديد اللي ببدأ يستخدم هذا الحقل.

الحالة الثانية: إعادة تسمية حقل (التحدي الحقيقي)

هاي هي الحالة اللي بتفرّق بين المبتدئ والمحترف. لنفرض عنا حقل اسمه phone في جدول users وبدنا نغير اسمه لـ phone_number لأنه أوضح.

لو عملت RENAME COLUMN مباشرة، الكود القديم اللي لسا شغال على السيرفرات راح “ينكسر” فوراً لأنه ببحث عن حقل اسمه phone بطل موجود.

الحل هو عملية متعددة المراحل (Multi-Phase Deployment)، وهيك القصة بتمشي:

  1. المرحلة 1 (نشر 1: الهجرة):

    نضيف الحقل الجديد phone_number ويكون قابل للقيمة الفارغة (nullable).

    ALTER TABLE users ADD COLUMN phone_number VARCHAR(20) NULL;

    النتيجة: الآن قاعدة البيانات فيها الحقلين: phone و phone_number. الكود القديم لا يزال يعمل 100% ويتعامل مع phone فقط.

  2. المرحلة 2 (نشر 2: تحديث الكود):

    نعدل الكود بحيث يقوم بـ “الكتابة المزدوجة”. أي عملية كتابة أو تحديث لرقم الهاتف يجب أن تحدث في كلا الحقلين. أما القراءة، فتظل من الحقل القديم phone لضمان الاستمرارية.

    // مثال بالكود الزائف (Pseudo-code)
    function updateUser(user, data) {
      user.phone = data.phone;
      user.phone_number = data.phone; // الكتابة المزدوجة
      user.save();
    }
    

    النتيجة: أي بيانات جديدة أو محدثة ستكون متزامنة في الحقلين. الكود القديم والجديد كلاهما يعمل.

  3. المرحلة 3 (سكربت لمرة واحدة):

    نكتب سكربت صغير ونشغله (بعيداً عن عملية النشر) يقوم بنسخ البيانات من الحقل القديم للجديد لكل السجلات القديمة التي لم يتم تحديثها بعد.

    UPDATE users SET phone_number = phone WHERE phone_number IS NULL;

    النتيجة: الآن كل البيانات في phone_number مطابقة تماماً للبيانات في phone.

  4. المرحلة 4 (نشر 3: تحديث الكود):

    نعدل الكود مرة أخرى. الآن، نجعل كل عمليات القراءة تتم من الحقل الجديد phone_number. يمكننا إيقاف الكتابة المزدوجة الآن والكتابة فقط في الحقل الجديد.

    // مثال بالكود الزائف (Pseudo-code)
    function updateUser(user, data) {
      user.phone_number = data.phone; // نكتب فقط في الجديد
      user.save();
    }
    
    function getUserPhoneNumber(user) {
      return user.phone_number; // نقرأ فقط من الجديد
    }
    

    النتيجة: التطبيق الآن يعتمد كلياً على الحقل الجديد phone_number. الحقل القديم phone صار “مهجوراً” ولا أحد يستخدمه.

  5. المرحلة 5 (نشر 4: الهجرة النهائية):

    بعد التأكد من أن كل شيء يعمل بشكل سليم ومرور فترة كافية (مثلاً أسبوع)، يمكننا الآن وبكل أمان إنشاء هجرة جديدة لحذف الحقل القديم.

    ALTER TABLE users DROP COLUMN phone;

    النتيجة: قاعدة البيانات نظيفة، الكود نظيف، والعملية تمت بدون أي توقف للخدمة. شغل مرتب!

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

الحالة الثالثة: حذف حقل أو جدول

حذف أي شيء هو عملية خطيرة. القاعدة هنا بسيطة: “لا تحذف العمود من قاعدة البيانات إلا بعد أن تتأكد 100% أن الكود لم يعد يستخدمه”.

العملية تتم على خطوتين (نشرين مختلفين):

  1. الخطوة 1 (نشر الكود): قم بتحديث الكود الخاص بك وإزالة أي إشارة أو استخدام للعمود الذي تريد حذفه. انشر هذا الكود وانتظر.
  2. الخطوة 2 (نشر الهجرة): بعد أن تتأكد من أن الإصدار الجديد من الكود يعمل بثبات ولا توجد أي أخطاء، قم بإنشاء هجرة جديدة تحتوي على أمر الحذف.

هذا الفصل يضمن أنك لن تحذف عموداً لا يزال الكود القديم (الذي قد يكون لا يزال يعمل على بعض الخوادم أثناء عملية النشر التدريجي) يحاول الوصول إليه.

نصائح من خبرة أبو عمر

  • لا تثق بالهجرات التلقائية بالكامل: بعض أطر العمل (Frameworks) تولّد ملفات الهجرة تلقائياً. هذا رائع، لكن دائماً راجع الملف الناتج وافهم ما يفعله قبل تطبيقه.
  • اختبر هجراتك: قبل تطبيق الهجرة على قاعدة البيانات الإنتاجية، اختبرها على نسخة طبق الأصل من بيانات الإنتاج (Production data clone). هذا سيكشف لك مشاكل الأداء أو الأخطاء غير المتوقعة.
  • اجعل هجراتك صغيرة ومركزة: لا تضع تغييرات متعددة غير مترابطة في ملف هجرة واحد. اجعل كل ملف هجرة يقوم بمهمة واحدة صغيرة وواضحة.
  • التواصل هو المفتاح: إذا كنت تعمل في فريق، يجب أن يكون الجميع على دراية بخطة الهجرة متعددة المراحل. التنسيق بين المطورين (Backend/Frontend) ومديري النظام (DevOps) ضروري جداً.
  • استخدم الأدوات المتاحة: لا تخترع العجلة. استخدم أدوات إدارة الهجرات المعروفة مثل Flyway, Liquibase, أو الأدوات المدمجة في الـ ORM الذي تستخدمه (مثل في Django, Rails, Laravel, TypeORM).

الخلاصة: من السهر إلى راحة البال 🧘

التحول من نهج “التوقف والصيانة” إلى نهج “الهجرات المتوافقة والنشر بدون توقف” هو نقلة نوعية في عقلية الفريق الهندسي. هو استثمار في الاستقرار، وتقليل للمخاطر، واحترام لوقت المستخدم ووقت المطورين على حد سواء.

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

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

أتمتة العمليات

كانت بيئاتنا غير متطابقة: كيف أنقذنا “الكود كبنية تحتية” (IaC) من جحيم “لكنه يعمل على جهازي”؟

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

18 مايو، 2026 قراءة المزيد
​معمارية البرمجيات

المعمارية الموجهة بالأحداث (EDA): طوق النجاة الذي أنقذنا من جحيم الخدمات المتشابكة

كانت خدماتنا متشابكة كخيوط العنكبوت، أي تغيير صغير كان يهدد بانهيار النظام بأكمله. في هذه المقالة، أروي لكم كـ "أبو عمر" كيف أنقذتنا المعمارية الموجهة...

18 مايو، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

كان تطبيقنا حصناً منيعاً: كيف أنقذتنا ‘الوصولية الرقمية’ (a11y) من جحيم استبعاد المستخدمين؟

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

18 مايو، 2026 قراءة المزيد
برمجة وقواعد بيانات

كانت صفحاتنا تُحمّل ببطء قاتل: كيف أنقذنا ‘التحميل المسبق’ (Eager Loading) من جحيم استعلامات N+1؟

أشارككم قصة حقيقية من قلب المعركة البرمجية، كيف اكتشفنا عدوًا خفيًا يسمى "N+1 Query" كان يلتهم أداء تطبيقنا، وكيف كان "التحميل المسبق" (Eager Loading) هو...

18 مايو، 2026 قراءة المزيد
البودكاست