تحديثات قاعدة البيانات بدون توقف: كيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من جحيم التوقفات المجدولة؟

يا جماعة الخير، السلام عليكم ورحمة الله.

اسمحولي اليوم أرجع بالذاكرة لكم سنة لورا، لأحد المشاريع اللي كنت شغال عليها. كانت الساعة تقارب الثانية فجراً، وأنا وفريق العمل في حالة استنفار قصوى. المكتب مليان فناجين قهوة فاضية، والعيون حمرا من التعب والتركيز. شو كنا بنعمل؟ كنا بنعمل عملية نشر (Deployment) لتحديث بسيط على قاعدة البيانات.

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

طبعاً، كالعادة، قانون مورفي اشتغل بكامل طاقته. السكربت علّق في نصه، بعض البيانات ما انتقلت صح، والعملاء بلشوا يبعتوا إيميلات ورسائل دعم فني: “الموقع لا يعمل!”. ضغط من مدير المشروع، وتوتر في الفريق، وساعات طويلة ضاعت بس عشان تعديل بسيط. بعد هداك اليوم، قلت لحالي: “خلص، لازم نلاقي طريقة أفضل. مش معقول في 2024 نضل نوقف الخدمة عشان نغير عمود في جدول!”.

وهون بدأت رحلتي مع استراتيجيات النشر بدون توقف، وتحديداً نمط “التوسيع والتعاقد” أو كما يعرف عالمياً بالـ “Expand/Contract Pattern”. هذا النمط غيّر طريقة تفكيرنا كلياً في التعامل مع تحديثات قواعد البيانات، وحوّل ليالي الرعب إلى عمليات نشر هادئة وسلسة بتصير في وضح النهار بدون ما المستخدم يحس بشي. تعالوا أحكيلكم كيف.

ما هو جحيم “التوقف المجدول”؟

قبل ما ندخل في الحل، خلينا نتفق على المشكلة. الطريقة التقليدية لتحديث هيكل قاعدة البيانات (Schema) تتلخص في الآتي:

  1. جدولة نافذة صيانة (Maintenance Window)، وغالباً بتكون في وقت متأخر من الليل.
  2. إيقاف الخدمة عن المستخدمين.
  3. تطبيق التغييرات على قاعدة البيانات (مثلاً ALTER TABLE).
  4. نشر الكود الجديد المتوافق مع الهيكل الجديد.
  5. إعادة تشغيل الخدمة.
  6. الدعاء والصلاة.

هذه الطريقة سيئة لعدة أسباب:

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

الحل السحري: نمط التوسيع والتعاقد (Expand/Contract)

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

العملية بتتقسم لمرحلتين رئيسيتين: مرحلة التوسيع (Expand) ومرحلة التعاقد (Contract).

المرحلة الأولى: التوسيع (The Expand Phase)

في هذه المرحلة، نحن “نوسّع” كل من قاعدة البيانات والكود البرمجي لاستيعاب التغيير الجديد، لكن بدون إزالة أي شيء قديم. الهدف هو إضافة الجديد مع الحفاظ على القديم لضمان التوافقية.

الخطوة 1: توسيع هيكل قاعدة البيانات (Expand Schema)

أول خطوة هي إضافة الأعمدة الجديدة first_name و last_name إلى جدول users. الأهم هنا هو أن تكون هذه الأعمدة قابلة للقيمة الفارغة (NULLABLE) عشان ما تسبب أي مشاكل للسجلات الموجودة حالياً.


ALTER TABLE users
ADD COLUMN first_name VARCHAR(100) NULL,
ADD COLUMN last_name VARCHAR(100) NULL;

الآن، هيكل قاعدة البيانات يحتوي على القديم والجديد. الكود الحالي ما زال يعمل بدون مشاكل لأنه لا يعرف شيئاً عن الأعمدة الجديدة ويتعامل فقط مع full_name.

الخطوة 2: توسيع الكود البرمجي (Expand Application Code)

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

  • عند الكتابة (Create/Update): أي عملية حفظ لمستخدم جديد أو تحديث لمستخدم حالي، يجب أن يكتب الكود في full_name ويكتب أيضاً في first_name و last_name.
  • عند القراءة (Read): الكود ما زال يقرأ ويعتمد على عمود full_name كمصدر أساسي للمعلومة.

مثال بسيط بلغة تشبه الـ Python لتوضيح الفكرة:


# الكود قبل التعديل
# def save_user(user_data):
#     db.execute("UPDATE users SET full_name = ? WHERE id = ?", 
#                (user_data['full_name'], user_data['id']))

# الكود بعد التعديل (مرحلة التوسيع)
def save_user(user_data):
    full_name = user_data.get('full_name')
    first_name = user_data.get('first_name')
    last_name = user_data.get('last_name')

    # للتوافقية، لو الاسم الكامل بس هو اللي وصل، بنحاول نفصله
    if full_name and not (first_name or last_name):
        parts = full_name.split(' ', 1)
        first_name = parts[0]
        last_name = parts[1] if len(parts) > 1 else ''

    # الكتابة في المكانين: القديم والجديد
    db.execute("UPDATE users SET full_name = ?, first_name = ?, last_name = ? WHERE id = ?",
               (full_name, first_name, last_name, user_data['id']))

# القراءة لا تزال من العمود القديم
def get_user(user_id):
    user_record = db.query("SELECT full_name FROM users WHERE id = ?", (user_id,))
    return {'full_name': user_record.full_name}

بعد كتابة هذا الكود، نقوم بنشره. الآن، أي سجل جديد أو محدّث سيحتوي على البيانات في الأعمدة القديمة والجديدة معاً. النظام مستقر، والإصدارات القديمة من الكود (إذا كنت تعمل ببيئة نشر تدريجي مثل Canary/Blue-Green) ما زالت تعمل بشكل صحيح.

الخطوة 3: ترحيل البيانات القديمة (Backfill Data)

الآن لدينا مشكلة: السجلات القديمة ما زالت تحتوي على بيانات في full_name فقط، بينما الأعمدة الجديدة فارغة. هنا يأتي دور عملية الترحيل أو الـ “Backfilling”.

سنقوم بتشغيل سكربت (مهمة خلفية – Background Job) يمر على كل السجلات القديمة ويملأ الأعمدة الجديدة بناءً على القيمة الموجودة في العمود القديم.

نصيحة من أبو عمر: لا تقم بتشغيل تحديث على ملايين السجلات في جملة UPDATE واحدة! هذا سيؤدي إلى قفل الجدول (Table Lock) لفترة طويلة ويؤثر على أداء قاعدة البيانات بشكل كارثي. قم بالعملية على دفعات (Batches).

مثال لسكربت ترحيل على دفعات:


-- ابحث عن السجلات التي تحتاج ترحيل
SELECT id, full_name 
FROM users 
WHERE first_name IS NULL AND full_name IS NOT NULL
LIMIT 1000; -- نعمل على 1000 سجل في كل مرة

-- بعد الحصول على البيانات في الكود، قم بتشغيل جمل UPDATE لكل سجل أو لمجموعة صغيرة
-- في تطبيقك، قم بالتكرار على الدفعات حتى لا يتبقى سجلات تحتاج للترحيل
-- مثال بسيط للتوضيح
UPDATE users
SET 
    first_name = SUBSTRING_INDEX(full_name, ' ', 1),
    last_name = SUBSTRING(full_name, LENGTH(SUBSTRING_INDEX(full_name, ' ', 1)) + 2)
WHERE 
    id IN ( ... قائمة الـ IDs من الدفعة الحالية ... );

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

المرحلة الثانية: التعاقد (The Contract Phase)

بعد التأكد من أن مرحلة التوسيع تمت بنجاح، وأن كل البيانات تم ترحيلها، وأن النظام مستقر لعدة أيام، نبدأ الآن في “تنظيف” الكود وقاعدة البيانات من الأجزاء القديمة. هذه المرحلة تسمى التعاقد أو التقليص.

الخطوة 4: تقليص الكود البرمجي (Contract Application Code)

حان الوقت لتحديث الكود ليعتمد بشكل كامل على الأعمدة الجديدة.

  • عند القراءة (Read): قم بتغيير كل الأماكن في الكود التي تقرأ full_name لتقرأ من first_name و last_name.
  • عند الكتابة (Write): أزل المنطق الذي كان يكتب في عمود full_name. الآن الكود يكتب فقط في الأعمدة الجديدة.

# الكود بعد التعديل (مرحلة التعاقد)
def save_user(user_data):
    # لم نعد بحاجة للكتابة في full_name
    db.execute("UPDATE users SET first_name = ?, last_name = ? WHERE id = ?",
               (user_data['first_name'], user_data['last_name'], user_data['id']))

# القراءة الآن من الأعمدة الجديدة
def get_user(user_id):
    user_record = db.query("SELECT first_name, last_name FROM users WHERE id = ?", (user_id,))
    return {
        'first_name': user_record.first_name,
        'last_name': user_record.last_name
    }

نقوم بنشر هذا الكود. الآن تطبيقك لا يعتمد على العمود القديم full_name إطلاقاً.

الخطوة 5: تقليص هيكل قاعدة البيانات (Contract Schema)

هذه هي الخطوة الأخيرة والمُرضية. بعد مراقبة النظام لعدة أيام والتأكد 100% أن العمود القديم لم يعد مستخدماً من أي جزء من النظام (لا تنسَ التحقق من التقارير، المهام المجدولة، أي خدمات أخرى)، يمكنك حذفه بأمان.

نصيحة من أبو عمر: قيس مرتين وقص مرة. قبل الحذف، تأكد من أدوات المراقبة (Monitoring) وسجلات الوصول (Access Logs) لقاعدة البيانات أن لا أحد يقرأ من هذا العمود. بعض المبرمجين يفضلون إعادة تسمية العمود أولاً (e.g., full_name_DEPRECATED) كخطوة أمان إضافية قبل الحذف النهائي.


-- الخطوة الأخيرة: حذف العمود القديم
ALTER TABLE users
DROP COLUMN full_name;

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

الخلاصة ونصيحة أخيرة

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

ملخص الخطوات:

  1. توسيع الهيكل: أضف الأعمدة الجديدة (NULLABLE).
  2. توسيع الكود: اجعل الكود يكتب في القديم والجديد، ويقرأ من القديم.
  3. ترحيل البيانات: املأ البيانات الجديدة في الخلفية وعلى دفعات.
  4. تقليص الكود: اجعل الكود يقرأ ويكتب من الجديد فقط.
  5. تقليص الهيكل: احذف الأعمدة القديمة بعد التأكد التام.

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

ودمتم سالمين.

أبو عمر

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

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

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

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

آخر المدونات

الشبكات والـ APIs

كانت إعادة المحاولة كارثة: كيف أنقذتنا مفاتيح عدم تكرار العمليات (Idempotency Keys) من جحيم الفواتير المزدوجة؟

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

4 يونيو، 2026 قراءة المزيد
الحوسبة السحابية

من التوقف التام إلى النجاة: كيف أنقذتنا استراتيجية “الضوء المرشد” (Pilot Light) يوم انقطعت السحابة؟

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

4 يونيو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

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

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

4 يونيو، 2026 قراءة المزيد
التكنلوجيا المالية Fintech

من الانتظار لأيام إلى الدفع في ثوانٍ: كيف أنقذتنا شبكات الدفع الفوري من جحيم التحويلات البنكية؟

أسرد لكم من واقع تجربتي كـ "أبو عمر"، كيف عانينا من بطء وتكلفة التحويلات البنكية الدولية، وكيف جاءت شبكات الدفع الفوري ومعيار ISO 20022 لتكون...

4 يونيو، 2026 قراءة المزيد
البنية التحتية وإدارة السيرفرات

كان كل خادم لدينا ‘ندفة ثلج’ فريدة: كيف أنقذنا ‘الكود كبنية تحتية’ (IaC) من جحيم الانجراف اليدوي؟

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

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

كانت تغطية الاختبارات 100% لكن الأخطاء تتسرب: كيف أنقذنا “الاختبار الطفري” من جحيم الثقة الزائفة؟

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

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