إدارة إصدارات مخطّط قاعدة بيانات تطبيقات الأعمال ── ممارسات الترحيل لمنع «اختلاف قاعدة البيانات من عميل لآخر»
· آخر تحديث: · غو كومورا · قواعد البيانات, SQLite, SQL Server, الترحيل, إدارة المخطّط, C#, .NET, الصيانة, جدول القرار, تطوير Windows
سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621728)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
غو كومورا (2026). إدارة إصدارات مخطّط قاعدة بيانات تطبيقات الأعمال ── ممارسات الترحيل لمنع «اختلاف قاعدة البيانات من عميل لآخر». شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621728 https://comcomponent.com/ar/blog/db-schema-migration-versioning-business-apps/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621728
- DOI (هذه النسخة)
- 10.5281/zenodo.22241050
«يوجد هذا العمود في قاعدة البيانات المثبَّتة لدى الشركة A، لكنّه غير موجود في قاعدة بيانات الشركة B. ولم يعد أحد يتذكّر في أيّ إصدار أُضيف هذا العمود» ── عند تسلُّم صيانة تطبيق أعمال يُثبَّت لدى العملاء، من المرجَّح جدّاً أن تصادف هذه الحالة.
في دليل إجراءات التحديث مكتوب «مرّر هذا SQL على قاعدة البيانات». لكن من نفّذه فعلاً لا يعرفه سوى العامل في الميدان، فتختلط مواقع نُسي فيها التنفيذ، ومواقع تُرك فيها الخطأ في المنتصف، ومواقع قفزت إصداراً فسقط ALTER TABLE الوسيط. بعد سنوات تُبتلع ساعات التحقيق في «خطأ يحدث لدى عميل معيّن فقط» بلا نهاية.
في هذه المدوّنة شرحنا في «كيف تختار مكان حفظ بيانات تطبيق Windows محليّاً» الحدّ الأدنى من الشيفرة لإلحاق رقم إصدار بالمخطّط، وفي «استخدام SQLite في تطبيقات C# للأعمال» تصميم تشغيل SQLite. هذا المقال امتداد لذلك، ويدخل في كيف ندير تغييرات مخطّط قاعدة البيانات بإصدارات، وكيف نطبّقها بأمان على عشرات قواعد البيانات الموزَّعة لدى العملاء. نجعل SQLite الموضوع الرئيس، ونرتّبه كتصميم مشترك أيضاً مع SQL Server (Express).
1. الخلاصة أوّلاً
- تغيير المخطّط لا يكون دليل إجراءات SQL، بل يُضمَّن في التطبيق نفسه كشيفرة (ترحيلات مرقَّمة) ويُطبَّق تلقائيّاً عند بدء التشغيل. تشغيل يعتمد على أن ينفّذ أشخاص الدليل ينهار لحظة توزُّع قواعد البيانات لدى العملاء.
- تسجّل قاعدة البيانات نفسها رقم إصدار المخطّط الحاليّ. في SQLite منطقة
PRAGMA user_versionمخصَّصة لهذا الغرض تحديداً.1 في SQL Server تُترك سجلّات التطبيق في جدول مخصَّص. - الترحيل أماميّ فقط وإلحاق فقط. لا تُعاد كتابة SQL لرقم شُحن مرّة، والإصلاح أيضاً برقم جديد. بهذا يصبح التحديث القافز من v1.2 إلى v1.5 مجرّد «تمرير غير المطبَّق بالترتيب».
- التغييرات المدمِّرة (حذف عمود أو إعادة تسميته) تُجرى بإصدار على مرحلتين expand-contract. expand-contract أسلوب إصدار على مرحلتين: تُضاف البنية الجديدة مع إبقاء البنية القائمة (expand)، ثمّ تُحذف البنية القديمة بعد اكتمال انتقال جهة التطبيق (contract). تُصدَر أوّلاً إضافة فقط، ثمّ إصدار الحذف بعد اختفاء المراجع إلى الصيغة القديمة (الفقرة 5.1).
- حادث فتح تطبيق بإصدار قديم لقاعدة بيانات جديدة يُدافَع عنه بفحص الحدّ الأدنى للإصدار. هنا يُحمَل «رقم المخطّط الحاليّ» و«حدّ الإقصاء الأدنى» كقيمتين منفصلتين. إن جمعتهما في القيمة نفسها، يُقصى التطبيق القديم لحظة تطبيق expand، فلا تقوم فترة التعايش أعلاه (الفقرة 5.2).
- يُؤخذ نسخ احتياطيّ تلقائيّ قبل التطبيق. في SQLite جملة
VACUUM INTOواحدة تعطي نسخة متّسقة،2 والاستعادة عند الفشل تصبح استبدال ملفّ. - ترحيل واحد = معاملة واحدة، وتحديث رقم الإصدار أيضاً في المعاملة نفسها. SQLite يستطيع إرجاع DDL أيضاً ضمن المعاملة.3 لـ SQL Server DDL استثنائيّ، لذلك تُفصَل العمليّات الاستثنائيّة في ترحيل مستقلّ.4
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 22، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. لماذا تحدث مشكلة «قاعدة البيانات تختلف من عميل لآخر»؟
إن فكّكت الأسباب، تصل كلّها إلى «تشغيل يفترض أن يقوم به إنسان».
- فوات تطبيق ALTER اليدويّ. لا يبقى في قاعدة البيانات أيّ سجلّ لمرور SQL الدليل من عدمه، ولحظة أن تصبح وسيلة التحقّق «النظر في تعريف الجداول» وحدها، يحدث الفوات حتماً.
- ترك الفشل في المنتصف. عندما يخطئ الثالث من خمسة SQL في الدليل، لا يستطيع العامل الحكم بالمتابعة أو الإرجاع، فيصير الأمر «التطبيق عمل فتركناه». تلك القاعدة لم تعد تطابق أيّ إصدار: مخطّط فريد في العالم.
- تحديث قافز بين الإصدارات. لدى العميل الذي يضع v1.5 بعد v1.2 يلزم تتبُّع تغييرات مخطّط v1.3 وv1.4 مجتمعة بشكل صحيح، وهذا عسير في تشغيل الدليل.
- رقعة ميدانيّة للطوارئ. يحدث «لهذا العميل أُضيف العمود أوّلاً»، فيصير خطأ تطبيق مزدوج في التحديث الرسميّ لاحقاً.
في نظام ويب على خادم واحد قاعدة البيانات واحدة، والحالة تُعرَف دائماً. الصعوبة الجوهريّة لتطبيق أعمال سطح المكتب هي أنّ قاعدة بيانات التطبيق نفسه تتوزّع على عشرات ومئات من حواسيب العملاء والمواقع، وليست كلّها بالإصدار نفسه. تشغيل يعالج جهازاً جهازاً ينهار بنسبة عدد الأجهزة، فالخلاصة واحدة: أن يملك التطبيق نفسه القدرة على فحص قاعدة بياناته ورفعها إلى أحدث مخطّط.
3. النمط الأساس: رقم إصدار المخطّط + ترحيل أماميّ
هيكل الآليّة ثلاثة عناصر فقط.
- تملك قاعدة البيانات نفسها رقم إصدار مخطّط (عدد صحيح مخصَّص للمخطّط، منفصل عن إصدار منتج التطبيق).
- تغييرات المخطّط تُلحَق كسلسلة ترحيلات مرقَّمة في شيفرة التطبيق.
- عند بدء التشغيل (بعد الاتّصال بقاعدة البيانات مباشرة) يطبّق التطبيق بالترتيب، ضمن معاملات، الترحيلات ذات الأرقام الأكبر من الإصدار الحاليّ.
رسم تدفّق بدء التشغيل كالتالي. الدفاعات التي نضيفها في الفصلين 5 و6 تدخل كلّها في موضع ما من هذا المسار.
flowchart TD
S["بدء التطبيق والاتّصال بقاعدة البيانات"] --> R["قراءة PRAGMA user_version ورقم التوافق الأدنى"]
R --> Q1{"هل رقم التوافق الأدنى<br/>أكبر من أقصى رقم يعرفه التطبيق؟"}
Q1 -->|"أكبر"| STOP["إيقاف البدء (الفقرة 5.2)"]
Q1 -->|"لا"| Q2{"هل الرقم الحاليّ<br/>أكبر من أقصى رقم يعرفه التطبيق؟"}
Q2 -->|"أكبر"| FUT["مخطّط مستقبليّ متوافق.<br/>بدء عاديّ دون تطبيق (الفقرة 5.2)"]
Q2 -->|"لا"| Q3{"هل توجد ترحيلات غير مطبَّقة؟"}
Q3 -->|"لا"| OK["بدء عاديّ كما هو"]
Q3 -->|"نعم"| BK["أخذ نسخة احتياطيّة قبل التطبيق<br/>(VACUUM INTO، الفقرة 5.3)"]
BK --> LOOP["تطبيق الأرقام غير المطبَّقة تصاعديّاً واحداً واحداً (الفقرة 6.1)<br/>BEGIN TRANSACTION ثمّ تغيير المخطّط وتحويل البيانات ثمّ<br/>PRAGMA user_version = ذلك الرقم ثمّ COMMIT"]
LOOP -.->|"عند الفشل في المنتصف"| FAIL["يُرجَع ذلك الواحد فقط،<br/>ويتوقّف عند الرقم السابق"]
LOOP --> DONE["عند الوصول إلى الأحدث، بدء عاديّ"]
الشكل 1: تدفّق التطبيق عند بدء التشغيل. الدفاعات التي تُضاف في الفصلين 5 و6 تدخل كلّها في موضع ما من هذا المسار.
في SQLite يمكن استخدام PRAGMA user_version كموضع لرقم الإصدار. هو عدد صحيح يُخزَّن في ترويسة قاعدة البيانات (الإزاحة 60)، وتنصّ الوثائق الرسميّة على أنّ «التطبيق حرّ في استخدامه، وSQLite نفسه لا يستخدم هذه القيمة».1 تستطيع قاعدة البيانات أن تعلن إصدارها بملفّها وحده، دون صنع جدول مخصَّص.
التنفيذ الذاتيّ بلغة C# يصبح عمليّاً في عشرات الأسطر التالية.
using Microsoft.Data.Sqlite;
public static class SchemaMigrator
{
// 追記専用のリスト。一度出荷した番号のSQLは絶対に書き換えない
private static readonly (int Version, string Sql)[] Migrations =
{
(1, """
CREATE TABLE customer (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
-- 締め出しの下限を置く表。1番の中で作って初期値を入れておく。
-- ここを別立てにすると誰も作らないまま GetMinCompatibleVersion が
-- 常に0を返し、締め出しが効かないうえ、最初の contract で
-- UPDATE schema_meta が「表が無い」で倒れる(5.2節)
CREATE TABLE schema_meta (key TEXT PRIMARY KEY, value INTEGER NOT NULL);
INSERT INTO schema_meta (key, value) VALUES ('min_compatible_version', 0);
"""),
(2, "ALTER TABLE customer ADD COLUMN phone TEXT"),
(3, """
CREATE TABLE invoice (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customer(id),
issued_at TEXT NOT NULL, -- UTC・書式固定で保存する
amount INTEGER NOT NULL -- 金額は最小通貨単位の整数
)
"""),
};
public static void Migrate(SqliteConnection conn)
{
// خطأ الإلحاق (تكرار رقم أو عكس الترتيب) يصير تطبيقاً مزدوجاً أو تخطّياً صامتين،
// لذا اكتشفه وأوقف قبل تطبيق أي شيء
for (int i = 1; i < Migrations.Length; i++)
if (Migrations[i].Version <= Migrations[i - 1].Version)
throw new InvalidOperationException(
"يجب أن تكون أرقام الترحيل تصاعدية وفريدة.");
int current = GetUserVersion(conn);
int latest = Migrations[^1].Version;
int minCompatible = GetMinCompatibleVersion(conn);
// الإقصاء يُحكَم بـ «أدنى رقم توافق» لا بـ «رقم المخطط».
// إن حكمت بـ current > latest، فما إن يطبّق التطبيق الجديد expand
// حتى يرتفع user_version ويعجز التطبيق القديم عن فتح قاعدة البيانات فوراً
// ── تصميم 5.1 «في فترة التعايش يكتب الجديد والقديم كلاهما» لا يقوم أصلاً.
// ارفع أدنى رقم توافق عند contract فقط
if (minCompatible > latest)
throw new InvalidOperationException(
$"تتطلّب قاعدة البيانات هذه تطبيقاً يفهم المخطط v{minCompatible} فما بعد" +
$" (هذا التطبيق يعرف حتى v{latest})." +
" حدِّث التطبيق.");
if (current > latest)
// مخطط مستقبلي لا يعرفه هذا التطبيق، لكنّه معلَن متوافقاً.
// لا شيء لتطبيقه (كلّه تحت current أو يساويه)،
// فتابع التشغيل العادي دون لمس أعمدة مجهولة (التفاصيل في 5.2)
return;
foreach (var (version, sql) in Migrations)
{
if (version <= current) continue;
using var tx = conn.BeginTransaction();
using var cmd = conn.CreateCommand();
cmd.Transaction = tx;
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
// バージョン更新も同じトランザクションで確定させる。
// これで「変更は入ったが番号が古いまま」という状態が消える
cmd.CommandText = $"PRAGMA user_version = {version}";
cmd.ExecuteNonQuery();
tx.Commit();
}
}
private static int GetUserVersion(SqliteConnection conn)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "PRAGMA user_version";
return Convert.ToInt32(cmd.ExecuteScalar());
}
// 「このDBを開いてよいアプリの下限」。user_version とは別に持つのが要点で、
// 同じ値で兼ねると、expand を適用した瞬間に旧アプリを締め出してしまう。
// 上げるのは contract のマイグレーションのときだけ(5.1・5.2節)
private static int GetMinCompatibleVersion(SqliteConnection conn)
{
using var exists = conn.CreateCommand();
exists.CommandText =
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schema_meta'";
if (exists.ExecuteScalar() is null) return 0; // 表が無い古いDB。下限なし
using var cmd = conn.CreateCommand();
cmd.CommandText =
"SELECT value FROM schema_meta WHERE key = 'min_compatible_version'";
var value = cmd.ExecuteScalar();
return value is null or DBNull ? 0 : Convert.ToInt32(value);
}
}
schema_meta يُصنَع، كما أعلاه، داخل ترحيل الرقم 1. إن فُصل في سكربت آخر، لا ينفّذه أحد، فيُرجع GetMinCompatibleVersion دائماً 0، ولا يعمل الإقصاء أصلاً. ثمّ يسقط أوّل contract عند UPDATE schema_meta بـ «الجدول غير موجود».
عند إلحاق هذه الآليّة بقاعدة بيانات قائمة لا يوجد schema_meta في قواعد البيانات التي طُبِّق عليها الرقم 1. اصنعه في ترحيل جولة الإدخال بـ CREATE TABLE IF NOT EXISTS وINSERT OR IGNORE (مجرّد إضافة رقم واحد). سبب أنّ GetMinCompatibleVersion يتحقّق أوّلاً من وجود الجدول هو فترة الانتقال هذه.
لا يُرفَع هذا الرقم إلّا في ترحيل contract.
-- 旧列を削除する回のマイグレーションで、同じトランザクションの中で上げる
UPDATE schema_meta SET value = 7 WHERE key = 'min_compatible_version';
ALTER TABLE customer DROP COLUMN old_name;
بهذا تُحَلّ مشكلات الفصل 2 بنيويّاً. لا يحدث فوات التطبيق (يُفحَص عند كلّ بدء)، الفشل في المنتصف يُرجَع (الفصل 6)، القفز بين الإصدارات لا مشكلة (إن كانت قاعدة v1.2 عند مخطّط v2، يطبّق تطبيق v1.5 الأرقام 3 و4 و5 بالترتيب). و«حالة قاعدة هذا العميل» تُجاب بقراءة PRAGMA user_version مرّة واحدة.
قاعدتا التشغيل يُلتزَم بهما بصرامة.
- لا تُعاد كتابة رقم شُحن. حتّى إن وُجد خلل في SQL لـ v3 يُصلَح في v4. إعادة الكتابة تولّد تفاوتاً جديداً بين «v3 القديم مطبَّق» و«v3 الجديد مطبَّق».
- تحويل البيانات أيضاً يُضمَّن في الترحيل. لا إضافة الأعمدة فقط، بل نقل البيانات القائمة (UPDATE) أيضاً في الرقم نفسه. إن وُحِّد تنسيق عمود التاريخ والمنطقة الزمنيّة أوّلاً بـ UTC وتنسيق ثابت كما في «التاريخ والوقت والمناطق الزمنية في تطبيقات الأعمال»، صار الترحيل اللاحق أبسط.
عمل آخر فقط عند إلحاق هذه الآليّة بنظام قائم. قواعد البيانات التي عاشت على الرقع اليدويّة قد تكون في حالة «user_version ما زال 0، والمخطّط الفعليّ تقدّم جزئيّاً» (رقعة الطوارئ في الفصل 2 هي ذلك تحديداً). إن وُضعت على هذه السلسلة كما هي، يسقط ALTER TABLE لتغيير مطبَّق مسبقاً بخطأ «العمود موجود بالفعل». في أوّل إصدار للإدخال، كمعالجة خطّ أساس لمرّة واحدة، افحص المخطّط الفعليّ (في SQLite تأكيد وجود العمود بـ PRAGMA table_info)، واحرق رقم الإصدار المقابل في قواعد البيانات التي عليها رقع يدويّة معروفة، ثمّ اترك ما بعد ذلك للترحيل الأماميّ. تخطّي هذه الخطوة ممكن فقط إن أُدخلت الآليّة من الإصدار الأوّل.
لا يملك SQL Server آليّة تعادل user_version، لذلك يُدرَج في جدول مخصَّص (مثال: schema_version) سطر لكلّ «رقم الإصدار، وقت التطبيق، إصدار التطبيق عند التطبيق». بقاء السجلّ كصفوف يقوّي التحقيق لاحقاً.
4. أداة أم تنفيذ ذاتيّ ── جدول القرار
وسائل تحقيق الشيء نفسه ثلاث سلاسل: EF Core Migrations، ومكتبات الترحيل (مثل DbUp)، والتنفيذ الذاتيّ في الفصل السابق. الاسم وحده لا يبيّن الأداة، فنضيف جملة لكلّ منها أوّلاً.
- EF Core (Entity Framework Core) ── مُخطِّط كائنات/علاقات من Microsoft لـ .NET (مكتبة تربط الكائنات بالجداول وتولّد SQL تلقائيّاً). وظيفتها الملحقة EF Core Migrations ترصد تغيير النموذج المكتوب بـ C# (تعريف الأصناف) وتولّد شيفرة تغيير المخطّط تلقائيّاً.
- DbUp ── مكتبة .NET مفتوحة المصدر متخصّصة في إدارة تطبيق سكربتات SQL. تكتب SQL بنفسك، وتتولّى هي فقط تسجيل «أيّ سكربت طُبِّق» وتنفيذ غير المطبَّق.5
| محور المقارنة | EF Core Migrations | مكتبة ترحيل (DbUp ونحوها) | تنفيذ ذاتيّ |
|---|---|---|---|
| وصف التغيير | توليد تلقائيّ من تغيير نموذج C# | تحويل سكربتات SQL إلى أصل كما هي | سلسلة SQL أو شيفرة C# |
| تكلفة التعلّم | عالية (يلزم فهم النموذج والأدوات والقيود) | منخفضة إلى متوسّطة | أدنى (فهم عشرات الأسطر فقط) |
| التوافق مع SQLite | △ تغيير العمود وحذفه يصيران إعادة بناء جدول. لا توليد سكربت متماثل6 | ○ محوره SQL Server لكنّه يدعم SQLite أيضاً5 | ◎ يُكتب مباشرة مع معرفة القيود |
| أصل SQL الخام القائم | صعب إعادة الاستخدام (يلزم الاستبدال بتعريف نموذج) | ◎ يمكن نقل SQL الدليل شبه كما هو | ◎ كذلك |
| إدارة المطبَّق | جدول سجلّ (تلقائيّ) | جدول يوميّات (تلقائيّ)5 | user_version / جدول ذاتيّ |
| التوافق مع شكل التوزيع | تضمين في التطبيق وMigrate() عند البدء (ملاحظات لاحقاً) |
تضمين في التطبيق وتنفيذ عند البدء | تضمين في التطبيق وتنفيذ عند البدء |
التوصية حسب الوضع كالتالي.
| الوضع | التوصية | السبب |
|---|---|---|
| الوصول إلى البيانات بـ EF Core بالفعل | EF Core Migrations | يُتجنَّب الإدارة المزدوجة للنموذج والمخطّط. لا سبب لإضافة أداة أخرى |
| محور SQL خام (ADO.NET / Dapper) + SQLite | تنفيذ ذاتيّ | يكفي بلا تبعيّة. قيود ALTER TABLE في SQLite ستُوعى بنفسك في النهاية |
| محور SQL خام + SQL Server، وتراكم كبير لـ SQL الدليل | مكتبة مثل DbUp | يمكن تحويل SQL القائم إلى سكربتات، ولا حاجة لصناعة إدارة التطبيق |
| كثرة الإجراءات المخزَّنة والعروض | مكتبة مثل DbUp | الكائنات التي لا تُولَّد من النموذج تُدار بسكربت SQL بصراحة |
| قاعدة صغيرة وتغييرات قليلة | تنفيذ ذاتيّ | تقليل تكلفة صيانة الآليّة إلى أدنى حدّ |
DbUp «مكتبة .NET تساعد على نشر التغييرات إلى قاعدة بيانات SQL Server»، تسجّل السكربتات المنفَّذة في جدول يوميّات وتنفّذ غير المنفَّذ فقط. تدعم أيضاً SQLite وPostgreSQL وMySQL وغيرها.5 كمسار انتقال «تحويل SQL الدليل إلى تطبيق آليّ مع سجلّ تنفيذ» هو الأقصر.
4.1 ملاحظات استخدام EF Core Migrations في تطبيق موزَّع
تطبيق EF Core أثناء التطوير هو dotnet ef database update، لكن حاسوب العميل لا يملك SDK ولا مصدراً. وسيلة التطبيق الواقعيّة هي context.Database.Migrate() عند بدء التطبيق.
ما ينبغي معرفته هنا أنّ وثائق Microsoft تنبّه بوضوح إلى التطبيق عند بدء التشغيل كوسيلة لإدارة قاعدة بيانات الإنتاج. الأسباب خمسة: (1) فشل أو تلف بسبب التطبيق المتزامن من عدّة نُسَخ (قبل EF Core 9)، (2) وصول تطبيقات أخرى إلى قاعدة البيانات أثناء التطبيق قد يسبّب مشكلات خطيرة، (3) يلزم التطبيق صلاحيّة مرتفعة لتغيير المخطّط، (4) وسائل التراجع شحيحة، (5) تعذّر مراجعة SQL المنفَّذ مسبقاً وتعديله، والتوصية هي توليد سكربت SQL وتطبيقه في خطوة النشر.7
لكن هذه التوصية تفترض نظام خادم «قاعدة واحدة وخطوة نشر موجودة». في تطبيق سطح مكتب لكلّ حاسوب عميل قاعدة محلّيّة، تشغيل يحمل السكربت ويدور في الميدان هو مشكلة الفصل 2 نفسها، لذلك Migrate() عند البدء هو الحلّ المعياريّ فعليّاً. نُعالِج المخاوف المتبقّية.
- التنفيذ المتزامن: من EF Core 9 فصاعداً يأخذ
Migrate()قفلاً تلقائيّاً ويمنع تنفيذ ترحيل متزامن من عدّة عمليّات.7 في الإصدارات الأسبق تُسلسَل بنفسك كما في الفصل 6. لكنّ هذا القفل يسلسل تنفيذ الترحيلات فيما بينها فقط، ولا يوقف قراءة تطبيق بإصدار قديم وكتابته عادياً أثناء التطبيق. في قاعدة مشتركة يُفترَض الجمع مع فحص الحدّ الأدنى للإصدار (الفقرة 5.2) أو نافذة صيانة. - مراجعة SQL مسبقاً: راجع الترحيلات المولَّدة حتماً، وأعدّ بروفة قبل الإصدار على قاعدة تعادل البيانات الفعليّة (الفقرة 6.3).
- لا تخلط مع
EnsureCreated(): يُبنى المخطّط بلا سجلّ ترحيل، ثمّ يفشلMigrate()لاحقاً. وحِّد علىMigrate()من البداية.7
في موفّر SQLite، الترحيلات التي تتضمّن تغيير نوع العمود أو حذفه تُنفَّذ كإعادة بناء جدول «صنع جدول جديد ثمّ نسخ البيانات ثمّ حذف الجدول القديم ثمّ إعادة التسمية»، ولا يمكن توليد سكربت متماثل أيضاً.6 حكم استخدام EF Core نفسه رتّبناه في الفصل 8 من «استخدام SQLite في تطبيقات C# للأعمال».
5. كيف تُكتب ترحيلات لا تنكسر
مبدأ كلّ ترحيل هو «لا تُجرِ في إصدار واحد تغييراً يكسر التوافق الخلفيّ».
5.1 التغييرات المدمِّرة بـ expand-contract (إصدار على مرحلتين)
إضافة عمود آمنة، أمّا الحذف وإعادة التسمية وتغيير النوع فتكسر «شيئاً يفترض الصيغة القديمة». حتّى في SQLite محلّيّ بعلاقة واحد لواحد بين التطبيق وقاعدة البيانات، يوجد غالباً واحد من: (أ) احتمال إرجاع التطبيق إلى إصدار قديم عند الخلل، (ب) أداة أخرى تقرأ قاعدة البيانات مباشرة (أداة تقارير، مُصدِّر CSV، ربط Access)، (ج) تكوين يشارك فيه عملاء جدد وقدماء SQL Server في الوقت نفسه. لذلك تُقسَم التغييرات المدمِّرة إلى مرحلتين: expand (توسيع) ثمّ contract (تقليص).
على المحور الزمنيّ، الجوهر هو إدخال «فترة تعايش يعمل فيها الشكلان الجديد والقديم» بينهما.
flowchart TB
A["الإصدار A (expand: توسيع)<br/>تُضاف البنية الجديدة وتُترك البنية القديمة كما هي<br/>يكتب التطبيق في الاثنين، والقراءة تعتبر البنية القديمة المرجع"]
B["فترة التعايش<br/>التطبيقات والأدوات القديمة تعمل كما هي (البنيتان حيّتان)<br/>خلالها تُبدَّل كلّ العملاء إلى الإصدار الجديد"]
C["حالة يمكن فيها إقصاء التطبيق القديم<br/>بفحص الحدّ الأدنى للإصدار (الفقرة 5.2)"]
D["الإصدار B (contract: تقليص)<br/>نسخ / تحويل نهائيّ لأحدث قيم البنية القديمة إلى الجديدة<br/>تبديل القراءة إلى البنية الجديدة وحذف البنية القديمة"]
A --> B --> C --> D
الشكل 2: تُدرَج فترة تعايش بين expand وcontract. الجوهر ألّا يُصدَر contract حتّى يصبح إقصاء التطبيق القديم ممكناً.
| محتوى التغيير | ما يحدث إن أُجري دفعة واحدة | المرحلتان الآمنتان |
|---|---|---|
| إعادة تسمية عمود | التطبيق القديم والتقارير التي تشير إلى الاسم القديم تموت فوراً | الإجراء طويل فنبيّنه مفصّلاً أدناه |
| حذف عمود | INSERT/SELECT للتطبيق القديم يخطئ | expand: يتوقّف التطبيق عن الإشارة فقط (العمود يبقى) ← contract: الحذف بعد عدّة إصدارات |
| تغيير النوع أو المعنى (مثال: وقت محلّي ← UTC) | تختلط القيم الجديدة والقديمة في عمود واحد وتنكسر بهدوء | expand: إضافة عمود جديد ووضع القيم المحوَّلة. فترة التعايش كإعادة التسمية (التطبيق الجديد يكتب في الاثنين، والقراءة تعتبر العمود القديم المرجع) ← contract: بعد إقصاء التطبيق القديم، تحويل نهائيّ من العمود القديم ثمّ تبديل القراءة وحذف العمود القديم |
| إضافة قيد NOT NULL | يفشل التطبيق على صفوف NULL القائمة. كتابة NULL من التطبيق القديم أيضاً تنتهك القيد وتموت فوراً | expand: تجهيز قيمة افتراضيّة وتحديث كلّ العملاء إلى إصدار يكتب غير NULL ← contract: بعد إقصاء التطبيق القديم ملء NULL المتبقّي بـ UPDATE ثمّ إضافة القيد |
إعادة تسمية العمود كثيرة التفرّع ولا تسكن في خلية جدول واحدة. تفكيك الإجراء كالتالي.
- expand: إضافة العمود الجديد ونسخ قيم العمود القديم. نفّذ
ALTER TABLE ... ADD COLUMNوUPDATEفي رقم الترحيل نفسه. - فترة التعايش: التطبيق الجديد يكتب في الاثنين، والقراءة تعتبر العمود القديم المرجع. إبقاء جهة القراءة على العمود القديم لأنّه في قاعدة مشتركة يعمل فيها التطبيق القديم والجديد معاً لا يكتب التطبيق القديم إلّا في العمود القديم. إن قرأت العمود الجديد فاتتك تحديثات أدخلها التطبيق القديم. يمكن أيضاً مزامنة القديم ← الجديد بمحفّز في جهة قاعدة البيانات.
- إقصاء التطبيق القديم. بفحص الحدّ الأدنى للإصدار (الفقرة 5.2) تُجعَل حالة لا يستطيع فيها التطبيق القديم فتح تلك القاعدة. حتّى هنا لا نُقصي. في الخطوة 1 يرتفع
user_version، لكن رقم التوافق الأدنى يبقى، فيفتح التطبيق القديم القاعدة ويواصل الكتابة في العمود القديم. إن جمعتهما في القيمة نفسها، تختفي فترة التعايش في الخطوة 2 لحظة تطبيق الخطوة 1. - contract: نسخ نهائيّ لأحدث قيم العمود القديم إلى الجديد، ثمّ تبديل القراءة إلى العمود الجديد، ثمّ حذف العمود القديم. هذا الترتيب حاسم؛ إن بدّلت القراءة إلى العمود الجديد قبل الإقصاء، فاتتك تحديثات كتبها التطبيق القديم في العمود القديم فقط.
إصدار جهة contract (الحذف) آمن بعد أن يصبح إقصاء التطبيق القديم ممكناً بفحص الحدّ الأدنى للإصدار (الفقرة التالية).
ظرف خاصّ بـ SQLite: ALTER TABLE يدعم فقط تغيير اسم الجدول وتغيير اسم العمود وإضافة عمود وحذف عمود، وللحذف قيود كثيرة مثل «لا يجوز على عمود PRIMARY KEY أو قيد UNIQUE، ولا على عمود تشير إليه فهارس أو قيود CHECK أو مفاتيح خارجيّة أو عروض». ما عدا ذلك يُجرى بالإجراء الذي تحدّده الوثائق الرسميّة: «صنع جدول جديد داخل معاملة، ونقل البيانات بـ INSERT INTO new_X SELECT ... FROM X، ثمّ حذف الجدول القديم وإعادة التسمية».3 في الجداول الكبيرة يصير الأمر نسخاً لكلّ الصفوف، فاحسب زمن التطبيق ومساحة القرص الفارغة.
5.2 الدفاع ضدّ التخفيض ── فحص الحدّ الأدنى للإصدار
في تصميم الترحيل الأماميّ فقط لا نكتب سكربتات للاتّجاه الهابط (لا فرصة لاستخدامها لدى العميل، والشيفرة غير المختبرة خطر فقط). البديل اللازم هو آليّة تتوقّف عندما يفتح تطبيق بإصدار قديم قاعدة بيانات جديدة.
الجوهر هنا فصل «رقم المخطّط» عن «حدّ الإقصاء الأدنى». في شيفرة الفصل 3 نحمل اثنين:
user_version── رقم المخطّط الحاليّ. يرتفع عند كلّ تطبيق ترحيلmin_compatible_versionفيschema_meta── الحدّ الأدنى للتطبيق المسموح له بفتح هذه القاعدة. لا يُرفَع إلّا عند contract
والإقصاء يُحكَم بالأخير فقط. الإقصاء بـ user_version لا يقيم فترة التعايش في الفقرة 5.1. لحظة تطبيق التطبيق الجديد لـ expand يرتفع user_version، فإن كان الحكم «ارفض إن كان أكبر من أقصى ما تعرفه»، لا يستطيع التطبيق القديم فتح القاعدة من تلك اللحظة. عندها لا يعمل تصميم «فترة التعايش يكتب فيها الاثنان» أصلاً.
بالفصل يسير الأمر كالتالي.
| المرحلة | user_version |
min_compatible_version |
التطبيق القديم |
|---|---|---|---|
| بعد تطبيق الإصدار A (expand) | يرتفع | يبقى | يفتح. يواصل الكتابة في العمود القديم |
| انتشار تحديث التطبيق القديم | لا يتغيّر | يبقى | ── |
| بعد تطبيق الإصدار B (contract) | يرتفع | يُرفَع | لا يفتح. يتوقّف مع حثّ على التحديث |
من جهة التطبيق القديم يعمل في حالة «مخطّط برقم لا يعرفه، لكنّه معلَن متوافقاً». الافتراض عندها عدم لمس أعمدة لا يعرفها. لذلك تقتصر تغييرات جهة expand على إضافة أعمدة، دون تغيير معنى الأعمدة القائمة.
وإن قرّرت «لا تُدخل تغييراً مدمِّراً في إصدار يحتمل الإرجاع (expand فقط)»، فقراءة القاعدة الجديدة بالتطبيق القديم نفسها آمنة. يمكن أيضاً تخفيف الإقصاء إلى «تحذير وبدء للقراءة فقط». الاختيار بحسب مدى جواز توقّف العمل.
5.3 النسخ الاحتياطيّ التلقائيّ قبل التطبيق
الترحيل جراحة على «بيانات إنتاج على حاسوب شخص آخر». نؤتمت «خُذ نسخة احتياطيّة ثمّ نفّذ». في SQLite VACUUM INTO الأمثل، ويصنع من قاعدة تعمل أيضاً لقطة متّسقة في ملفّ آخر بجملة واحدة.2
// conn … 開いている SqliteConnection(第3章の Migrate に渡すのと同じもの)
// latest … Migrations の最後の番号(第3章の latest と同じ)
// backupDir … バックアップの置き場所。DB本体と同じフォルダーに置くと
// ディスク故障で同時に失うため、別ドライブや共有フォルダーを推奨
var backupDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp", "db-backup");
// 適用が必要なときだけ、直前に1世代バックアップを取る
if (GetUserVersion(conn) < latest)
{
Directory.CreateDirectory(backupDir);
// 前回の失敗で残った作業ファイルを、ここで実際に片づける。
// 残っていても普通は次回の邪魔をしない(名前に時刻が入るため)が、
// 消さない限りDB1個ぶんのゴミが失敗のたびに積み上がる。
// そして同じ秒に再実行されると名前が衝突し、VACUUM INTO は
// 「出力先が存在しない(または空)」を要求するのでそこで止まる。
//
// 消すのは「十分に古いもの」だけにする。このブロックは 6.2 のミューテックスの
// 内側で動かす前提だが、それでも別の版や別のツールが同じフォルダーを
// 使うことはあり、いま走っている実行の作業ファイルを消すと、
// その実行が File.Move の直前で FileNotFoundException になる
DateTime staleBefore = DateTime.UtcNow - TimeSpan.FromHours(1);
foreach (var stale in Directory.EnumerateFiles(backupDir, "*.db.tmp"))
{
try
{
if (File.GetLastWriteTimeUtc(stale) < staleBefore) { File.Delete(stale); }
}
catch (IOException) { } // 他プロセスが掴んでいる。次回に回す
catch (UnauthorizedAccessException) { }
}
var backupPath = Path.Combine(backupDir,
$"app_schema_v{GetUserVersion(conn)}_{DateTime.Now:yyyyMMdd_HHmmss}.db");
// 実行途中で電源断・プロセス強制終了が起きた不完全なファイルを
// 「完成したバックアップ」に見せないため、一時名で作り成功後にリネームする
var tempPath = backupPath + ".tmp";
using var cmd = conn.CreateCommand();
cmd.CommandText = "VACUUM INTO $path";
cmd.Parameters.AddWithValue("$path", tempPath);
cmd.ExecuteNonQuery(); // VACUUMはトランザクション外で実行する
File.Move(tempPath, backupPath);
}
شغّل هذه الكتلة داخل الحصر الذي نجهّزه في 6.2. النسخ الاحتياطيّ جزء من الترحيل، لا خطوة منفصلة. إن لم تُحَط «رؤية الإصدار ← أخذ النسخة ← التطبيق» كسلسلة واحدة، يصل عمليّتان إلى الحكم نفسه معاً في يوميّات تطبيق الأعمال: الجميع يشغّل التطبيق معاً أوّل الصباح. ينظّف أحدهما ملفّ عمل الآخر، فيحدث FileNotFoundException قبيل File.Move ── النسخة أُخذت صحيحاً لكنّ البدء وحده يفشل، شكل يصعب شرحه. حذف «ما هو قديم بما يكفي فقط» في الشيفرة أعلاه تأمين لتقليل الضرر عند نسيان الإحاطة. تأمين، وليس بديلاً عن الحصر.
تنظيف .tmp ليس «سأكتبه لاحقاً»، بل جزء من هذه المعالجة. إن انقطع VACUUM INTO بانقطاع الطاقة أو إنهاء العمليّة قسراً، يترك ملفّ إخراج ناقصاً وتالفاً.2 صنعه باسم مؤقّت فلا يبدو «نسخة مكتملة»، لكن ما لم يُحذَف يتراكم على حاسوب المستخدم قمامة بحجم قاعدة واحدة عند كلّ فشل. يأكل سعة وجهة النسخ بهدوء، ويضيف عند الاستعادة حكماً إضافيّاً «أيّها الأصليّ».
وVACUUM INTO يشترط ألّا يكون ملفّ الوجهة موجوداً (أو أن يكون فارغاً).2 التسمية أعلاه تتضمّن الوقت فلا يحدث تصادم عادة، لكن إن أعاد المستخدم تشغيل التطبيق الذي سقط في الترحيل فوراً (أو أعادت خدمة المراقبة التشغيل) يدخل في الثانية نفسها فيتطابق الاسم ويتوقّف هناك. عطل «فشل النسخ الاحتياطيّ فلا يمكن البدء»، أصعب الأشكال شرحاً.
إدخال رقم إصدار المخطّط في اسم الملفّ يجعل «إلى أين نرجع» واضحاً عند الاستعادة. تفاصيل النسخ الاحتياطيّ، مثل أنّ النسخ البسيط لملفّ قاعدة تعمل مرتع للتلف، في الفصل 7 من «استخدام SQLite في تطبيقات C# للأعمال». في SQL Server الشكل هو تنفيذ BACKUP DATABASE قبل التطبيق، والفكرة نفسها.
6. فخاخ التشغيل
6.1 الفشل في المنتصف والمعاملات ── اعرف فرق نظم إدارة قواعد البيانات
شيفرة الفصل 3 تحيط كلّ ترحيل بمعاملة واحدة، وتضمّن تحديث user_version في المعاملة نفسها. هذا يقوم لأنّ SQLite يستطيع تنفيذ DDL (CREATE TABLE وALTER TABLE ونحوهما) داخل معاملة، وإرجاعه عند الفشل. إجراء إعادة بناء الجدول الرسميّ نفسه تكوين «بدء معاملة، ثمّ CREATE/INSERT/DROP/RENAME، ثمّ تثبيت».3 حتّى إن انقطعت الطاقة في المنتصف، قاعدة البيانات عند البدء التالي حالة متّسقة «قبيل ذلك الترحيل».
SQL Server أيضاً يستطيع تنفيذ كثير من DDL داخل معاملة، لكن توجد استثناءات. مثلاً ALTER DATABASE لا يُستخدم داخل معاملة صريحة، وCREATE FULLTEXT INDEX أيضاً لا يُوضَع داخل معاملة مستخدم.4 وEF Core أيضاً يحيط كلّ ترحيل تلقائيّاً بمعاملة متى أمكن، وينصّ على أنّ «بعض العمليّات لا يمكن تنفيذها داخل معاملة بحسب قاعدة البيانات».8 قاعدة العمل: لا تخلط عمليّة لا تدخل المعاملة في الترحيل نفسه مع تغيير مخطّط عاديّ. عند تبديل نظام إدارة قواعد البيانات تحقّق حتماً من «هل يشارك DDL في المعاملة».
الحادث التقليديّ هو «تحديث الإصدار بمعاملة منفصلة». إن نجح جسم التغيير وسقط قبل تحديث الرقم، يُعاد تنفيذ الترحيل نفسه عند البدء التالي ويفشل البدء إلى الأبد بـ «الجدول موجود بالفعل». إن ضُمِّن تحديث الرقم في المعاملة نفسها لا يحدث هذا من حيث المبدأ.
6.2 بدء عدّة عمليّات معاً ── تسلسل التطبيق
تطبيق الأعمال برمجيّة «في الصباح يشغّلها الجميع دفعة واحدة». عملاء عدّة ينظرون إلى قاعدة مشتركة (SQL Server)، أو تشغيل متعدّد على الجهاز نفسه، قد يشغّلون الترحيل معاً.
- من EF Core 9 فصاعداً يأخذ
Migrate()تلقائيّاً قفلاً على قاعدة البيانات كلّها ويمنع التطبيق المتزامن (قبل ذلك لا توجد هذه الحماية). قفل موفّر SQLite منفَّذ بجدول قفل، وتلاحظ الوثائق رسميّاً احتمال بقاء الجدول إن انتهت العمليّة أثناء التطبيق على نحو غير طبيعيّ.7 إن تعذّر البدء مع انتظار القفل، بعد التأكّد من عدم وجود عمليّة أخرى تنفّذ الترحيل، يمكن الاستعادة بإسقاط جدول القفل المتبقّي (__EFMigrationsLock). - في التنفيذ الذاتيّ، لقاعدة محلّيّة التسلسل بـ Mutex مسمّى سهل.
// using System.Threading; (Mutex / AbandonedMutexException)
// conn … 開いている SqliteConnection。マイグレーションを走らせる前に
// 接続だけ済ませておき、Mutex を取ってから Migrate を呼ぶ
using var conn = new SqliteConnection(connectionString);
conn.Open();
// Global\ を付け、RDPやユーザー切り替えで複数のログオンセッションから
// 起動されてもPC全体で直列化されるようにする(Local\ は同一セッション内のみ)
using var mutex = new Mutex(false, @"Global\MyApp.SchemaMigration");
try
{
mutex.WaitOne();
}
catch (AbandonedMutexException)
{
// 前の所有プロセスがReleaseせずに異常終了したケース。
// 例外が出ても所有権自体は取得できているので、そのまま続行してよい。
// 適用が中途半端に終わった可能性には、この後のバージョン再確認と
// マイグレーションごとのトランザクションで備える
}
try
{
SchemaMigrator.Migrate(conn);
}
finally
{
mutex.ReleaseMutex();
}
الجانب المنتظِر، بعد الحصول على القفل، يتحقّق من الإصدار مرّة أخرى (شيفرة الفصل 3 تنظر version <= current قبل كلّ تطبيق)، فلا يحدث تطبيق مزدوج. كائنات Global\ المسمّاة تحمل افتراضيّاً ACL مستمدّاً من المستخدم الذي أنشأها، لذلك قد يحدث UnauthorizedAccessException عند فتح الـ Mutex نفسه من جلسة حساب Windows آخر. إن كان الاستخدام من حسابات عدّة مفترضاً، اصنعه مع منح مستخدمي الاستخدام حقوق المزامنة والتعديل عبر MutexAcl في System.Threading.AccessControl، أو مِل إلى قفل جهة قاعدة البيانات المذكور تالياً. في قاعدة مشتركة لا يعبر Mutex الآلات، فمِل إلى التسلسل في جهة قاعدة البيانات: «إتمام التطبيق في جهة الخادم قبل توزيع التحديث»، أو «أخذ قفل جهة قاعدة البيانات عند بدء التطبيق (BEGIN IMMEDIATE في SQLite، أو قفل تطبيق في SQL Server)».
6.3 البروفة ── اختبار تطبيق دفعة واحدة من «أقدم قاعدة»
خلل الترحيل لا يُكتشَف أوّلاً على جهاز التطوير. قاعدة جهاز التطوير دائماً بأحدث مخطّط، والبيانات نظيفة. ما ينكسر هو قاعدة العميل القديمة، الكبيرة، وفيها بيانات غير متوقَّعة. ثلاثة أمور على الأقلّ قبل الإصدار.
- احفظ ملفّ قاعدة لكلّ إصدار مخطّط كمثبت اختبار، وأتمت اختبار تطبيق دفعة واحدة من كلّ منها إلى الأحدث. أنماط القفز مثل «من v1 إلى v5» و«من v3 إلى v5» هي واقع العميل. في SQLite يكفي وضع ملفّ القاعدة في المستودع، فالاختبار من النوع السهل الكتابة.
- جرّب بكمّ ونوع يعادلان البيانات الفعليّة. عمود مليء بـ NULL، تكرار غير متوقَّع، زمن إعادة البناء في جدول ضخم (الفقرة 5.1) لا يظهر ما لم تقترب البيانات من الحقيقيّ. إن أمكن، أعدّ بروفة على قاعدة عميل مجهولة الهويّة.
- جرّب مسار الفشل. اقتل العمليّة في منتصف التطبيق، وتأكّد من الاستعادة الصحيحة عند البدء التالي (إعادة التطبيق من الإصدار الذي أُرجع).
6.4 إجراءات التحقّق في الميدان والإرجاع
بعد إدخال الآليّة، اكتب كإجراء «هل نجح» و«ماذا نفعل إن فشل». ستأتي حتماً لحظة توجيه العامل عبر الهاتف، فالشكل العمليّ هو أن يكون كأوامر.
التحقّق من نتيجة التطبيق. إن وُجد صدفة سطر الأوامر الرسميّة لـ SQLite (sqlite3)، تُقرأ رقم إصدار المخطّط الحاليّ بسطر واحد. ما يُرجع عدد صحيح واحد.
sqlite3 "C:\ProgramData\MyApp\app.db" "PRAGMA user_version;"
كثيراً ما يتعذّر وضع sqlite3.exe على حاسوب العميل، لذلك عرض إصدار المنتج وإصدار المخطّط كليهما في شاشة معلومات إصدار التطبيق يمكّن من التحقّق من الحالة بمكالمة واحدة. مجرّد استدعاء GetUserVersion في الفصل 3 كما هو. في SQL Server يؤدّي SELECT MAX(version) FROM schema_version; الدور نفسه.
الإرجاع عند الفشل. نسخة الفقرة 5.3 تُرجَع بالإجراء التالي.
- أنهِ التطبيق تماماً. الكلّ، بما فيه التشغيل المتعدّد والأجهزة الأخرى التي تنظر إلى القاعدة نفسها.
- أبعِد الأصل. انقل ملفّ القاعدة الحاليّ، وفي وضع WAL ملفّات
-wal/-shmالتي تحمل الاسم نفسه أيضاً، إلى مجلّد آخر. أبقِه ولا تحذفه. يلزم لتحقيق السبب. - انسخ ملفّ النسخة الاحتياطيّة بالاسم الأصليّ. لأنّ الفقرة 5.3 أدخلت رقم إصدار المخطّط في اسم الملفّ، يُعرَف من الاسم إلى أيّ نقطة نرجع.
- شغّل التطبيق وتأكّد من أنّ
PRAGMA user_versionهو الرقم المراد. بعد ذلك واصل التشغيل بتطبيق الإصدار القديم إلى أن يُوزَّع إصدار أصلحت فيه السبب.
وجود هذا الإجراء مكتوباً أو لا يغيّر زمن الاستعادة يوم العطل. في الإصدار نفسه الذي فيه تنفيذ الترحيل، أضف صفحة أيضاً إلى دليل التشغيل.
7. الخلاصة
- «قاعدة البيانات تختلف من عميل لآخر» ليست انتباه المسؤول، بل نتيجة بنيويّة لـ تشغيل ينفّذ فيه أشخاص دليل SQL. في تطبيق أعمال سطح مكتب تتوزّع فيه القواعد، لا خيار سوى أن يحدّث التطبيق نفسه قاعدة بياناته.
- الهيكل هو رقم إصدار مخطّط تحمله قاعدة البيانات نفسها (
PRAGMA user_versionفي SQLite1) وتطبيق ترحيلات أماميّة مرقَّمة عند بدء التشغيل. بلغة C# يكفي تنفيذ ذاتيّ بعشرات الأسطر. - الوسائل ثلاث سلاسل: EF Core Migrations، ومكتبات مثل DbUp، والتنفيذ الذاتيّ. الاختيار بحسب ما إذا كنت تستخدم EF Core بالفعل، وكم أصل SQL الخام لديك (جدول قرار الفصل 4). لـ
Migrate()عند بدء EF Core نقاط انتباه رسميّة،7 فاستخدمه مع مواجهة التنفيذ المتزامن والبروفة. - التغييرات المدمِّرة تُجرى بـ إصدار على مرحلتين expand-contract، وحادث فتح التطبيق القديم لقاعدة جديدة يُوقَف بـ فحص الحدّ الأدنى للإصدار. قيود ALTER TABLE في SQLite وإجراء إعادة البناء يتبعان الوثائق الرسميّة.3
- المبدأ ترحيل واحد = معاملة واحدة، وتحديث الإصدار أيضاً في المعاملة نفسها. لـ SQL Server DDL لا يدخل المعاملة،4 فتُفصَل العمليّات الاستثنائيّة. لا يكون الترحيل «قابلاً للشحن إلى العميل» إلّا مع نسخة
VACUUM INTOقبل التطبيق2 وبروفة تطبيق دفعة واحدة من أقدم إصدار.
إن كان لديك أثر لتشغيل ALTER بالدليل، جرّب في الإصدار التالي إدخال «تسجيل رقم الإصدار» و«التطبيق عند البدء» وحدهما. إن وُجد الأساس، يمكن إضافة الإصدار على مرحلتين والنسخ الاحتياطيّ شيئاً فشيئاً لاحقاً.
مقالات ذات صلة
- استخدام SQLite في تطبيقات C# للأعمال ── وضع WAL، والتحكّم الحصريّ، والوقاية من التلف، والتمييز عن EF Core
- كيف تختار مكان حفظ بيانات تطبيق Windows محليّاً ── جدول قرار بين SQLite وJSON وRegistry وAccess
- ليس appsettings.json وحده ── إدارة الإعدادات عمليّاً في تطبيقات Windows للأعمال
- التاريخ والوقت والمناطق الزمنية في تطبيقات الأعمال ── من فخاخ DateTime إلى مبدأ التخزين بـ UTC وتصميم الاختبارات
مجالات الاستشارة ذات الصلة
تتولّى شركة كومورا سوفت ذ.م.م. تصميم قواعد بيانات تطبيقات الأعمال المثبَّتة لدى العملاء وإدخال بنية الترحيل، وتحقيق المخطّطات التي تفرّقت بتشغيل الدليل وتطبيعها، وتصميم توزيع التحديث في تكوينَي EF Core وSQL الخام كلٍّ على حدة.
- تطوير تطبيقات Windows
- تعديل وصيانة برمجيّات Windows القائمة
- الاستشارة التقنيّة ومراجعة التصميم
- الاتّصال بنا
روابط مرجعية
-
SQLite, Pragma statements supported by SQLite - user_version. حول أنّ user_version عدد صحيح يُخزَّن في ترويسة قاعدة البيانات (الإزاحة 60)، ومُعَدّ ليستخدمه التطبيق بحريّة، وأنّ SQLite نفسه لا يستخدم هذه القيمة. ↩ ↩2 ↩3
-
SQLite, VACUUM. حول أنّ VACUUM INTO لا يغيّر القاعدة الأصليّة، ويستطيع صنع لقطة متّسقة لقاعدة تعمل في ملفّ آخر، ويمكن استخدامه بديلاً عن واجهة النسخ الاحتياطيّ. ومع ذلك متطلّب «الملفّ المحدَّد في جملة INTO يجب ألّا يكون موجوداً مسبقاً، أو أن يكون ملفّاً فارغاً. وإلّا يفشل أمر VACUUM INTO بخطأ»، ووصف «لكن إن انقطع أمر VACUUM INTO بإيقاف غير مخطَّط أو فقدان طاقة، فقد تكون قاعدة الإخراج المولَّدة ناقصة وتالفة». ↩ ↩2 ↩3 ↩4 ↩5
-
SQLite, ALTER TABLE. حول اقتصار ALTER TABLE في SQLite على تغيير اسم الجدول وتغيير اسم العمود وإضافة عمود وحذفه، ووجود قيود كثيرة على حذف العمود، وأنّ سائر تغييرات المخطّط تُنفَّذ بالإجراء الرسميّ داخل معاملة: صنع جدول جديد ثمّ نسخ البيانات ثمّ حذف الجدول القديم ثمّ إعادة التسمية. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, ALTER DATABASE (Transact-SQL) وCREATE FULLTEXT INDEX (Transact-SQL). حول أنّ ALTER DATABASE يلزم تنفيذه في وضع التثبيت التلقائيّ ولا يُسمح به داخل معاملة صريحة أو ضمنيّة، وأنّ CREATE FULLTEXT INDEX لا يُوضَع داخل معاملة مستخدم. ↩ ↩2 ↩3
-
DbUp, DbUp Documentation وSupported Databases. حول أنّها مكتبة .NET تساعد على نشر التغييرات إلى قاعدة بيانات SQL Server، وتسجّل سكربتات SQL المنفَّذة وتنفّذ غير المنفَّذ فقط، وأنّها تدعم أيضاً SQLite وPostgreSQL وMySQL وغيرها. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, SQLite EF Core Database Provider Limitations. حول أنّ كثيراً من عمليّات الترحيل في موفّر SQLite تُنفَّذ كإعادة بناء جدول، وأنّه لا يُولَّد سكربت متماثل. ↩ ↩2
-
Microsoft Learn, Applying Migrations (EF Core). حول الأسباب الخمسة التي تجعل تطبيق الترحيل وقت التشغيل (عند البدء) غير مناسب لإدارة قاعدة بيانات الإنتاج، والتوصية بتوليد سكربت SQL، وعدم الجمع بين EnsureCreated() وMigrate()، وأخذ Migrate() من EF Core 9 فصاعداً قفلاً على قاعدة البيانات كلّها تلقائيّاً، وأنّ قفل موفّر SQLite منفَّذ بجدول وقد يبقى عند الانتهاء غير الطبيعيّ. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Managing Migrations (EF Core). حول أنّ EF Core يحيط كلّ ترحيل تلقائيّاً بمعاملة عند التطبيق متى أمكن، وأنّ بعض العمليّات لا يمكن تنفيذها داخل معاملة بحسب قاعدة البيانات. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
إلى متى ستستمر تطبيقات VB6 في العمل ── وضع دعم بيئة التشغيل والمسار العملي للترحيل إلى .NET
إلى متى تستمر تطبيقات VB6 في العمل؟ نرتّب وضع بيئة التشغيل المشمولة حتى في Windows 11 وانتهاء دعم بيئة التطوير، وجدول القرار بين إعادة ال...
CI/CD عمليّ لتطبيقات WinForms / WPF ── أتمتة البناء والتوقيع والتوزيع عبر GitHub Actions
دليل عمليّ لبناء CI/CD لتطبيقات WinForms / WPF عبر GitHub Actions. يشمل YAML الأدنى للبناء والاختبار على windows-latest، وترقيم الإصدار ا...
التعديل الآمن على تطبيق أعمال قديم بلا اختبارات ── ممارسة اختبار التوصيف وإعادة الهيكلة
لإجراء تعديلات آمنة على تطبيق أعمال بلا اختبارات، نشرح خطوات اختبار التوصيف (أسلوب Golden Master) الذي يثبّت السلوك الحالي، وكيفيّة صنع ن...
كيف تختار مكان حفظ بيانات تطبيق Windows محليّاً ── جدول قرار بين SQLite وJSON وRegistry وAccess
أين ينبغي حفظ بيانات تطبيق سطح مكتب Windows، وبأيّ صيغة؟ نرتّب الفرق بين AppData وProgramData، ونقاط قوّة وعيوب كلّ من SQLite وملفّات JSO...
قائمة التحقّق قبل ترحيل .NET Framework إلى .NET
قائمة تحقّق عمليّة قبل ترحيل .NET Framework إلى .NET: نوع المشروع، والتقنيات غير المدعومة، وتبعيّات NuGet، وأسلوب SDK، و WPF/WinForms، و ...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- كيف ينبغي إدارة تغييرات مخطّط قاعدة بيانات تطبيقات الأعمال؟
- بدلاً من أن ينفّذ أشخاص دليل إجراءات SQL يدويّاً، ينبغي تضمين ترحيلات مرقَّمة (شيفرة تغيير المخطّط) داخل التطبيق نفسه، وتطبيقها تلقائيّاً عند بدء التشغيل. تسجّل قاعدة البيانات نفسها رقم إصدار مخطّطها الحاليّ (PRAGMA user_version في SQLite، وجدول مخصَّص في SQL Server)، ويطبّق التطبيق الأرقام غير المطبَّقة فقط بالترتيب ضمن معاملة. بهذا الشكل، حتّى عند تحديث يقفز من v1.2 إلى v1.5 تُطبَّق كلّ تغييرات المخطّط الوسيطة، ولا تحدث بنيويّاً حالة «اختلاف شكل قاعدة البيانات من عميل لآخر».
- هل يجوز استدعاء Migrate() الخاصّة بـ EF Core عند بدء تشغيل التطبيق؟
- خيار واقعيّ بشروط. تنبّه وثائق Microsoft إلى التطبيق عند بدء التشغيل في بيئة الإنتاج، لأسباب منها التطبيق المتزامن من عدّة نُسَخ، ومنح التطبيق صلاحيّة تغيير المخطّط، وتعذّر مراجعة SQL مسبقاً، وتوصي في تطبيقات الخادم بالتطبيق عبر توليد سكربت SQL. أمّا في تطبيقات سطح المكتب للأعمال التي تملك قاعدة بيانات محلّيّة لكلّ جهاز عميل، فإنّ تشغيل السكربت ميدانيّاً غير قابل للتطبيق عمليّاً، فتصبح Migrate() عند بدء التشغيل الحلّ المعياريّ فعليّاً. حتّى في هذه الحالة، اجمع دائماً بين إجراء مواجهة التشغيل المتزامن (القفل التلقائيّ من EF Core 9 فصاعداً، أو Mutex ذاتيّ) والنسخ الاحتياطيّ قبل التطبيق.
- ماذا يحدث لقاعدة البيانات إن فشل الترحيل في منتصف تنفيذه؟
- إن أحطت كلّ ترحيل بمعاملة واحدة، وضمّنت تحديث رقم الإصدار في المعاملة نفسها، فسيتراجع عند الفشل إلى الحالة السابقة لبدء ذلك الترحيل، ولن يتبقّى مخطّط منتصف التنفيذ. يمكن لـ SQLite تنفيذ DDL مثل CREATE TABLE وALTER TABLE ضمن معاملة أيضاً، والإجراء الرسميّ لإعادة بناء الجدول نفسه مكتوب على افتراض وجود معاملة. يستطيع SQL Server أيضاً تنفيذ كثير من DDL ضمن معاملة، لكن توجد استثناءات مثل ALTER DATABASE والفهرسة النصّيّة الكاملة، لذا افصل العمليّات الاستثنائيّة في ترحيل مستقلّ. كما أنّ وجود نسخة احتياطيّة تلقائيّة قبل التطبيق يجعل الاستعادة ممكنة حتّى في أسوأ الحالات باستبدال الملفّ.
- إذا كان مخطّط قاعدة البيانات مختلفاً بالفعل من عميل لآخر، كيف نطبّعه؟
- حدّد أوّلاً «المخطّط المرجعيّ الصحيح» الواحد، ثمّ افحص قاعدة بيانات كلّ عميل واستخرج الفروق عن الحالة الراهنة. بعد ذلك، اكتب لقاعدة البيانات التي لا تحمل رقم إصدار ترحيلاً أوّليّاً يكتشف الأنماط الفعليّة الموجودة ويوحّدها إلى الصيغة القياسيّة، وسجّل رقم الإصدار عند اكتمال ذلك الترحيل. في SQLite يمكن تحديد وجود عمود من عدمه آليّاً عبر sqlite_master أو PRAGMA table_info، ويمكن استيعاب الاختلاف بـ SQL دفاعيّ على شكل «أضِف العمود إن لم يكن موجوداً». إن وضعت كلّ التغييرات اللاحقة ضمن ترحيلات مرقَّمة، فلن يتكرّر التفاوت.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.