كيف تختار مكان حفظ بيانات تطبيق Windows محليّاً ── جدول قرار بين SQLite وJSON وRegistry وAccess

· آخر تحديث: · · SQLite, Windows, .NET, C#, تخزين البيانات, Registry, Access, التصميم, جدول القرار, الاستشارات التقنية

«هل يكفي استخدام ملفّ INI للإعدادات؟» «بيانات السجلّ التاريخيّ تزايدت، أريد نقلها إلى Access» «كيف يُفاضَل بين Registry وملفّ الإعدادات؟» ── عند بناء تطبيقات أعمال لِـ Windows، يمثّل اختيار مكان حفظ البيانات طريقاً لا مفرّ من المرور به. لكن هذا الاختيار غالباً ما يُحسَم في البداية بشكل عفويّ ثمّ لا يُعاد النظر فيه، فتظهر المشكلة بعد سنوات على هيئة «ملفّ JSON تضخّم إلى عشرات الميغابايتات فبطُؤ بدء التشغيل»، أو «ملفّ Access في مجلّد مشترك يتلف مرّة في الأسبوع»، أو «الكتابة مباشرةً تحت Program Files تمنع التشغيل في بيئة Windows 11».

في هذا المقال، نرتّب حفظ البيانات المحليّة لتطبيقات أعمال Windows بفصل «أين توضَع» (اختيار المجلّد) عن «بماذا تُحفَظ» (اختيار الصيغة/المحرّك). وباستخدام صيغة جدول القرار التي استخدمناها عدّة مرّات في هذه المدوّنة، نلخّص مجالات تفوّق وعيوب كلّ من SQLite وJSON وRegistry وAccess.

1. الخلاصة أوّلاً

  • اختيار مكان الحفظ قرارٌ مستقلٌّ ذو شقّين: «أين توضَع» و«بماذا تُحفَظ». الخطأ في الأوّل يؤدّي إلى مشكلات الصلاحيّات وتعدّد المستخدمين، والخطأ في الثاني يؤدّي إلى مشكلات التلف والأداء والصيانة.
  • أساس اختيار المكان: لبيانات وإعدادات كلّ مستخدم على حدة استخدم %LOCALAPPDATA% (Environment.SpecialFolder.LocalApplicationData)، ولمشاركة جميع المستخدمين استخدم %PROGRAMDATA%، ولا تكتب أبداً في المجلّد نفسه الذي يوجد فيه ملفّ exe (تحت Program Files).1
  • الخيار الأوّل للصيغة بسيط ويتلخّص في اثنين: الإعدادات الصغيرة المهيكَلة تُحفَظ في ملفّ JSON، والبيانات المتنامية للأعمال والسجلّ التاريخيّ والبيانات التي تحتاج بحثاً تُحفَظ في SQLite. وبهذين الاثنين وحدهما تُغطّى غالبيّة حالات الحفظ المحلّيّ لتطبيقات الأعمال.2
  • الـ Registry هو «مكان لوضع أعلام صغيرة أو معلومات تكامل مع Windows نفسه»، وليس مخزن بيانات للتطبيق. استخدامه دون فهم إعادة التوجيه بين 32-bit و64-bit (Wow6432Node) يوقعك في مشكلة «القيمة التي كتبتها لا تظهر».3
  • لم يعد هناك سبب يُذكَر لاختيار Access (.accdb) كمخزن بيانات في تطوير جديد. وحتّى عند استخدامه للتكامل مع أصول قائمة، يلازمه قيد على مستوى التوزيع، وهو ضرورة تطابق عدد بتّات موفّر ACE.4
  • أيًّا كانت الصيغة، المعلومات السرّيّة (كلمات المرور، مفاتيح API) وحدها تُعامَل معاملةً مختلفة. لا تضعها كنصّ صريح في JSON أو Registry، بل احمِها بواسطة DPAPI. للتفاصيل راجع المقال «حفظ المعلومات السرّيّة في تطبيقات Windows - تجنّب الإعدادات النصّيّة الصريحة عبر DPAPI».

2. تصنيف البيانات المطلوب حفظها إلى 4 أنواع

قبل تحديد بماذا تُحفَظ، نصنّف طبيعة البيانات التي نريد حفظها. تنقسم البيانات المحليّة لتطبيقات الأعمال عادةً إلى الأنواع الأربعة التالية.

التصنيف مثال الخصائص
الإعدادات جهة الاتّصال، تخطيط الشاشة، آخر مجلّد تمّ فتحه صغيرة الحجم. تُقرَأ بالكامل عند بدء التشغيل. قد يريد المستخدم تحريرها مباشرةً أحياناً
بيانات الأعمال والسجلّ التاريخيّ نتائج القياس، سجلّ المعالجة، نسخة محلّيّة من البيانات الرئيسيّة تتزايد باستمرار. يُراد البحث فيها وتجميعها. تلفها يُحدِث أثراً كبيراً على العمل
التخزين المؤقّت (Cache) الصور المصغّرة، الموارد التي جرى تنزيلها يمكن إعادة توليدها إن فُقدت. تحتاج إدارة للسعة
المعلومات السرّيّة كلمات المرور المحفوظة، الرموز المميّزة (tokens) كمّيّتها قليلة. يجب عدم وضعها كنصّ صريح

جوهر هذا المقال هو أنّ مكان الوضع والصيغة المناسبَين يختلفان باختلاف كلّ تصنيف. والتطبيقات التي «تضع الإعدادات والسجلّ التاريخيّ كلّها في ملفّ XML واحد» يكون أوّل خطوة للتحسين لديها هو إعادة هذا التصنيف من جديد.

3. أين توضَع ── أساسيّات اختيار المجلّد

في .NET، اجعل الأساس هو المواقع التي يمكن الحصول عليها عبر Environment.GetFolderPath.5

المكان طريقة الحصول عليه الاستخدام
%LOCALAPPDATA%\اسم الشركة\اسم التطبيق SpecialFolder.LocalApplicationData الخيار الافتراضيّ لبيانات كلّ مستخدم. ابدأ من هنا
%APPDATA%\اسم الشركة\اسم التطبيق (Roaming) SpecialFolder.ApplicationData فقط للإعدادات التي يُراد أن تتبع المستخدم في بيئة ملفّات التعريف المتنقّلة (Roaming Profiles)
%PROGRAMDATA%\اسم الشركة\اسم التطبيق SpecialFolder.CommonApplicationData بيانات مشتركة بين جميع المستخدمين. تحتاج تصميم ACL
تحت مجلّد المستندات SpecialFolder.MyDocuments فقط للمخرجات التي يتعامل معها المستخدم بوصفها ملفّاته الخاصّة (كالتقارير المُصدَّرة مثلاً)

هذا كلّ ما يلزم في الشيفرة، لكنّ جعل إدراج طبقة «اسم الشركة\اسم التطبيق» وإنشاء المجلّد عند أوّل مرّة جزءاً من معالجة مشتركة يمنع تكاثر أماكن الحفظ بشكل عشوائيّ.

public static class AppPaths
{
    public static string DataDir { get; } = CreateDir(
        Environment.SpecialFolder.LocalApplicationData);

    private static string CreateDir(Environment.SpecialFolder root)
    {
        var dir = Path.Combine(
            Environment.GetFolderPath(root), "KomuraSoft", "MyApp");
        Directory.CreateDirectory(dir);  // لا تفعل شيئاً إن كان موجوداً بالفعل
        return dir;
    }
}

استخدام Environment.GetFolderPath بدل تجميع متغيّر البيئة %LOCALAPPDATA% بربط النصوص هو لأنّه يُعيد المكان الصحيح حتّى مع حساب خدمة، أو التشغيل بمستخدم آخر، أو بيئة تمّ فيها ضبط إعادة توجيه المجلّدات. وهذا يتجنّب أيضاً حادثة «تغيّر المسار بمجرّد التشغيل بحساب مختلف من جدولة المهام (Task Scheduler)» (وهي أحد أشكال مشكلة «يعمل يدويّاً فقط» التي وردت في الفصل 5 من مقال جدولة المهام).

نذكر ثلاثة مطبّات:

  • لا تكتب في المجلّد الذي يوجد فيه exe. لا يمكن للمستخدم القياسيّ الكتابة تحت Program Files. وفي التطبيقات القديمة ذات 32-bit، قد تُعاد التوجيه بصمت إلى VirtualStore بفضل ميزة توافق UAC، ما يسبِّب عرضاً غريباً وهو «اختلاف محتوى ملفّ الإعدادات بين التشغيل كمدير والتشغيل كمستخدم قياسيّ».
  • ProgramData «يمكن الكتابة فيه لكنّه ليس آمناً». قد تكون قوائم التحكّم بالوصول (ACL) الافتراضيّة بحيث لا يستطيع مستخدم تعديل ملفّ أنشأه مستخدم آخر. إن أردت القراءة والكتابة بمشاركة جميع المستخدمين، أنشئ المجلّد عبر المثبِّت واضبط ACL صراحةً.
  • لا تجعل Roaming الخيار الافتراضيّ. في بيئة ملفّات التعريف المتنقّلة الخاضعة لنطاق (domain)، تتمّ مزامنة ما تحت Roaming عند تسجيل الدخول والخروج. وضع بيانات كبيرة الحجم أو خاصّة بالجهاز (كالتخزين المؤقّت أو إعدادات العتاد) في Roaming يسبِّب تأخّر المزامنة أو «تلوّث» جهاز آخر. عند التردّد، اختر Local.

4. بماذا تُحفَظ ── طبيعة الخيارات الأربعة

4.1 ملفّ JSON ── الخيار الأوّل للإعدادات

يمكن قراءته وكتابته ببساطة عبر System.Text.Json، وهو قابل للقراءة من قبل الإنسان، ويسهل إدارته عبر Git ومقارنة الفروقات (diff) — وهذه كلّها مزايا تجتمع لصالح استخدامه في الإعدادات. توجد نقطتان تستحقّان الانتباه.

اتّخذ إجراءً وقائيّاً ضدّ التلف. إذا انقطعت الطاقة أثناء الكتابة وبقي ملفّ ناقص، يصبح غير قابل للقراءة عند التشغيل التالي. الأسلوب المتعارف عليه هو «الكتابة إلى ملفّ مؤقّت ثمّ الاستبدال»، وفي .NET يوفّر File.Replace استبدالاً مصحوباً بنسخة احتياطيّة.

var json = JsonSerializer.Serialize(settings, options);
var tmp = path + ".tmp";
File.WriteAllText(tmp, json);
if (File.Exists(path))
    File.Replace(tmp, path, path + ".bak");
else
    File.Move(tmp, path);

تضمين سلوك تدهور تدريجيّ من البداية في جانب القراءة أيضاً — أي «إن كان تالفاً جرِّب .bak، وإن فشل ذلك أيضاً ابدأ التشغيل بالقيم الافتراضيّة مع تحذير» — يمنع تلف ملفّ الإعدادات من أن يتحوّل إلى حالة دعم فنّيّ.

لا تجعله مخزن بيانات. نطاق تطبيق JSON هو الحجم الذي يصحّ فيه «قراءة كلّ شيء عند البدء وكتابة كلّ شيء عند الإنهاء» (كمعيار تقريبيّ: حتّى بضع مئات من الكيلوبايتات). إن بدأتَ بوضع سجلّ تاريخيّ يستمرّ بالإضافة إليه أو بيانات تحتاج البحث في السجلّات داخل JSON، فتلك إشارة للانتقال إلى SQLite التالي.

4.2 SQLite ── الخيار الأوّل للبيانات المتزايدة والبيانات المطلوب البحث فيها

SQLite قاعدة بيانات مضمَّنة (embedded) لا تحتاج خادماً، وتتكوّن من ملفّ واحد، وهي في النطاق العامّ (public domain)، ويمكن التعامل معها من .NET عبر موفّر ADO.NET الذي تصونه Microsoft وهو Microsoft.Data.Sqlite، أو عبر موفّر SQLite الخاصّ بـ EF Core.2 وتوصي بها Microsoft نفسها بوصفها وسيلة لحفظ البيانات المحلّيّة في تطبيقات Windows6، لذا فمن المقبول اعتبار SQLite الخيار الأوّل لأيّ «بيانات مهيكَلة تتزايد محلّيّاً».

أوّلاً، نوضّح بالشيفرة مدى سهولة استخدامه. بإضافة Microsoft.Data.Sqlite عبر NuGet، لا حاجة إلى إعداد خادم ولا شاشة إدارة لسلسلة الاتّصال، ويكفي تحديد مسار الملفّ للبدء في الاستخدام.

using Microsoft.Data.Sqlite;

var dbPath = Path.Combine(AppPaths.DataDir, "app.db");
using var conn = new SqliteConnection($"Data Source={dbPath}");
conn.Open();

// المرّة الأولى فقط: تفعيل وضع WAL وإنشاء الجداول
using (var cmd = conn.CreateCommand())
{
    cmd.CommandText = """
        PRAGMA journal_mode=WAL;
        CREATE TABLE IF NOT EXISTS measurement (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            device_id  TEXT    NOT NULL,
            value      REAL    NOT NULL,
            created_at TEXT    NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS ix_measurement_device
            ON measurement(device_id, created_at);
        """;
    cmd.ExecuteNonQuery();
}

// الإدراج يتطلّب معاملات إلزاميّة (لا تبنِ SQL عبر ربط النصوص)
using (var cmd = conn.CreateCommand())
{
    cmd.CommandText =
        "INSERT INTO measurement (device_id, value) VALUES ($device, $value)";
    cmd.Parameters.AddWithValue("$device", "CAM-01");
    cmd.Parameters.AddWithValue("$value", 23.5);
    cmd.ExecuteNonQuery();
}

ستلاحظ أنّ الجهد المبذول لا يختلف كثيراً عن «الإضافة المستمرّة إلى ملفّ JSON»، بينما تحصل في المقابل على بحث وتجميع مفهرَسَين وسجلّ تاريخيّ بلا حدّ لعدد السجلّات. وإن أردت إقحام ORM، فموفّر SQLite الخاصّ بـ EF Core يُبنى فوق هذه المكتبة.

وفوق ذلك، نذكر أهمّ النقاط العمليّة.

  • فعِّل وضع WAL. وهو PRAGMA journal_mode=WAL; في الشيفرة أعلاه. يرفع تزامن القراءة والكتابة، فلا يحدث اختناق بسهولة حتّى في تصميم يتعامل فيه خيط واجهة المستخدم والمعالجة الخلفيّة مع نفس القاعدة. إعداد WAL يُحفَظ بشكل دائم في ملفّ قاعدة البيانات نفسه، لذا لا حاجة لتفعيله في كلّ اتّصال.
  • اجعل الكتابة من خيط واحد لكلّ عمليّة. كتابة SQLite حصريّة على مستوى قاعدة البيانات. إن كنتَ ستكتب من عدّة خيوط، فالتصميم الآمن هو تمرير الكتابة عبر طابور (queue) وتوحيدها في دور كتابة واحد. كذلك، عند إجراء عدد كبير من عمليّات INSERT الصغيرة، يصبح الأداء أسرع بمراتب إذا جُمِعت داخل معاملة (transaction) صريحة بدل الالتزام (commit) لكلّ سجلّ على حدة.
  • لا تضعه في مشاركة شبكيّة. قفل الملفّات عبر SMB كثير المشكلات المرتبطة بالبيئة، وحتّى مشروع SQLite الرسميّ يذكر المشاركة على أنظمة الملفّات الشبكيّة كأوّل سبب للتلف.7 إن أردتَ استخدامه من عدّة أجهزة ومستخدمين في آنٍ واحد، فذلك يدخل في نطاق قاعدة بيانات من نوع عميل-خادم (كـ SQL Server Express).
  • اعلم أنّ الأنواع أربعة فقط. يتألّف جوهر SQLite من INTEGER / REAL / TEXT / BLOB، وتُحفَظ التواريخ وقيم GUID كـ TEXT. مراجعة قواعد تخطيط الأنواع في Microsoft.Data.Sqlite مرّة واحدة تجنّبك الحيرة عند مقارنة التواريخ وترتيبها.8 وسبب ضبط created_at في المثال أعلاه بـ datetime('now') (بتوقيت UTC) هو أنّ خلط التوقيت المحلّي يسبِّب مشكلات عند الترتيب وعند تقاطع التوقيت الصيفيّ. تحويلها إلى التوقيت المحلّيّ عند العرض هو النهج الآمن.
  • النسخ الاحتياطيّ عبر VACUUM INTO أو Backup API لا عبر «نسخ الملفّ». نسخ ملفّ قاعدة البيانات نسخاً بسيطاً أثناء التشغيل قد يلتقط تضارباً بين WAL والملفّ الأساسيّ (التفاصيل في الفصل 6).

4.3 Registry ── فقط للأعلام الصغيرة ومعلومات التكامل مع Windows

يكون Registry مناسباً لمعلومات التكامل مع Windows نفسه — مثل «هل التطبيق مثبَّت أم لا»، و«التسجيل في بدء التشغيل»، و«ربط الملفّات» — وكذلك لإعدادات مستخدم صغيرة جدّاً فقط. المبدأ هو أن توضَع الإعدادات التي يستخدمها التطبيق نفسه تحت HKCU، وأن يكتب المثبِّت المعلومات المشتركة بين الجهاز تحت HKLM (تجنَّب تصميماً يكتب إلى HKLM أثناء التشغيل، لأنّه سيتطلّب صلاحيّات المدير).

أكبر مطبّ هو عدد البتّات. في Windows 64-bit، يُعاد توجيه HKLM\Software كما تراه عمليّة 32-bit إلى HKLM\Software\Wow6432Node.3 والأعراض مثل «القيمة موجودة عند النظر عبر محرِّر Registry لكن لا يمكن للتطبيق قراءتها» أو «القيمة التي كتبها تطبيق 32-bit لا تظهر من أداة صيانة 64-bit» سببها هذا غالباً. تظهر هذه المشكلة عادةً عند الانتقال إلى AnyCPU أو عند التحوّل إلى 64-bit، لذا احرص على فهمها في السياق نفسه لمشكلة 32-bit/64-bit في COM وActiveX («مطبّات COM/OCX/ActiveX ── عدد بتّات Visual Studio وصلاحيّات المدير»).

إن احتجتَ حتماً من .NET لقراءة عرض (view) ببتّات مختلفة (كتطبيق ما زال يُصان بـ32-bit ويريد قراءة قيمة مسجَّلة في الجانب 64-bit)، يمكن تحديد العرض صراحةً عبر RegistryView.

using Microsoft.Win32;

// قراءة HKLM بعرض 64-bit من عمليّة 32-bit
using var hklm64 = RegistryKey.OpenBaseKey(
    RegistryHive.LocalMachine, RegistryView.Registry64);
using var key = hklm64.OpenSubKey(@"SOFTWARE\KomuraSoft\MyApp");
var installDir = key?.GetValue("InstallDir") as string;

وبالمقابل، بمجرّد الحاجة إلى هذا التحديد، فتلك إشارة على تأجيل قرار تصميميّ هو «أيّهما الصحيح للكتابة: 32-bit أم 64-bit؟». والنهج السليم هو توحيد عدد بتّات جهة الكتابة وجهة القراءة.

وضع بيانات تتجاوز بضعة كيلوبايتات أو بيانات شبيهة بالمصفوفات في Registry أمر غير مُجدٍ من ناحية النسخ الاحتياطيّ والترحيل والتشخيص على حدّ سواء. اترك هذا الاستخدام للملفّات (JSON / SQLite).

4.4 Access (.accdb) ── تقريباً لا اعتماد جديد عليه، وللتكامل مع الأنظمة القائمة تعامل معه بحسم

كانت Access (JET/ACE) في الماضي هي قاعدة البيانات المحلّيّة المعتادة لتطبيقات الأعمال، لكن لم يعد هناك اليوم سبب يُذكَر لاختيارها في تطوير جديد. والسبب الرئيسيّ يتعلّق بالتوزيع. فالوصول إلى .accdb من الشيفرة يتطلّب موفّر ACE (Access Database Engine)، ولا يمكن الاتّصال ما لم يتطابق عدد بتّات التطبيق مع عدد بتّات ACE.4 وتوجد أيضاً مشكلة التعايش مع عدد بتّات Office، فحالة «يعمل على جهاز التطوير لكن يظهر لدى العميل خطأ Microsoft.ACE.OLEDB.12.0 provider is not registered on the local machine» قضيّة دعم فنّيّ معتادة. كما أنّ الحاجة إلى تثبيت الحزمة القابلة لإعادة التوزيع (Access Database Engine 2016 Redistributable) تزيد من عدد ملفّات التوزيع.9

ومع ذلك، توجد مواقف واقعيّة يتدخّل فيها Access، كالتكامل مع بيانات نظام أعمال قائم مبنيّ على Access، أو قراءة بيانات رئيسيّة أُنشئت بواسطة Access. في تلك الحالة، نوصي بالحسم التالي:

  • تثبيت عدد بتّات العمليّة التي تقرأ وتكتب (التثبيت على x86 هو الأكثر واقعيّة غالباً)، والتحقّق من وجود ACE المناظر عبر المثبِّت
  • تجنّب في التصميم الكتابة المتزامنة من عدّة أشخاص إلى ملفّ .accdb موضوع في مجلّد مشترك (تكلفة الاستعادة عند التلف لا تستحقّ العناء)
  • الاحتفاظ على المدى الطويل بمسار انتقال نحو SQLite أو قاعدة بيانات من نوع خادم

ويُنظَّم التعامل مع الأصول القائمة، بما فيها أصول Excel/VBA، أيضاً في «ما هي VBA - القيود، آفاق المستقبل، والمواقف التي ينبغي فيها الاستبدال».

5. جدول القرار

المحور ملفّ JSON SQLite Registry Access (.accdb)
البيانات التي يتفوّق فيها إعدادات صغيرة بيانات مهيكَلة متزايدة، بحث وتجميع أعلام صغيرة، تكامل مع Windows تكامل مع أصول Access قائمة
حجم البيانات التقريبيّ حتّى بضع مئات من الكيلوبايتات حتّى عشرات الغيغابايتات حتّى بضعة كيلوبايتات حتّى 2 غيغابايت (الحدّ الأقصى وفق المواصفات)
البحث والتجميع ✕ (يفترض قراءة الكلّ) ◎ (SQL) ○ (SQL)
قابليّة القراءة المباشرة من الإنسان △ (تحتاج أداة) △ (تحتاج Access)
الصمود أمام التلف △ (يحتاج إجراءً ذاتيّاً) ○ (معاملات)
الوصول المتزامن من عدّة عمليّات ○ (داخل الجهاز نفسه)
المشاركة من عدّة أجهزة ✕ (عمليّاً)
ملفّات توزيع إضافيّة لا يوجد لا يوجد (مُضمَّن مع NuGet) لا يوجد موفّر ACE إلزاميّ

كما يظهر من السطر الأخير، لا تصلح أيّ تقنيّة من تقنيّات الحفظ المحلّيّ لـ«المشاركة من عدّة أجهزة». قد يبدو أنّ وضعها في مجلّد مشترك يجعلها قابلة للمشاركة، لكنّ JSON لا يملك آليّة حصر (exclusion)، وقفل SQLite عبر SMB غير موثوق، وAccess يصل إلى حدوده مصحوباً بمخاطر التلف. إن ظهر متطلّب تعدّد المواقع والمستخدمين للتعامل مع البيانات نفسها، فاعتبر ذلك خطّ القرار للانتقال إلى قاعدة بيانات من نوع خادم كـ SQL Server Express أو إقامة Web API.

6. لا يتلف، وقابل للترحيل، وقابل للاستعادة ── تصميم مشترك بصرف النظر عن الصيغة

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

6.1 إلحاق رقم إصدار بالمخطّط (schema) والصيغة

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

في SQLite، أُعِدّ PRAGMA user_version تحديداً لهذا الغرض.

int GetVersion(SqliteConnection conn)
{
    using var cmd = conn.CreateCommand();
    cmd.CommandText = "PRAGMA user_version";
    return Convert.ToInt32(cmd.ExecuteScalar());
}

void Migrate(SqliteConnection conn)
{
    void Exec(string sql)
    {
        using var cmd = conn.CreateCommand();
        cmd.CommandText = sql;
        cmd.ExecuteNonQuery();
    }

    var v = GetVersion(conn);
    if (v > 2)
        // حالة فتح تطبيق قديم لقاعدة بيانات أنشأها إصدار أحدث من التطبيق.
        // الأسلم التوقّف هنا دون لمس مخطّط غير معروف
        throw new InvalidOperationException(
            $"تمّ إنشاء قاعدة البيانات هذه (الإصدار {v}) بواسطة تطبيق أحدث.");

    using var tx = conn.BeginTransaction();
    if (v < 1) Exec("ALTER TABLE measurement ADD COLUMN unit TEXT");
    if (v < 2) Exec("CREATE TABLE operator (id INTEGER PRIMARY KEY, name TEXT)");
    Exec("PRAGMA user_version = 2");
    tx.Commit();
}

هذا هو الشكل الأدنى لما يُعرَف بالترحيل (migration): النظر إلى الإصدار عند بدء التشغيل وتطبيق الفرق فقط. رفض «إصدار أحدث من إصدار نفسه» في البداية هو لمنع حادثة إفساد قد تقع حين يُعاد التطبيق إلى إصدار قديم (rollback) فتكتب الشيفرة القديمة إلى مخطّط لا تعرفه. الفكرة نفسها تنطبق على JSON: اجعل للجذر "version": 2، وأدرج تحويلاً من الصيغ القديمة عند القراءة، وارفض قراءة الصيغ الأحدث ممّا هو معروف. «لا تُصدِر أبداً صيغة بيانات بلا رقم إصدار» — الالتزام بهذا وحده يُنقذ نفسك في المستقبل.

6.2 حدِّد سلفاً سلوك التدهور التدريجيّ عند التلف

تطرّقنا في الفصل 4 إلى إجراءات الوقاية من التلف الخاصّة بكلّ صيغة (الكتابة الذرّيّة لـ JSON، معاملات SQLite)، لكن ستواجه رغم ذلك «بيانات لا يمكن قراءتها»: عطل في القرص، حجر بواسطة برنامج مضادّ للفيروسات بسبب كشف خاطئ، تحرير يدويّ من المستخدم. وإن لم تحدِّد كيف يتصرّف التطبيق في تلك اللحظة، سينتهي بك الأمر إلى تطبيق لا يبدأ التشغيل حتّى.

  • تعذّرت قراءة الإعدادات ← ابدأ التشغيل بالقيم الافتراضيّة وأخطِر المستخدم بذلك (العودة الصامتة للقيم الافتراضيّة تتحوّل إلى استفسار «اختفت إعداداتي»)
  • تعذّرت قراءة بيانات الأعمال ← اعرض «أيّ ملفّ تالف» عبر وضع قراءة فقط أو شاشة خطأ. لا تُصلحه بالكتابة فوقه تلقائيّاً (فذلك يمحو الدليل)
  • إن وُجدت نسخة احتياطيّة ← اقترح الاستعادة. لكنّ الاستعادة التلقائيّة وجهٌ لعملة واحدة مع خطر «الكشف الخاطئ عن تلفٍ والرجوع إلى بيانات قديمة»، لذا اجعل تدخّل المستخدم مبدأً أساسيّاً

6.3 النسخ الاحتياطيّ: «هل يمكن الاستعادة؟» أهمّ من «هل أُخِذ؟»

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

  • ماذا: بيانات الأعمال مشمولة، والتخزين المؤقّت غير مشمول، والمعلومات السرّيّة يجب مراعاة أنّها بحكم طبيعة DPAPI لا يمكن فكّ تشفيرها إلّا من نفس المستخدم ونفس الجهاز (يلزم إجراء منفصل للترحيل إلى جهاز آخر)
  • متى وإلى أين: عند بدء التشغيل أو يوميّاً، إلى مجلّد backup داخل %LOCALAPPDATA% مع ترقيم أجيال. أمّا وضعها فوق مجلّد مشترك أو مجلّد نسخ احتياطيّ موجود مسبقاً للأجهزة فذلك أمر يُبحَث مع فريق التشغيل
  • كيف: يُمنَع نسخ ملفّ SQLite نسخاً بسيطاً أثناء التشغيل. أمّا VACUUM INTO 'backup.db' فيتيح أخذ لقطة (snapshot) متّسقة بجملة واحدة
// VACUUM INTO لا ينشئ المجلّد الأصل، ويحدث خطأً إن كانت الوجهة موجودة مسبقاً.
// أنجز إنشاء المجلّد وتحديد اسم ملفّ غير متكرّر مسبقاً
var backupDir = Path.Combine(AppPaths.DataDir, "backup");
Directory.CreateDirectory(backupDir);
var backupPath = Path.Combine(backupDir, $"app-{DateTime.Now:yyyyMMdd-HHmmss}.db");

using var cmd = conn.CreateCommand();
cmd.CommandText = "VACUUM INTO $path";
cmd.Parameters.AddWithValue("$path", backupPath);
cmd.ExecuteNonQuery();

بما أنّ الأجيال تتراكم باستمرار، أدرج أيضاً معالجة «الإبقاء على آخر N جيل فقط وحذف الأقدم» بعد كلّ عمليّة نسخ احتياطيّ.

وقم مرّة واحدة على الأقلّ بتجربة استعادة فعليّة. من الحالات المألوفة جدّاً في أنظمة الأعمال وجود ملفّات نسخ احتياطيّ لكن لا أحد يعرف كيفيّة الاستعادة أو جرّبها من قبل. إذا كتبتَ دليل إجراءات لنقل البيانات إلى جهاز جديد عند استبدال الحاسوب، ستجد غالباً ثغرات في تصميم النسخ الاحتياطيّ (كعدم انتقال بيانات الاعتماد المحميّة بـ DPAPI، أو تلف المسار عند تضمينه اسم المستخدم عند استخدامه من مستخدم آخر). راجع أيضاً «ما ينبغي فعله قبل التخلّص من حاسوب Windows» بخصوص كيفيّة محو البيانات عند التخلّص من الحاسوب.

7. إرشادات للحالات الشائعة الّتي يصعب فيها الحسم

  • «إعدادات لكن يُتوقّع أن تتزايد مستقبلاً» ── إن كان هناك احتمال انهيار أسلوب «قراءة الكلّ عند البدء»، اجعلها SQLite من البداية. لا ضير أبداً في إنشاء «جدول settings» داخل SQLite.
  • «الترحيل من INI/XML» ── إن كان الأمر مجرّد استبدال للصيغة فانتقل إلى JSON، وإن كانت بيانات من نوع السجلّ التاريخيّ ممزوجة عند تلك اللحظة ففصّلها وانقلها إلى SQLite. إبقاء آليّة احتياطيّة (fallback) للصيغة القديمة في جانب القراءة لإصدار أو إصدارين يجعل الترحيل أكثر أماناً.
  • «يُطلَب منك أن تُعرَض في Excel» ── بدل جعل مخزن البيانات نفسه Excel/Access، إضافة وظيفة تحفظ في SQLite ثمّ تُصدِّر إلى CSV/Excel تُلبّي موثوقيّة البيانات والطلب معاً. راجع «كيف تبني إخراج تقارير Excel» بخصوص طريقة بناء إخراج التقارير.
  • «أريد القراءة والكتابة في نفس الملفّ من عدّة عمليّات» ── داخل الجهاز نفسه، يمكن أن يفي SQLite (بوضع WAL) بالغرض إلى حدّ بعيد، لكن يلزم تصميم لتضارب الكتابة. إن كان التكامل قائماً على الملفّات، استخدم أنماط الحصر (exclusion) الواردة في «أفضل ممارسات التكامل عبر الملفّات والقفل».
  • «أريد المشاركة من عدّة أجهزة» ── هذا تخرّج من الحفظ المحلّيّ. الخيار الأوّل هو بنية عميل-خادم تُقام فيها SQL Server Express (مجّانيّة، حتّى 10 غيغابايت لقاعدة البيانات) على جهاز يعادل خادم ملفّات. ومع ذلك، فإنّ «LocalDB» الخاصّة بـ SQL Server، خلافاً لاسمها، بيئة أحاديّة المستخدم موجَّهة لأغراض التطوير، فلا تختارها لغرض المشاركة. وإذا تجاوز الأمر مواقع متعدّدة أو استُخدم من خارج الشركة، فذلك خطّ القرار للنظر في بنية تتوسّطها Web API.

8. الخلاصة

بالتفكير في اختيار مكان الحفظ عبر تقسيمه إلى «أين توضَع» (LocalAppData / ProgramData، ولا كتابة أبداً في Program Files) و«بماذا تُحفَظ» (الإعدادات في JSON، البيانات المتزايدة في SQLite، Registry بالحدّ الأدنى، Access فقط للتكامل مع أنظمة قائمة)، يمكن الحسم دون تردّد في معظم الحالات.

وفوق ذلك، أدرج في الإصدار الأوّل — أيّاً كانت الصيغة — العناصر الثلاثة للفصل 6: رقم إصدار الصيغة، وسلوك التدهور التدريجيّ عند التلف، والنسخ الاحتياطيّ القابل للاستعادة. والمعلومات السرّيّة وحدها تُحفَظ دوماً في مكان منفصل عبر DPAPI. بالتمسّك بجدول القرار والتصميم المشترك الواردَين في هذا المقال، يمكن تجنّب معظم البُنى التي تكلِّف كثيراً لاحقاً، مثل «JSON تضخّم إلى عشرات الميغابايتات» أو «Access مشترك يتلف مرّة أسبوعيّاً». وإذا كنت غير مطمئنّ إلى طريقة الحفظ في تطبيق قائم، نوصي بالبدء أوّلاً بـ«جرد ما يُحفَظ وأين يُحفَظ».

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت ذ.م.م. مع إعادة النظر في طريقة حفظ بيانات تطبيقات الأعمال (بما في ذلك تصميم الترحيل من INI/XML/Access)، وتحقيق أسباب تلف البيانات وتدهور الأداء.

المراجع

  1. Microsoft Learn, KNOWNFOLDERID. حول تعريف مجلّدات Windows المعروفة مثل LocalAppData وRoamingAppData وProgramData. 

  2. Microsoft Learn, Microsoft.Data.Sqlite overview. حول لمحة عن موفّر ADO.NET الخاصّ بـ SQLite الذي تصونه Microsoft، وكونه أساساً لموفّر EF Core SQLite.  2

  3. Microsoft Learn, Registry Redirector. حول آليّة إعادة توجيه وصول Registry لعمليّات 32-bit إلى Wow6432Node في Windows 64-bit.  2

  4. Microsoft Learn, Can’t establish a connection to Access Database Engine OLE DB. حول ضرورة تطابق عدد بتّات موفّر ACE OLE DB مع عدد بتّات العمليّة التي تصل إليه.  2

  5. Microsoft Learn, Environment.SpecialFolder Enum. حول التعداد المستخدَم للحصول على المجلّدات المعروفة من .NET. 

  6. Microsoft Learn, Use a SQLite database in a Windows app. البرنامج التعليميّ الرسميّ الذي يوصي باستخدام SQLite وMicrosoft.Data.Sqlite / EF Core لحفظ البيانات المحلّيّة في تطبيقات Windows. 

  7. SQLite, How To Corrupt An SQLite Database File. حول كون خلل القفل على أنظمة الملفّات الشبكيّة سبباً رئيسيّاً لتلف قاعدة البيانات. 

  8. Microsoft Learn, Data types (Microsoft.Data.Sqlite). حول الأنواع الأربعة الأساسيّة لِـ SQLite، وقاعدة تخطيط DateTime وGuid إلى TEXT. 

  9. Microsoft, Microsoft Access Database Engine 2016 Redistributable. حول الحزمة القابلة لإعادة التوزيع (32-bit/64-bit) للوصول إلى .accdb / .mdb. 

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

أين ينبغي حفظ ملفّ إعدادات تطبيق Windows؟
بالنسبة لإعدادات وبيانات كلّ مستخدم على حدة، فالأساس هو الوضع تحت `%LOCALAPPDATA%` (`Environment.SpecialFolder.LocalApplicationData`) بترتيب هرميّ «اسم الشركة\اسم التطبيق». أمّا للمشاركة بين جميع المستخدمين فيُستخدَم `%PROGRAMDATA%`، لكنّ قوائم التحكّم بالوصول (ACL) الافتراضيّة قد تجعل البنية بحيث لا يستطيع مستخدم آخر تعديلها، لذا يُضبَط ACL صراحةً عبر المثبِّت. ولا يجوز أبداً الكتابة في المجلّد نفسه الذي يوجد فيه exe (تحت Program Files). فالمستخدم القياسيّ لا يستطيع الكتابة هناك، وفي التطبيقات القديمة ذات 32-bit تحدث إعادة توجيه صامتة إلى VirtualStore، ما يسبِّب عرضاً غامضاً لا يمكن تفسيره.
هل ينبغي حفظ الإعدادات بصيغة JSON أم SQLite؟
الإعدادات الصغيرة المهيكَلة تُحفَظ في ملفّ JSON، وبيانات الأعمال المتزايدة والسجلّ التاريخيّ والبيانات التي يُراد البحث فيها تُحفَظ في SQLite كخيار أوّل، وبهذين الاثنين تُغطّى غالبيّة حالات الحفظ المحلّيّ لتطبيقات الأعمال. نطاق تطبيق JSON يمتدّ إلى الحجم الذي يصحّ فيه «قراءة كلّ شيء عند بدء التشغيل وكتابة كلّ شيء عند الإنهاء»، أي حتّى بضع مئات من الكيلوبايتات كمعيار تقريبيّ. وإن بدأتَ بوضع سجلّ تاريخيّ يستمرّ بالإضافة إليه أو بيانات تحتاج بحثاً في السجلّات داخل JSON، فتلك إشارة للانتقال إلى SQLite. وحتّى الإعدادات، إن كان يُتوقّع تزايدها مستقبلاً، فلا مشكلة في إنشاء جدول settings في SQLite منذ البداية.
هل يجوز وضع قاعدة بيانات SQLite في مجلّد مشترك على الشبكة؟
ينبغي تجنّب ذلك. قفل الملفّات عبر SMB كثير المشكلات المرتبطة بالبيئة، وحتّى مشروع SQLite الرسميّ يذكر المشاركة على أنظمة الملفّات الشبكيّة كأوّل سبب لتلف قاعدة البيانات. وJSON لا يملك آليّة حصر (exclusion)، وAccess أيضاً يصل إلى حدوده مصحوباً بمخاطر التلف، فلا تصلح أيّ تقنيّة من تقنيّات الحفظ المحلّيّ للمشاركة من عدّة أجهزة. وإذا ظهر متطلّب تعدّد المواقع والمستخدمين للتعامل مع البيانات نفسها، فذلك خطّ القرار للانتقال إلى قاعدة بيانات من نوع خادم كـ SQL Server Express (مجّانيّة، حتّى 10 غيغابايت لقاعدة البيانات) أو إقامة Web API.
هل يجوز حفظ بيانات التطبيق في Registry؟
يكون Registry مناسباً لمعلومات التكامل مع Windows نفسه — كالتسجيل في بدء التشغيل وربط الملفّات — وكذلك لإعدادات مستخدم صغيرة جدّاً فقط. والبيانات التي تتجاوز بضعة كيلوبايتات أو البيانات الشبيهة بالمصفوفات غير مُجدية فيه من ناحية النسخ الاحتياطيّ والترحيل والتشخيص على حدّ سواء، فتُترَك لِـ JSON أو SQLite. كما أنّه في Windows 64-bit يُعاد توجيه `HKLM\Software` كما تراه عمليّة 32-bit إلى Wow6432Node، ما يسبِّب عرض «القيمة موجودة عند النظر عبر محرِّر Registry لكن لا يمكن للتطبيق قراءتها». والنهج السليم هو توحيد عدد بتّات جهة الكتابة وجهة القراءة.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

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

روابط عامة

العودة إلى المدونة