كيف أغير بنية قاعدة البيانات دون إفساد بيانات المستخدمين؟
عالم البرمجة
تعمل الميزة الجديدة في بيئة التطوير، لكن إطلاقها يحتاج تعديلًا في قاعدة بيانات يستخدمها التطبيق
طوال الوقت.
ربما تريد إضافة مرجع جديد إلى جدول قائم، نقل قيمة إلى بنية مختلفة، أو تغيير نوع عمود يحتوي ملايين الصفوف.
هنا يصبح الخطأ أغلى من مجرد فشل عملية نشر.
المشكلة ليست أن أمر ALTER TABLE خطير بذاته في كل مرة.
بعض التغييرات قد تكون سريعة جدًا، بينما قد يعيد تغيير آخر كتابة جدول ضخم أو يحتجز قفلًا يمنع عمليات أخرى.
وما يصلح في محرك أو إصدار قد يتصرف بصورة مختلفة في محرك آخر.
الخطر الأكبر يظهر عندما تصبح نسخة التطبيق القديمة والجديدة وبنية قاعدة البيانات غير متوافقة أثناء الانتقال.
إذا أزلت عنصرًا ما زال الكود القديم يستخدمه، أو جعلت حقلًا إلزاميًا قبل أن تبدأ كل عمليات الكتابة بتعبئته، فقد تتعطل الطلبات حتى لو كان أمر الهجرة نفسه قد نجح.
لذلك لا تتعامل مع تغيير البنية كأمر SQL واحد.
تعامل معه كسلسلة انتقال يجب أن يبقى كل جزء منها صالحًا أثناء وجود أكثر من نسخة من التطبيق والبيانات في الوقت نفسه.
الخطر الحقيقي هو كسر التوافق أثناء الانتقال
في بيئة الإنتاج لا ينتقل النظام عادة من الحالة القديمة إلى الجديدة في لحظة واحدة.
قد تبقى خوادم تشغل الإصدار السابق أثناء بدء نشر الإصدار الجديد، وقد تستمر مهام خلفية أو عمال معالجة لم يصلهم التحديث بعد.
لهذا تكون التغييرات الإضافية أسهل عادة من التغييرات الهدامة.
إضافة عمود اختياري جديد تسمح للكود القديم بتجاهله، بينما حذف عمود أو إعادة تسميته مباشرة قد يكسر أي نسخة ما زالت تشير إلى الاسم السابق.
الفكرة المعروفة باسم التوسيع ثم التقليص
تعتمد على هذا المبدأ: تضيف البنية الجديدة أولًا، تجعل التطبيق قادرًا على العمل معها من دون كسر القديمة، تنقل البيانات والاستخدام تدريجيًا، ثم تزيل الجزء القديم بعد التأكد من توقف الاعتماد عليه.
لكن هذا النمط لا يضمن «انعدام التوقف».
فائدته أنه يقلل مساحة التغيير غير المتوافق ويمنحك نقاط تحقق أكثر.
أما القفل والضغط على التخزين والتكرار المتأخر وأخطاء التطبيق فتبقى مخاطر يجب قياسها بحسب قاعدة البيانات التي تستخدمها.
صنّف التغيير قبل أن تكتب ملف الهجرة
ليست كل تغييرات الـSchema متساوية.
إضافة عمود قابل للقيم الفارغة قد تكون عملية Metadata
سريعة في حالة معينة، بينما تغيير نوع عمود أو إضافة قيمة افتراضية أو قيد جديد قد يحتاج مسحًا أو إعادة كتابة أو قفلًا أقوى وفق المحرك والإصدار.
قبل كتابة الهجرة، اقرأ خطة التنفيذ التي يوثقها محرك قاعدة البيانات للعملية نفسها.
اسأل: ما مستوى القفل؟ هل يُعاد بناء الجدول؟ هل يمكن أن تستمر القراءات والكتابات؟ ما مساحة القرص الإضافية؟ وهل العملية قابلة للإلغاء إذا لم تحصل على القفل بسرعة؟
افحص كذلك حجم الجدول ومعدل الكتابة عليه ووجود معاملات طويلة.
أمر يستغرق ثواني في جدول اختبار صغير قد ينتظر طويلًا في الإنتاج إذا اصطدم بمعاملة مفتوحة
أو كان عليه فحص ملايين الصفوف.
ومن المفيد فصل Schema Migration عن Data Migration.
الأولى تغير شكل القاعدة، والثانية تحول البيانات الموجودة.
جمعهما داخل معاملة ضخمة واحدة قد يجعل التراجع أصعب ويطيل زمن الاحتفاظ بالأقفال.
احفظ تغييرات البنية في ملفات Migration تخضع لمراجعة الكود وتُطبق بالترتيب نفسه، بدل تعديلات يدوية لا يظهر تاريخها.
لكن وجود الملف لا يجعل العملية آمنة تلقائيًا؛ راجع SQL الفعلي الذي سيصدره الإطار، لأن أمرًا بسيطًا في الكود قد يتحول إلى عملية مكلفة داخل المحرك.
القاعدة هنا بسيطة:
لا تعتمد على اسم الأمر لتقدير خطره؛ اعتمد على سلوك العملية في محركك وحجم بياناتك وحركة الإنتاج.
أضف البنية الجديدة قبل أن تجعل التطبيق يعتمد عليها
لنفترض أن جدول الحسابات يخزن اسم الخطة كنص، وأردت الانتقال إلى plan_id يرتبط بجدول للخطط.
الخطوة الخطرة هي إضافة
العمود الجديد وجعله إلزاميًا ثم نشر كود يتوقع أن كل صف قد أصبح مكتملًا.
الأهدأ أن تضيف plan_id أولًا بطريقة متوافقة مع النسخة الحالية، من دون حذف plan_name.
في هذه اللحظة
يستطيع الكود القديم مواصلة عمله، بينما أصبحت لديك مساحة لبناء الانتقال الجديد.
بعد ذلك تنشر نسخة من التطبيق تعرف الحقلين وتستطيع ملء الحقل الجديد عند إنشاء البيانات أو تعديلها.
ولا تجعل القراءة من البنية الجديدة إلزامية قبل أن تعرف أن البيانات القديمة نُقلت وأن مسار الكتابة الجديد يعمل كما توقعت.
هذه المرحلة هي ما يمنحك التوافق بين الإصدارات.
فإذا احتجت إلى إيقاف الإصدار الجديد، تستطيع النسخة السابقة الاستمرار لأن العنصر الذي تعتمد عليه
لم يُحذف بعد.
أما الحذف أو إعادة
التسمية أو جعل الحقل NOT NULL فيأتي لاحقًا، بعد أن تتحول البنية الجديدة من «شيء موجود» إلى «شيء يعتمد عليه النظام وتأكدت من اكتماله».
انقل البيانات القديمة على دفعات قابلة للتكرار والتحقق
Backfill على جدول كبير ليس مجرد سكربت تشغله مرة وتنتظر.
تحديث ملايين الصفوف في معاملة واحدة قد يستهلك سجل المعاملات ويزيد ضغط الإدخال والإخراج ويطيل الأقفال أو يرفع تأخر النسخ المتماثلة.
صمم عملية النقل على دفعات صغيرة يمكن إيقافها واستكمالها.
احتفظ بنقطة تقدم واضحة، واجعل إعادة تشغيل الدفعة آمنة قدر الإمكان، بحيث لا تفسد البيانات
إذا نفذت المهمة مرتين بعد فشل جزئي.
اقرأ ايضا : لماذا يفشل ربط الواجهة البرمجية رغم أن الكود يبدو صحيحًا؟
في مثال plan_id تستطيع معالجة مجموعة محددة من الحسابات، تحويل القيمة القديمة إلى المرجع الجديد، ثم تسجيل عدد الصفوف التي نجحت والتي لم تجد لها مطابقة.
لا تجعل «انتهى السكربت بلا خطأ» هو معيار الصحة الوحيد.
أضف مقارنة قابلة للقياس بين القديم والجديد: كم صفًا ما زال بلا قيمة؟ كم حالة لا تتطابق؟
هل عدد السجلات المتوقع يساوي ما نُقل؟ وإذا كانت هناك حالات استثنائية، عالجها صراحة بدل إخفائها بقيمة افتراضية غير صحيحة.
سرعة الـBackfill يجب أن تتكيف مع النظام.
إذا ارتفع زمن الاستعلامات أو تأخر التكرار أو زادت طوابير الاتصال، خفف معدل الدفعات أو أوقفها.
الهدف هو إنهاء النقل من دون تحويله إلى منافس لحركة المستخدمين.
إذا كتبت في الهيكلين فخطط لاحتمال اختلافهما
الكتابة المزدوجة شائعة أثناء الانتقال، لكنها ليست ضمانًا تلقائيًا للتطابق.
قد تنجح
الكتابة في العمود القديم وتفشل في الجديد، أو يحدث Retry في أحد المسارين فقط، أو يمر كود قديم لا يعرف البنية الجديدة أصلًا.
لذلك حدد خلال الفترة الانتقالية مصدر الحقيقة بوضوح.
هل القديم هو المرجع إلى أن يكتمل التحويل؟ أم أصبحت البنية الجديدة المرجع بعد نقطة محددة؟
من دون هذا القرار يصبح إصلاح الاختلافات لاحقًا تخمينًا.
إذا كانت الكتابتان داخل المعاملة نفسها وكان محركك ونموذجك يسمحان بذلك، فقد تقل بعض حالات الانقسام، لكنها لا تلغي أخطاء المنطق أو المصادر الأخرى للكتابة.
وإذا كان الانتقال بين جداول أو خدمات مختلفة فقد تحتاج آلية أقوى للمزامنة وإعادة المحاولة.
راقب الاختلافات بدل افتراض عدم وجودها.
عداد بسيط للحالات غير المتطابقة أو مهمة تحقق دورية قد تكشف أن مسارًا قديمًا ما زال يكتب بيانات لا تصل إلى الهيكل الجديد.
والأهم ألا تطيل فترة الكتابة المزدوجة بلا حاجة.
كلما بقي نظامان قابلان للكتابة مدة أطول، زاد عدد المسارات التي يجب فهمها قبل حذف القديم.
الفهارس والقيود تحتاج خطة خاصة بكل محرك
إضافة فهرس إلى جدول كبير مثال واضح على خطورة التعميم.
بعض المحركات توفر طريقة لبناء الفهرس مع السماح باستمرار عمليات الكتابة، لكنها قد تستهلك
وقتًا وموارد أكثر، ولها قيود وحالات فشل خاصة.
في PostgreSQL مثلًا يسمح البناء المتزامن للفهرس باستمرار عمليات الإدخال والتحديث والحذف،
لكنه ينفذ عملًا إضافيًا وقد يبطئ عمليات أخرى.
وإذا فشل قد يبقى فهرس غير صالح يحتاج معالجة، لذلك عبارة «يبنى في الخلفية بلا أثر» غير دقيقة.
القيود تحتاج الحذر نفسه.
تطبيق Foreign Key أو NOT NULL أو Unique Constraint على بيانات قديمة قد يتطلب فحصًا واسعًا.
بعض المحركات تتيح فصل إضافة القيد عن التحقق الكامل من الصفوف القديمة، ثم تنفيذ التحقق في خطوة لاحقة بأقفال مختلفة.
وفي MySQL توجد عمليات Online DDL وخيارات مثل LOCK=NONE، لكن دعمها يعتمد على نوع التغيير والخوارزمية والإصدار؛ إذا كانت العملية لا تدعم مستوى التزامن المطلوب فلا يصح افتراض أنها ستنفذه
بلا حجب.
اكتب الخطة بصيغة «في هذا المحرك وهذه النسخة، هذا الأمر يفعل كذا»، لا بصيغة «قواعد البيانات الحديثة تفعل كذا».
هذه الجملة وحدها تمنع عددًا كبيرًا من الهجرات المبنية على معرفة صحيحة في منتج وخاطئة في آخر.
افصل بين نشر الكود وخطر تعديل قاعدة البيانات
يمكنك تشغيل القراءة من الهيكل الجديد خلف Feature Flag أو تفعيلها تدريجيًا لشريحة محدودة من المستخدمين.
هذا مفيد لاكتشاف أخطاء التطبيق ومقارنة النتائج قبل جعل المسار الجديد هو الافتراضي للجميع.
لكن لا تخلط ذلك مع حصر أثر عملية الـDDL نفسها.
عندما تنشئ فهرسًا أو تغير جدولًا مشتركًا، فإن العملية تقع على قاعدة البيانات التي يستخدمها الجميع؛ تفعيل الميزة لخمسة بالمئة لا يجعل قفل الجدول يؤثر في خمسة بالمئة فقط.
استخدم النشر التدريجي لما يمكن التحكم فيه على مستوى التطبيق: القراءة الجديدة، منطق التحويل، أو مسار كتابة جديد.
أما تغييرات الـSchema والـBackfill فراقبها بمقاييس قاعدة البيانات نفسها.
راقب زمن الاستعلامات، انتظار الأقفال، معدل الأخطاء، ضغط المعالج والتخزين، عدد الاتصالات، وتأخر النسخ إن وجدت.
أضف أيضًا مقاييس صحة البيانات، لأن Migration قد تكون سريعة ولا تسبب أخطاء HTTP
ومع ذلك تنقل قيمًا بصورة خاطئة.
حدد مسبقًا شروط التوقف.
إذا تجاوز Lock
Wait أو Latency أو Replication Lag حدًا
لا يقبله نظامك، يجب أن تعرف هل ستوقف الـBackfill أو تلغي العملية أو تبقي التطبيق على المسار القديم.
التراجع ليس دائمًا تشغيل الهجرة بالعكس
من المغري أن تكتب لكل Migration دالة down ثم تعتبر Rollback محلولًا.
هذا مفيد لبعض التغييرات، لكنه لا يعيد دائمًا البيانات التي حُذفت أو يحول القيم المعقدة إلى شكلها السابق بلا فقد.
إذا أسقطت عمودًا ثم اكتشفت أن الإصدار السابق يحتاجه، فلن يعيد إنشاء العمود القيم التي كانت داخله.
وإذا دمجت قيمًا متعددة في تمثيل واحد غير قابل للعكس، فإن الهجرة العكسية لا تستطيع اختراع المعلومات المفقودة.
لهذا تكون أفضل خطة تراجع
في كثير من التحولات هي إبقاء البنية القديمة متوافقة مدة كافية كي تستطيع إعادة نشر كود سابق، ثم إصلاح المشكلة أو التقدم إلى نسخة مصححة قبل تنفيذ التنظيف الهدام.
النسخ الاحتياطي مهم، لكن عبارة «لدينا Backup» لا تكفي.
يجب أن تعرف زمن الاستعادة، وما البيانات التي ستفقد منذ لحظة النسخة، وهل اختبرت الاستعادة أصلًا.
استرجاع قاعدة كاملة ليس زر Undo مناسبًا لكل خطأ صغير في Migration.
واختبر مسار الفشل لا مسار النجاح فقط.
أوقف الـBackfill بعد دفعة، أعد تشغيله، جرّب فشلًا في منتصف النشر، وتأكد أن النسخة السابقة تستطيع العمل قبل اعتماد خطة التراجع.
اجعل الحذف والتنظيف آخر خطوة بعد فترة مراقبة تثبت أن لا كود ولا مهمة خلفية ولا تقرير
ما زال يعتمد على البنية القديمة.
استخدم بوابة أمان قبل كل تغيير في الإنتاج
قبل التنفيذ مرر الخطة عبر خمس نقاط.
الأولى التوافق: هل يستطيع الإصدار القديم والجديد العمل مع الـSchema نفسها أثناء النشر؟ إذا كانت الإجابة لا، فقد جمعت خطوات يجب فصلها.
الثانية القفل: ما
الذي يوثقه محركك عن العملية على هذا الحجم؟ هل تعيد كتابة الجدول، وما مستوى التزامن الذي تسمح به، وما مهلة الانتظار المقبولة قبل إلغاء الأمر؟
الثالثة البيانات: كيف
ستنقل الصفوف القديمة، وكيف ستتحقق من الاكتمال والتطابق، وهل تستطيع إعادة تشغيل النقل بعد فشل جزئي من دون مضاعفة الخطأ؟
الرابعة المراقبة: ما المقاييس التي ستكشف ضرر الأداء وضرر البيانات؟ لا يكفي أن ترى أن التطبيق
«ما زال يعمل» إذا كانت الأقفال تتراكم أو القيم الجديدة لا تطابق المصدر.
الخامسة الخروج: ماذا
ستفعل إذا فشل التغيير بعد منتصف الطريق؟ هل تستطيع إيقافه، إعادة التطبيق إلى المسار القديم، أو التقدم بإصلاح من دون حذف بيانات؟
خذ التغيير القادم في مشروعك ودوّن إجابة صريحة عن هذه النقاط الخمس قبل تشغيل أول Migration في الإنتاج.
اقرأ ايضا : كيف تفهم مشروعًا برمجيًا قديمًا لم تشارك في كتابته؟
إذا بقيت نقطة بلا جواب، فهذه ليست دعوة للخوف من قاعدة البيانات؛ إنها إشارة إلى أن خطة الانتقال نفسها ما زالت ناقصة.
التغيير الآمن لا يعني أن تعد نفسك بعدم وقوع أي توقف أو خطأ.
يعني أن تقلل التغيير غير المتوافق، تعرف أثر أوامر محركك، تنقل البيانات بطريقة قابلة للتحقق، وتحتفظ بمسار خروج قبل أن تزيل البنية التي كان النظام يعتمد عليها.
