سجل التعديلات (3 تحديثات، آخر تحديث 2 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- تُرجمت تنبيه تلف SQLite عبر PRAGMA quick_check وقالب السجل إلى العربية، مع الإبقاء على {Detail}.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621601)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
غو كومورا (2026). استخدام SQLite في تطبيقات C# للأعمال ── وضع WAL، والتحكّم الحصريّ، والوقاية من التلف، والتمييز عن EF Core. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621601 https://comcomponent.com/ar/blog/csharp-sqlite-practical-guide/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621601
- DOI (هذه النسخة)
- 10.5281/zenodo.22240987
في المقال السابق «كيف تختار مكان حفظ بيانات تطبيق Windows» كتبنا أنّ SQLite هو الخيار الأوّل لحفظ بيانات الأعمال والسجلّ التاريخيّ المتزايد. الخلاصة التي نرثها كافتراض، في سطر واحد: «المكان لكلّ مستخدم %LOCALAPPDATA%، وللمشاركة بين جميع المستخدمين %PROGRAMDATA%. الصيغة للإعدادات الصغيرة JSON، ولبيانات الأعمال والسجلّ التاريخيّ المتزايد SQLite. وكلمات المرور ومفاتيح API وحدها مسار منفصل بـ DPAPI». يعالج هذا المقال «ما بعد اختيار SQLite» فقط، فيكفي حمل هذا السطر كافتراض حتّى إن لم تقرأ السابق.
الحكم بذلك محسوم، لكن عند الدخول فعليّاً في مرحلة الدمج تظهر حيرة من نوع آخر. ما نسمعه كثيراً في الاستشارات: «عند البحث عن SQLite في NuGet تظهر عدّة حزم ولا أدري أيّها أثبِّت»، و«يعمل لكن يظهر أحياناً database is locked. أتجاوزه بإعادة المحاولة، فهل هذا صحيح؟»، و«هل يكفي النسخ الاحتياطيّ نسخ ملفّ قاعدة البيانات؟».
كلّها نقاط يمرّ بها حتماً من يشغّل SQLite سنوات في تطبيق أعمال، وإن ثُبِّتت في التصميم الأوّل لم تُتعب لاحقاً. يرتّب هذا المقال، بافتراض Microsoft.Data.Sqlite، البنود التي نؤكّدها في كلّ مراجعة تصميم: اختيار المكتبة، وسلسلة الاتّصال والتجميع، وآليّة وضع WAL، ومواجهة SQLITE_BUSY، ومطبّات تخطيط الأنواع، والوقاية من التلف والنسخ الاحتياطيّ، والتمييز عن EF Core.
1. الخلاصة أوّلاً
في سطر: المكتبة Microsoft.Data.Sqlite، وفعِّل وضع WAL من البداية، ووحِّد مسار الكتابة في واحد. والتفصيل كالتالي.
- المكتبة المستخدمة في تطوير جديد أساسها
Microsoft.Data.Sqlite(أو موفّر SQLite في EF Core الجالس فوقها). هي شيء آخر لا يتوافق معSystem.Data.SQLiteلا في سلسلة الاتّصال ولا في تفاصيل السلوك، فانتبه دائماً إلى أيّهما يفترضه المثال على الويب.1 - فعِّل وضع WAL من الإصدار الأوّل. ترتفع توازيّة القراءة والكتابة، وتختفي غالبيّة
database is locked. الإعداد يُحفَظ في ملفّ قاعدة البيانات نفسه، لكنّه لا يُستخدم على مشاركة شبكيّة.2 database is locked(SQLITE_BUSY) ليس هدفاً لـ«إضافة معالجة خطأ» بل إشارة لإعادة النظر في التصميم. تعيدMicrosoft.Data.Sqliteالمحاولة تلقائيّاً حتّى المهلة (30 ثانية افتراضيّاً)3، لكنّ الحلّ الجذريّ توحيد مسار الكتابة.- عند تنفيذ كمّ كبير من INSERT الصغيرة، تجميعها في معاملة صريحة وحدها يسرّع برتبتين أو ثلاث. إن شعرت أنّ «SQLite بطيء» فاشكّ أوّلاً في وحدة الالتزام.
- أنواع SQLite عمليّاً أربعة: INTEGER / REAL / TEXT / BLOB، وتُحفَظ DateTime وGuid وdecimal كـ TEXT. وخصوصاً decimal يصير مقارنته وترتيبه وفق قاعدة السلاسل، لذا المبلغ آمن كعدد صحيح بأصغر وحدة نقديّة.4
- النسخ الاحتياطيّ يمنع النسخ البسيط للملفّ أثناء التشغيل. استخدم
VACUUM INTOأو Backup API (SqliteConnection.BackupDatabase). وحذف ملفّات-wal/-shmيدوياً محظور أيضاً.5 - SQLite الخام لا يدعم التشفير.
Passwordفي سلسلة الاتّصال لا يعمل إلّا إذا أُدمجت مكتبة أصليّة من فصيلة SQLCipher6، وللكمّ القليل من المعلومات السرّيّة انظر أوّلاً في الحماية بـ DPAPI (Data Protection API؛ واجهة تشفير يقدّمها Windows كوظيفة نظام تشغيل فلا يحتاج التطبيق حمل المفتاح) بدل تشفير قاعدة البيانات كلّها. التفاصيل في «حفظ المعلومات السرّيّة في تطبيقات Windows - تجنّب الإعدادات النصّيّة الصريحة عبر DPAPI».
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 27، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. اختيار المكتبة ── أسماء متشابهة بمحتوى مختلف
حزم استخدام SQLite من .NET عدّة، وتشابه الأسماء أوّل مطبّ. الترتيب كالتالي.
| الحزمة | الموقع | معيار الاعتماد الجديد |
|---|---|---|
Microsoft.Data.Sqlite |
موفّر ADO.NET تصونه Microsoft. خفيف، وكيان SQLite الأصليّ مضمَّن في NuGet | ◎ الخيار الأوّل |
Microsoft.EntityFrameworkCore.Sqlite |
موفّر SQLite في EF Core. يستخدم داخليّاً Microsoft.Data.Sqlite |
◎ للتطبيقات المتمحورة حول الكيانات (الفصل 8) |
System.Data.SQLite |
موفّر عريق من سلسلة فريق تطوير SQLite. سوابق كثيرة من عهد .NET Framework | △ لصيانة الأصول القائمة فقط |
Dapper |
معيِّن خفيف يجلس فوق ADO.NET. ليس خاصّاً بـ SQLite | ○ عندما تريد تقليل شيفرة إعادة التعبئة في ADO.NET الخام |
تصون Microsoft.Data.Sqlite فريق EF Core، وهي أيضاً أساس موفّر SQLite في EF Core.1 الحزمة تضمّ الثنائيّ الأصليّ (كيان SQLite)، فلا حاجة إلى عمل توزيع على أجهزة العملاء، وفروق x86/x64/ARM64 يمتصّها جانب NuGet.
ما ينبغي الانتباه إليه أنّ معلومات موجَّهة لـ System.Data.SQLite لا تُستخدم كما هي. لطول استخدامها تكثر الأمثلة على الويب بافتراض System.Data.SQLite، فتدوس عدم التوافق التالي.
- لا توافق في سلسلة الاتّصال. كلمات مثل
Version=3وUseUTF16EncodingوDateTimeFormatالتي تغيّر تنسيق التاريخ غير موجودة فيMicrosoft.Data.Sqlite، وتحديدها يرمي استثناء. الكلمات قليلة جدّاً:Data Source/Mode/Cache/Password/Foreign Keys/Default Timeout/Poolingوغيرها.6 - معاملة الأنواع مختلفة. مثلاً Guid يُحفَظ افتراضيّاً كـ BLOB في
System.Data.SQLiteوكـ TEXT فيMicrosoft.Data.Sqlite. في فترة ترحيل يقرأ فيها المكتبتان ملفّ قاعدة البيانات نفسه ويكتبان فيه، يظهر هذا الفرق كعدم اتّساق بيانات.
Microsoft.Data.Sqlite مصنوعة رقيقة عن قصد، وبلا تحويل أنواع خاصّ ولا وظائف مريحة، فتنطبق أوصاف وثائق SQLite الرسميّة كما هي. إن كان تطبيقاً قائماً يعمل بثبات على System.Data.SQLite فلا موجب للترحيل قسراً، لكنّ الشيفرة الجديدة تُمال إلى Microsoft.Data.Sqlite، وإن رحّلت فالجرد أوّلاً لعدم التوافق أعلاه كبنود ترحيل، هذا هو المبدأ.
3. الاتّصال وسلسلة الاتّصال ── التجميع (pooling) يُبقي الملفّ ممسوكاً
الشكل الأساسيّ لسلسلة الاتّصال مسار الملفّ فقط. ما يُوعى به في العمل Mode وPooling.6
using Microsoft.Data.Sqlite;
var builder = new SqliteConnectionStringBuilder
{
DataSource = dbPath,
Mode = SqliteOpenMode.ReadWriteCreate // 既定値。無ければ作る
};
using var conn = new SqliteConnection(builder.ConnectionString);
conn.Open();
Mode: الافتراضيّReadWriteCreate(يُنشئ إن لم يوجد). في حالات مثل توزيع قاعدة بيانات رئيسيّة للقراءة فقط حدِّدReadOnlyفتمنع الكتابة بعلّة أو خطأ تشغيل.Cache: عادة اترك الافتراضيّ. الوثائق تنصّ صراحة على أنّ الجمع بينCache=Sharedووضع WAL غير موصى به، فلا موضع له في نهج هذا المقال الذي يستخدم WAL.6Password: التحديد يرسلPRAGMA keyفور الاتّصال، لكن المكتبة الأصليّة المعياريّة لا تدعم التشفير فلا يحدث شيء.6 إن كان تشفير قاعدة البيانات كلّها متطلّباً لزم استبدال حزمة من فصيلة SQLCipher (مثلSQLitePCLRaw.bundle_e_sqlcipher)، وحفظ مفتاح التشفير يحتاج في النهاية DPAPI. DPAPI (Data Protection API) واجهة تشفير يقدّمها Windows كوظيفة نظام تشغيل، وتُستخدَم منSystem.Security.Cryptography.ProtectedData. القيمة المشفَّرة بتحديدDataProtectionScope.CurrentUserمرتبطة ببيانات اعتماد تسجيل دخول ذلك المستخدم، ولا تُفكّ مبدأً لمستخدم آخر ولا على حاسوب آخر. يكفي فهمها كأداة تنيب نظام التشغيل عن التراجع اللانهائيّ «أين نضع مفتاح التشفير نفسه» (التفاصيل في رابط الفصل 1).Default Timeout: مهلة الأمر (30 ثانية افتراضيّاً). تصير الحدّ الأعلى لزمن إعادة المحاولة في الفصل 5.
ونقطة أخرى عن واجهات غير متزامنة. SQLite نفسه لا يملك إدخالاً/إخراجاً غير متزامن، فدوال async مثل ExecuteNonQueryAsync تنفيذ متزامن داخليّاً.7 «جعلته async فلا تتجمّد الواجهة» لا يصحّ، فأخرِج الاستعلام الثقيل صراحة إلى خيط عامل بـ Task.Run ونظائره. معيار حكم async/await كما رتّبناه في «جدول قرار C# async/await العمليّ».
3.1 مطبّ التجميع (pooling) ── الملفّ يبقى مفتوحاً حتّى بعد Close
من الإصدار 6.0 صار تجميع الاتّصالات في Microsoft.Data.Sqlite مفعَّلاً افتراضيّاً.6 المزيّة عدم إعادة فتح الملفّ في كلّ Open، وبالمقابل حتّى بعد Close / Dispose يبقى الاتّصال الأصليّ في التجميع ممسكاً بمقبض ملفّ قاعدة البيانات. فتفشل العمليّات التالية بـ«الملفّ قيد الاستخدام».
- وظيفة «تهيئة البيانات» التي تحذف ملفّ قاعدة البيانات وتعيد إنشاءه
- استبدال ملفّ قاعدة البيانات عند الاستعادة من نسخة احتياطيّة
- نقل ملفّ قاعدة البيانات عند إلغاء التثبيت أو الإخلاء
الإجراء: أفرغ التجميع قبيل عمليّة الملفّ.
// この接続文字列に対応するプールを破棄し、ファイルハンドルを解放する
SqliteConnection.ClearPool(new SqliteConnection(connectionString));
File.Delete(dbPath);
في وضع WAL (الفصل التالي) قد تبقى ملفّات -wal / -shm، فنظِّفها معها. إن أردت تحرير الكلّ عند إنهاء التطبيق فـ SqliteConnection.ClearAllPools()، وإن كانت أداة لمرّة واحدة لا تحتاج تجميعاً أصلاً فـ Pooling=False في سلسلة الاتّصال. «أغلقت وما زلت لا أحذف» استفسار شائع بعد الترحيل إلى SQLite، فأدخل ClearPool من البداية في أدوات تلمس ملفّ قاعدة البيانات.
4. آليّة عمل وضع WAL ── استخدمه وأنت تعرف ما الذي يحدث
في المقال السابق اكتفينا بـ«فعِّل وضع WAL»، فنغوص هنا إلى الآليّة. في أسلوب مجلّة التراجع الافتراضيّ تُحظَر القراءة أثناء الكتابة، وهذا السبب الرئيسيّ لـ database is locked. في أسلوب WAL (Write-Ahead Logging) تُكتَب التغييرات لا في كيان قاعدة البيانات بل في ملفّ سجلّ مخصّص للإلحاق، لذا القراءة لا تحظر الكتابة، والكتابة لا تحظر القراءة.2 فيقوم كما هو التكوين النمطيّ لتطبيق أعمال: خيط الواجهة يعرض السجلّ التاريخيّ بينما تُكتَب قيم القياس في الخلفيّة.
عند وضع WAL يظهر ملفّان بجانب كيان قاعدة البيانات (المحتوى المثبَّت عند نقطة التحقّق).
| الملفّ | الدور |
|---|---|
app.db-wal |
سجلّ التغييرات الملحقة. يتضمّن تغييرات ملتزَماً بها لم تنعكس بعد في الكيان |
app.db-shm |
ذاكرة مشتركة تُدعى wal-index. تضبط موضع قراءة WAL بين العمليّات |
علاقة الملفّات الثلاثة بالقراءة والكتابة في المخطّط كالتالي.
flowchart TB
W["اتّصال الكتابة في وقت واحد واحد فقط"]
R["اتّصالات القراءة يمكن فتح أيّ عدد معاً"]
WAL["app.db-wal (سجلّ الإلحاق) تغييرات ملتزَم بها لم تنعكس في الكيان حذف هذا يدوياً يمحو أحدث البيانات"]
SHM["app.db-shm (wal-index) ذاكرة مشتركة. تضبط بين العمليّات إلى أين تُقرأ WAL"]
DB["app.db (الكيان) محتوى مثبَّت عند نقطة التحقّق"]
CP["نقطة التحقّق الافتراضيّ تنفيذ تلقائيّ عندما تبلغ WAL 1000 صفحة (نحو 4 ميغابايت) قراءة طويلة جاثمة تمنع التقدّم فتتضخّم WAL"]
W -->|"يلحق التغييرات"| WAL
R -->|"يقرأ الجزء المثبَّت"| DB
R -->|"الالتزامات غير المنعكسة تُقرأ من هنا"| WAL
SHM -.->|"يخبر بنطاق القراءة"| R
WAL --> CP
CP -->|"ينسخ إلى الكيان ويفرغ WAL"| DB
القراءة تنظر في الكيان وفي WAL معاً، لذا لا تتوقّف القراءة أثناء الكتابة. هذا أثر WAL. وفي الوقت نفسه يُقرأ من المخطّط مباشرة قيدان: «نسخ الكيان وحده لا يتضمّن أحدث البيانات» و«-shm ذاكرة مشتركة فلا تقوم على مشاركة شبكيّة».
أربع خصائص ينبغي تثبيتها.
- نقطة التحقّق: معالجة نسخ محتوى
-walإلى الكيان، وتنفَّذ تلقائيّاً افتراضيّاً عندما تبلغ WAL 1000 صفحة (نحو 4 ميغابايت).2 إن جثمت معاملة قراءة طويلة لم تتقدّم نقطة التحقّق وتضخّم-wal، فتجنّب تصميماً «يبقي اتّصال القراءة مفتوحاً ويدور به»، وافتح عند الاستخدام وأغلق (إعادة الفتح سريعة بفضل التجميع). - الإعداد يُحفَظ في قاعدة البيانات:
PRAGMA journal_mode=WALإن نُفِّذ مرّة سُجِّل في ملفّ قاعدة البيانات نفسه، ويبقى WAL بعدها بأيّ اتّصال تُفتَح.2 لا حاجة إلى إصداره في كلّ اتّصال. - الكتابة ما زالت واحدة في الوقت نفسه: ما يرفعه WAL توازيّة القراءة والكتابة، والكتابات فيما بينها ما زالت حصريّة. إن أخطأت الفهم وظننت «WAL إذن الكتابة حرّة من خيوط عدّة» لقيت
SQLITE_BUSYفي الفصل 5. - لا يُستخدم على مشاركة شبكيّة: wal-index يفترض ذاكرة مشتركة، فلا يعمل بين عمليّات على أجهزة مختلفة.2 وعدم وضع SQLite على مشاركة شبكيّة أصلاً كما في المقال السابق.
ومن انتباه التشغيل: ملفّ -wal يحمل معاملات ملتزَماً بها لم تنعكس بعد في الكيان. «يكفي نسخ .db الكيان» و«-wal ملفّ مؤقّت فيجوز حذفه» كلاهما خطأ، فيختفي أحدث التزام أو في الأسوأ تتلف قاعدة البيانات.5 وهذا يتّصل مباشرة بحديث الفصل 7 «النسخ الاحتياطيّ ليس نسخ ملفّ».
4.1 التحقّق من أنّ الإعداد سارٍ
التبديل إلى WAL تنفيذ PRAGMA journal_mode=WAL مرّة واحدة، لكن لمنع «ظننت أنّي نفّذت وهو غير سارٍ» اقرأ الإعداد دائماً وأكّد. PRAGMA بلا قيمة يعيد القيمة الحاليّة، فالتأكيد أسطر قليلة.
using var cmd = conn.CreateCommand();
cmd.CommandText = "PRAGMA journal_mode";
var mode = (string)cmd.ExecuteScalar()!; // إن كان WAL سارياً فالقيمة "wal"
if (!string.Equals(mode, "wal", StringComparison.OrdinalIgnoreCase))
logger.LogWarning("journal_mode ما زال {Mode}", mode);
نجمع بنوداً يُستحسَن النظر إليها في التشخيص الذاتيّ عند البدء.
| ما تريد تأكيده | الاستعلام | القيمة المتوقَّعة | كيف يسري |
|---|---|---|---|
| هل صار وضع WAL | PRAGMA journal_mode |
wal |
يُحفَظ في جانب ملفّ قاعدة البيانات. ضبط مرّة يكفي فيبقى WAL بأيّ اتّصال لاحق2 |
| هل قيود المفاتيح الخارجيّة مفعَّلة | PRAGMA foreign_keys |
1 |
إعداد لكلّ اتّصال. e_sqlite3 الذي تستخدمه Microsoft.Data.Sqlite افتراضيّاً مبنيّ بحالة مفعَّلة فلا حاجة لتحديد في سلسلة الاتّصال، لكن استبدال المكتبة الأصليّة يغيّر الافتراض6 |
| إصدار المخطّط | PRAGMA user_version |
رقم الإصدار الذي يفترضه التطبيق | مادّة حكم ترحيل البدء (القسم 7.3) |
| هل ليست تالفة | PRAGMA quick_check |
ok |
القسم 7.1 |
المهمّ اختلاف «كيف يسري» في العمود الرابع. journal_mode يُسجَّل في ملفّ قاعدة البيانات فلا حاجة إلى إصداره في كلّ اتّصال، أمّا البقيّة فإعدادات تُحسَم في كلّ فتح اتّصال. الاستشارة «كان سارياً على جهاز التطوير وغير سارٍ في الإنتاج» نمطها أنّ هذا الموضع تغيّر بفرق سلسلة الاتّصال أو المكتبة الأصليّة.
إن أردت فحص ملفّ قاعدة البيانات وحده بلا تشغيل التطبيق، يمكن تنفيذ PRAGMA نفسه في صدفة سطر الأوامر الرسميّة sqlite3. كأداة تحقيق ميدانيّ تؤكّد في دقيقة «هل قاعدة البيانات هذه WAL حقاً».
5. الحصريّة وSQLITE_BUSY ── توحيد الكتابة في مسار واحد
SQLITE_BUSY (في رسالة الاستثناء database is locked) يحدث عندما يحتفظ اتّصال آخر بقفل كتابة. أوّل ما ينبغي معرفته أنّ Microsoft.Data.Sqlite تعيد المحاولة تلقائيّاً حتّى مهلة الأمر (30 ثانية افتراضيّاً) أمام أخطاء busy/locked.3 أي أنّ إعادة المحاولة الذاتيّة بـ catch ثمّ نوم ثمّ إعادة تنفيذ غير لازمة عادة. فإن بلغ المهلة وطار الاستثناء رغم ذلك، فهو واحد ممّا يلي.
- اتّصال آخر (أو عمليّة أخرى) يمسك معاملة طويلة تتجاوز 30 ثانية
- محاولة الترقّي إلى الكتابة داخل معاملة بدأت بالقراءة فاصطدمت بكتابة أخرى ── الانتظار لا يحلّه فيفشل فوراً. الأسلوب المتعارف تصميم معالجة «اقرأ ثمّ اكتب» كمعاملة كتابة من البداية
- كتابات صغيرة كثيرة تضرب من خيوط عدّة فتصير منازعة أقفال
لا يحلّ أيّاً منها «زيادة عدد إعادة المحاولة». الإجراء تقصير المعاملات وتوحيد مسار الكتابة في واحد.
5.1 إنشاء طابور كتابة عبر System.Threading.Channels
البيانات التي تتعدّد مصادر خيوطها (قيم قياس، سجلّ تشغيل، إلخ) لا يكتبها كلّ خيط مباشرة في قاعدة البيانات، بل تُرمى في طابور وتعالجها حلقة كتابة متفرّغة. في .NET تُستخدم System.Threading.Channels كما هي.
using System.Globalization;
using System.Threading.Channels;
using Microsoft.Data.Sqlite;
public sealed record Measurement(string DeviceId, double Value, DateTime CreatedAtUtc);
public sealed class MeasurementWriter : IAsyncDisposable
{
private readonly Channel<Measurement> _channel =
Channel.CreateBounded<Measurement>(new BoundedChannelOptions(10_000)
{
FullMode = BoundedChannelFullMode.Wait // 溢れたら生産側を待たせる
});
private readonly string _connectionString;
private readonly Task _loop;
public MeasurementWriter(string connectionString)
{
_connectionString = connectionString;
_loop = Task.Run(WriteLoop);
}
// どのスレッドから呼んでもよい。DBには触らない。書き込みループが
// 死んでいる場合は ChannelClosedException で投入側にもすぐ分かる
public ValueTask EnqueueAsync(Measurement m) => _channel.Writer.WriteAsync(m);
private async Task WriteLoop()
{
try
{
await WriteLoopCore();
}
catch (Exception ex)
{
// ディスクフル等で書き込み役が死んだことをチャネル経由で投入側へ
// 伝える。これを怠ると、キューが満杯になった時点で EnqueueAsync が
// 永遠に待ち続け、誰も障害に気づけない
_channel.Writer.TryComplete(ex);
throw;
}
}
private async Task WriteLoopCore()
{
using var conn = new SqliteConnection(_connectionString);
conn.Open();
var buffer = new List<Measurement>(500);
while (await _channel.Reader.WaitToReadAsync())
{
// 溜まっている分を最大500件まで回収して1トランザクションで書く
buffer.Clear();
while (buffer.Count < 500 && _channel.Reader.TryRead(out var m))
buffer.Add(m);
using var tx = conn.BeginTransaction();
using var cmd = conn.CreateCommand();
cmd.Transaction = tx;
cmd.CommandText =
"INSERT INTO measurement (device_id, value, created_at) " +
"VALUES ($device, $value, $at)";
var pDevice = cmd.Parameters.Add("$device", SqliteType.Text);
var pValue = cmd.Parameters.Add("$value", SqliteType.Real);
var pAt = cmd.Parameters.Add("$at", SqliteType.Text);
foreach (var m in buffer)
{
pDevice.Value = m.DeviceId;
pValue.Value = m.Value;
// カルチャ既定の暦・数字に影響されないよう InvariantCulture を明示
pAt.Value = m.CreatedAtUtc.ToString(
"yyyy-MM-dd HH:mm:ss.fffffff", CultureInfo.InvariantCulture);
cmd.ExecuteNonQuery();
}
tx.Commit();
}
}
public async ValueTask DisposeAsync()
{
// 書き込みループが失敗で先にチャネルを閉じていても例外にしない
// (Complete() だと二重クローズで投げ、元の例外を隠してしまう)
_channel.Writer.TryComplete();
await _loop; // 残りを書き切る。ループが死んでいた場合は元の例外がここで出る
}
}
بهذا الشكل ينتفي تصادم الكتابة بنيويّاً، ويتجمّع ما تراكم في الطابور دفعة بشكل طبيعيّ فتحصل أيضاً على تسريع القسم التالي. ويؤثّر في تطبيقات الأعمال كذلك إغلاق الطابور بعد كتابته في DisposeAsync عند الإنهاء، وحسم سلوك الامتلاء صراحة بـ FullMode، ونشر فشل حلقة الكتابة إلى جهة الإدخال عبر TryComplete(ex) (إن ماتت جهة الكتابة الموحَّدة صامتة ظهر العطل كانتظار أبديّ عند امتلاء الطابور). الفكرة نفسها «توحيد المسار» مشتركة مع تصميم الحصر عند التكامل عبر ملفّات («أفضل ممارسات تكامل الملفّات والقفل»).
5.2 التجميع في معاملة واحدة يغيّر رتبة الأداء
ينفّذ SQLite كتابة متزامنة إلى وحدة التخزين (fsync) عند كلّ التزام، لذا INSERT سطراً بسطر بالتزام ضمنيّ يبلغ سقفه بضع مئات إلى بضعة آلاف في الثانية حتّى على SSD، وبضع عشرات في الثانية على HDD. تجميع INSERT نفسه في معاملة صريحة بوحدة 1000 سطر (إحاطة بـ BeginTransaction وCommit في النهاية) يصل إلى رتبة عشرات الآلاف إلى مئات الآلاف في الثانية. تغيير ثلاث أسطر أكبر من أن يُدعى ضبطاً، فيتغيّر برتبتين أو ثلاث.
كثير من استشارات «استيراد CSV يستغرق 20 دقيقة» و«ترحيل البيانات عند البدء لا ينتهي» سببها هذا، ويُحلّ بجعلها معاملة وإعادة استخدام المعاملات (شكل شيفرة القسم السابق) فقط. وبالمقابل، إطالة المعاملة أكثر من اللازم تنتظر الكتابات الأخرى، فالتسوية العمليّة «بضع مئات إلى بضعة آلاف، أو ما يعادل بضع مئات المللي ثانية» التزاماً واحداً.
6. مطبّات تخطيط الأنواع ── ماذا نُسنِد إلى الأنواع الأربعة فقط
الأنواع التي يحفظها SQLite فعلاً أربعة فقط: INTEGER / REAL / TEXT / BLOB، وأنواع .NET تُسنَد إلى واحد منها. التخطيط الرئيسيّ في Microsoft.Data.Sqlite كالتالي.4
| نوع .NET | نوع SQLite | شكل الحفظ | انتباه عمليّ |
|---|---|---|---|
| bool / int / long | INTEGER | bool هو 0 / 1 | |
| double | REAL | خطأ الفاصلة العائمة يبقى كما هو | |
| string | TEXT | UTF-8 | |
| DateTime | TEXT | yyyy-MM-dd HH:mm:ss.FFFFFFF |
توحيد التنسيق والمنطقة الزمنيّة إلزاميّ |
| DateTimeOffset | TEXT | مع إزاحة | اختلاط الإزاحات يجعل الترتيب متعذّراً |
| Guid | TEXT | مفصول بشرطات | لا يتوافق مع افتراضيّ System.Data.SQLite (BLOB) |
| decimal | TEXT | شكل 0.0###... |
المقارنة والترتيب يصيران وفق قاعدة السلاسل |
الثلاثة التي تسقط إلى TEXT تستحقّ الانتباه.
- DateTime: تنسيق TEXT من فصيلة ISO 8601، إن وُحِّد التنسيق والمنطقة الزمنيّة، صار ترتيب السلاسل = ترتيب الزمن، فلا مشكلة عمليّة. وبالمقابل ينكسر لحظة اختلاط UTC بالتوقيت المحلّي. الحلّ الوحيد حسم «الحفظ UTC، والتحويل إلى المحلّي عند العرض» من البداية والالتزام به في كلّ مسار، ودوال SQLite مثل
datetime('now')تعيد UTC أيضاً. ونقطة أخرى: عند التحويل إلى سلسلة بنفسك مرِّر حتماًCultureInfo.InvariantCultureإلىToString. بثقافة الجهاز الافتراضيّة يتغيّر تدوين السنة على أجهزة تعمل بثقافة غير الميلاديّة مثل التقويم اليابانيّ أو التقويم البوذيّ، فينكسر الترتيب والقراءة (راجع مثال شيفرة 5.1). - Guid: يُحفَظ كسلسلة فالمطابقة مطابقة سلاسل. إن كتبته أداة أو مكتبة أخرى بتنوين مختلف (حروف كبيرة/صغيرة، شكل BLOB) فشلت المطابقة، فإن لمسته لغات وأدوات عدّة فنصّ التنوين كمواصفة.
- decimal: أكبر مطبّ. يُحفَظ TEXT لأنّ REAL يفقد الدقّة4، لكن مقارنة مثل
WHERE amount > 1000على عمود TEXT لا تعمل كما قُصد لأنّ TEXT أكبر دائماً من العدد في تسلسل أنواع SQLite. والتجميع مثلSUMيتحوّل داخليّاً إلى REAL فتضيع الدقّة التي لأجلها اخترت decimal.
المبدأ العمليّ لـ decimal بسيط: احمل المبلغ عدداً صحيحاً (INTEGER) بأصغر وحدة نقديّة. بالين اليابانيّ احفظ long بوحدة الين، وحوِّل عند العرض. العدد الصحيح مقارنة وتجميع دقيقان وسريعان، ويختفي مطبّ النوع. إن ألزمتك المخطّطات القائمة بالإبقاء على decimal، فاحسم ألا تُجرى المقارنة والتجميع في جانب SQL بل بعد القراءة في جانب .NET.
واسم النوع المكتوب في CREATE TABLE مجرّد تلميح «تآلف (affinity)»، وأسماء أنواع خاصّة مثل STRING منبع حوادث تحويل ضمنيّ. التوصية الرسميّة استخدام أسماء الأنواع الأربعة فقط INTEGER / REAL / TEXT / BLOB أيضاً لأسماء أعمدة الأنواع.4
7. التشغيل ── لا تُفسِد، وإن فسدت يمكن الاستعادة
7.1 فحص quick_check عند بدء التشغيل
SQLite محميّ بمعاملات، غير أنّ التلف بعطل قرص أو عمليّة ملفّ خاطئة لا يصير صفراً. حتى لا يستمرّ العمل على قاعدة تالفة فيتّسع الضرر، أدخل فحص اتّساق عند البدء. integrity_check الكامل يستغرق وقتاً على قاعدة كبيرة، فيكفي لليوم الخفيف quick_check.
using var cmd = conn.CreateCommand();
cmd.CommandText = "PRAGMA quick_check";
var result = (string)cmd.ExecuteScalar()!;
if (result != "ok")
{
// لا تكتب مزيداً إلى قاعدة بيانات تالفة. انحدر إلى وضع القراءة فقط وحثّ على الاستعادة
logger.LogError("اكتُشف تلف قاعدة البيانات: {Detail}", result);
EnterReadOnlyMode(result);
}
سياسة عدم الإصلاح التلقائيّ ولا الإرجاع التلقائيّ عند اكتشاف التلف (إدخال عمليّة مستخدم) كما كتبنا في القسم 6.2 من المقال السابق.
7.2 النسخ الاحتياطيّ ── لماذا لا يصحّ نسخ الملفّ
النسخ البسيط لملفّ قاعدة البيانات أثناء التشغيل قد يلتقط حالة في منتصف معاملة، وتنصّ وثائق SQLite الرسميّة عليه كسبب تلف.5 وفي وضع WAL، نسخ الكيان وحده وترك -wal (يحمل التزامات لم تنعكس في الكيان؛ الفصل 4) يُسقط أحدث البيانات.
الطريقتان الصحيحتان كلتاهما تأخذان لقطة متّسقة أثناء التشغيل.
VACUUM INTO: بجملة SQL واحدة تصنع نسخة بأصغر حجم بعد إزالة التجزئة.8 مثال الشيفرة في القسم 6.3 من المقال السابق فارجع إليه.SqliteConnection.BackupDatabase: غلاف Backup API في SQLite، ينسخ بين كائنات اتّصال.
using var source = new SqliteConnection($"Data Source={dbPath}");
using var target = new SqliteConnection($"Data Source={backupPath}");
source.Open();
target.Open();
source.BackupDatabase(target); // 稼働中でも一貫したスナップショットになる
غير أنّ لـ BackupDatabase نقطة انتباه. التنفيذ الحاليّ في Microsoft.Data.Sqlite ينسخ بأسرع ما يمكن، ويحظر الكتابة من الاتّصالات الأخرى حتّى الاكتمال.9 نسخ قاعدة كبيرة احتياطيّاً أثناء استمرار القياس أو تشغيل المستخدم يظهر كتابات تلك الفترة كـ SQLITE_BUSY أو تجميد شاشة. الفصل الآمن: النسخ الاحتياطيّ اليوميّ للأجيال بـ VACUUM INTO، وBackupDatabase للنسخ المتبادل مع قاعدة في الذاكرة أو للنسخ في فترة توقّفت فيها الكتابة أو في معالجة صيانة. إن استخدمت Task Scheduler للتنفيذ الدوريّ، فلا تجعل الأمر نسخ ملفّ من الخارج، بل اجعل التطبيق نفسه (أو أداة صغيرة تفتح SQLite فتحاً صحيحاً) ينفّذ النسخ الاحتياطيّ أعلاه. تصميم التنفيذ الدوريّ نفسه كما كتبنا في «تشغيل دوريّ آمن عبر Task Scheduler».
7.3 موضع الملفّ والترحيل (migration)
موضع ملفّ قاعدة البيانات أساسه %LOCALAPPDATA%\اسم الشركة\اسم التطبيق، وإدارة إصدار المخطّط حدّها الأدنى ترحيل عند البدء عبر PRAGMA user_version. كلاهما مكتوب بشيفرة في المقال السابق (الفصل 3 والقسم 6.1) فلا نكرّر هنا. تكملة واحدة: أخذ جيل واحد من نسخ 7.2 الاحتياطيّ قبل تنفيذ الترحيل يجعل أسوأ حالة «فشل الترحيل فتعذّر البدء» مجرّد استبدال ملفّ.
8. متى نستخدم EF Core ── نقطة التعادل الخاصّة بـ ORM
كتبنا حتّى هنا بـ Microsoft.Data.Sqlite الخام، لكنّ مواضع ينبغي فيها استخدام موفّر SQLite في EF Core واضحة أيضاً. محور الحكم طابع التطبيق.
| طابع التطبيق | التوصية | السبب |
|---|---|---|
| شاشات كثيرة، وCRUD متمحور حول الكيانات هو الجوهر (طلبات، إدارة بيانات رئيسيّة، إلخ) | EF Core + ترحيل | يقلّ مجموع شيفرة إعادة التعبئة وSQL اليدويّ، وتتبع تغيّر المخطّط بـ dotnet ef migrations |
| متخصّص كتابة ومخطّط صغير (سجلّ قياس، سجلّ تدقيق، تخزين مؤقّت) | Microsoft.Data.Sqlite الخام (+ Dapper إن لزم) |
عبء تتبّع التغيير ضائع. طابور الكتابة + الدفعة في الفصل 5 يُركَّب باستقامة |
| اختلاط الطابعين | الجمع | لا بأس أن تكون شاشات CRUD بـ EF Core وكتابة السجلّ بـ ADO.NET الخام على ملفّ قاعدة البيانات نفسه |
إن اخترت EF Core لزم معرفة قيود خاصّة بموفّر SQLite.10
- إعادة بناء الجدول بسبب قيود ALTER TABLE: SQLite لا يدعم مباشرة تغيير نوع العمود ولا حذفه، فترحيل يتضمّن
AlterColumnأوDropColumnيُنفَّذ بإعادة بناء «إنشاء جدول جديد ← نسخ البيانات ← حذف القديم ← إعادة التسمية». في بيئة كمّ البيانات كبير يؤثّر في زمن التطبيق واستخدام القرص، فخطّط لتغيير مخطّط الجداول الكبيرة. - لا يمكن صنع سكربت متماثل القوّة: لا تُولَّد سكربتات ترحيل بشرط if-then كما في SQL Server. التطبيق الواقعيّ
dbContext.Database.Migrate()عند بدء التطبيق. - عمليّات decimal / DateTimeOffset تقييم عميل: ظروف النوع في الفصل 6 لا تختفي في EF Core أيضاً. المقارنة غير المساواة والترتيب تصير تقييم عميل، فمبدأ حمل المبلغ بأصغر وحدة صحيحة نفسه في EF Core (يمكن التحويل إلى
longعبر محوّل قيمة عند التخزين). - WAL مفعَّل افتراضيّاً: قاعدة البيانات التي ينشئها EF Core في وضع WAL من البداية7، فلا حاجة لإعداد الفصل 4. فهم السلوك ما زال لازماً.
وحتّى عند اعتماد EF Core، استخدام قاعدة SQLite في الذاكرة لاختبار الوحدة في طبقة المستودع كثيراً ما ينفع. تعمل بالموفّر نفسه الذي في الإنتاج فيضيق فراغ «يمرّ في المحاكاة ويسقط على القاعدة الحقيقيّة». فكرة أيّ طبقة تُكتب فيها الاختبارات في «حدود اختبار الوحدة واختبار التكامل».
9. الخلاصة
SQLite مكتبة «الدمج في 30 دقيقة، والتشغيل الصحيح يحتاج تصميماً». غير أنّ التصميم اللازم مقرَّر، وقائمة تحقّق محتوى هذا المقال ستّ نقاط.
- المكتبة
Microsoft.Data.Sqlite(أو EF Core فوقها). لا تخلط بمعلومات تفترضSystem.Data.SQLite - فعِّل وضع WAL من الإصدار الأوّل، وافهم دور
-wal/-shm - وحِّد الكتابة في واحد (طابور كتابة Channels)، وجمِّع INSERT الصغيرة في معاملة
- وحِّد DateTime على UTC، والمبلغ عدد صحيح بأصغر وحدة نقديّة. لا تقارن decimal ولا تجمّعه وهو TEXT
- عند البدء
quick_check، والنسخ الاحتياطيّVACUUM INTOأوBackupDatabase. لا تنسخ الملفّ أثناء التشغيل - لا تنسَ
SqliteConnection.ClearPoolفي معالجة حذف ملفّ قاعدة البيانات أو استبداله
إن كان التكوين الذي تعرفه «نتجاوز database is locked بإعادة المحاولة» أو «نأخذ النسخة بنسخ الملفّ»، فاجرد ببنود هذا المقال مرّة قبل أن يتلف. التصحيح نفسه في كلّها تغيير صغير.
مقالات ذات صلة
- كيف تختار مكان حفظ بيانات تطبيق Windows ── جدول قرار SQLite / JSON / Registry / Access
- حفظ المعلومات السرّيّة في تطبيقات Windows - تجنّب الإعدادات النصّيّة الصريحة عبر DPAPI
- تشغيل دوريّ آمن عبر Task Scheduler
- جدول قرار C# async/await العمليّ - Task.Run وConfigureAwait
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع مراجعة تصميم تطبيقات أعمال تدمج SQLite (الحصر والنسخ الاحتياطيّ وتصميم الترحيل)، وتحقيق أعطال التطبيقات العاملة مثل database is locked وتلف البيانات وتدهور الأداء، ودعم الترحيل من مخازن بيانات قائمة مثل Access.
- الاستشارات التقنيّة ومراجعة التصميم
- تطوير تطبيقات Windows
- دعم استخدام الأصول القائمة والترحيل
- التواصل معنا
المراجع
-
Microsoft Learn, Microsoft.Data.Sqlite overview. حول أنّه موفّر ADO.NET خفيف تصونه Microsoft، وأنّه أساس موفّر EF Core SQLite. ↩ ↩2
-
SQLite, Write-Ahead Logging. حول دور ملفّي -wal / -shm، ونقطة التحقّق (1000 صفحة افتراضيّاً)، وتوازيّة القراءة والكتابة، واستمرار الوضع، وعدم العمل على أنظمة الملفّات الشبكيّة. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Database errors (Microsoft.Data.Sqlite). حول إعادة المحاولة التلقائيّة حتّى مهلة الأمر (30 ثانية افتراضيّاً) أمام أخطاء busy / locked، وكون كائنات الاتّصال والأمر ونظائرها ليست آمنة للخيوط. ↩ ↩2
-
Microsoft Learn, Data types (Microsoft.Data.Sqlite). حول الأنواع الأربعة الأساسيّة في SQLite، وتخطيط DateTime / Guid / decimal إلى TEXT، ووجوب قصر أسماء أنواع الأعمدة أيضاً على أسماء الأنواع الأربعة الأساسيّة. ↩ ↩2 ↩3 ↩4
-
SQLite, How To Corrupt An SQLite Database File. حول أنّ نسخ ملفّ قاعدة البيانات أثناء التشغيل (أثناء المعاملة) وحذف ملفّ المجلّة الساخنة أو WAL أو فصله أسباب تلف. ↩ ↩2 ↩3
-
Microsoft Learn, Connection strings (Microsoft.Data.Sqlite). حول قائمة كلمات سلسلة الاتّصال، وكون Pooling مفعَّلاً افتراضيّاً، وعدم أثر Password إن كانت المكتبة الأصليّة لا تدعم التشفير، وعدم التوصية بالجمع بين Cache=Shared وWAL، وعدم إرسال PRAGMA إن لم يُحدَّد Foreign Keys، وعدم الحاجة إلى التفعيل في مكتبات مبنيّة بـ SQLITE_DEFAULT_FOREIGN_KEYS مثل e_sqlite3. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Async limitations (Microsoft.Data.Sqlite). حول أنّ SQLite لا يدعم إدخالاً/إخراجاً غير متزامن فدوال async تنفيذ متزامن، وكون WAL مفعَّلاً افتراضيّاً في قواعد ينشئها EF Core. ↩ ↩2
-
SQLite, VACUUM. حول أنّ جملة VACUUM INTO تصنع في ملفّ آخر نسخة متّسقة بأصغر حجم دون تغيير الملفّ الأصليّ. ↩
-
Microsoft Learn, Backup (Microsoft.Data.Sqlite). حول التنفيذ الحاليّ الذي ينسخ BackupDatabase بأسرع ما يمكن ويحظر كتابة الاتّصالات الأخرى حتّى الاكتمال. ↩
-
Microsoft Learn, SQLite EF Core Database Provider Limitations. حول تنفيذ كثير من عمليّات الترحيل بإعادة بناء الجدول، وتعذّر توليد سكربت متماثل القوّة، وصيرورة عمليّات decimal / DateTimeOffset تقييم عميل. ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
كيف تختار مكان حفظ بيانات تطبيق Windows محليّاً ── جدول قرار بين SQLite وJSON وRegistry وAccess
أين ينبغي حفظ بيانات تطبيق سطح مكتب Windows، وبأيّ صيغة؟ نرتّب الفرق بين AppData وProgramData، ونقاط قوّة وعيوب كلّ من SQLite وملفّات JSO...
التاريخ والوقت والمناطق الزمنية في تطبيقات الأعمال ── من فخاخ DateTime إلى مبدأ التخزين بـ UTC وتصميم الاختبارات
ينزاح الوقت 9 ساعات بعد نقل الخادم. نرتّب أسباب أعطال التاريخ والوقت انطلاقاً من Kind في DateTime والتحويل الضمني. نشرح التمييز مع DateTi...
كيفيّة إنشاء خدمات Windows وتشغيلها ── من التمييز عن جدولة المهام إلى تحويل BackgroundService إلى خدمة
هل ينبغي تحويل المعالجة المقيمة إلى خدمة Windows، أم تكفي جدولة المهام؟ نرتّب من منظور عمليّ جدول القرار، وكيفيّة إنشاء الخدمة عبر .NET W...
كيف نفهم عزل الجلسات في Windows ── Session 0 وRDP وتشغيل عدة مستخدمين معاً
نوضح مفهوم «الجلسة» الذي يربك كثيراً من مطوّري تطبيقات Windows. نشرح عملياً سبب عزل Session 0 الذي يمنع الخدمة من عرض واجهة، وسلوك الجلسا...
منع التشغيل المتعدّد لتطبيقات Windows ── Mutex المسمّى وتفعيل النافذة عند التشغيل المزدوج
نرتّب تنفيذ منع التشغيل المتعدّد لتطبيقات Windows للأعمال بـ Mutex مسمّى. نشرح الفرق بين Global\ وLocal\ ومطبّاته في بيئات RDP، وAbandone...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- لماذا يظهر «database is locked» في SQLite؟
- يحدث SQLITE_BUSY عندما يحتفظ اتّصال آخر بقفل كتابة. تعيد Microsoft.Data.Sqlite المحاولة تلقائيّاً حتّى بلوغ مهلة الأمر (30 ثانية افتراضيّاً)، لذا لا حاجة عادةً إلى إعادة محاولة يدويّة. إن استمرّ ظهور الاستثناء رغم ذلك، فالسبب معاملة طويلة تتجاوز 30 ثانية، أو تصادم ناتج عن الترقّي من القراءة إلى الكتابة، أو تنازع على القفل بين عمليّات كتابة صغيرة متعدّدة من خيوط عدّة، وزيادة عدد إعادة المحاولات لا تحلّ أيّاً منها. الحلّ الجذريّ هو تقصير المعاملات وتوحيد مسار الكتابة في واحد باستخدام طابور مثل System.Threading.Channels. كما أنّ تفعيل وضع WAL يُزيل معظم حالات الحظر بين القراءة والكتابة.
- ما المكتبة التي ينبغي اختيارها لاستخدام SQLite في C#؟
- في التطوير الجديد، الأساس هو Microsoft.Data.Sqlite، موفِّر ADO.NET الذي تصونه Microsoft (أو موفِّر SQLite الخاصّ بـ EF Core المبنيّ فوقها). بما أنّ حزمة NuGet تتضمَّن ملفّ SQLite الأصليّ نفسه، فلا حاجة إلى عمليّة توزيع منفصلة. وهي مختلفة تماماً عن System.Data.SQLite العريقة من ناحية سلسلة الاتّصال ومعاملة الأنواع؛ فمثلاً يُحفَظ Guid كـ BLOB بشكل افتراضيّ في System.Data.SQLite، بينما يُحفَظ كـ TEXT في Microsoft.Data.Sqlite. انتبه دائماً إلى أيّهما يفترضه المثال الذي تجده على الويب.
- هل يكفي نسخ الملفّ لأخذ نسخة احتياطيّة من SQLite؟
- يُمنَع النسخ المباشر للملفّ أثناء التشغيل. فقد يلتقط حالة معاملة في منتصف تنفيذها، وتنصّ وثائق SQLite الرسميّة صراحةً على أنّ هذا سبب للتلف. في وضع WAL، يحتوي ملفّ -wal على التزامات لم تنعكس بعد في الملفّ الرئيسيّ، لذا فإنّ نسخ الملفّ الرئيسيّ فقط يُفقِد أحدث البيانات. الطريقتان الصحيحتان هما VACUUM INTO (تُنشئ بجملة SQL واحدة نسخة متماسكة بأصغر حجم) أو SqliteConnection.BackupDatabase. لكن بما أنّ BackupDatabase تحظر الكتابة من الاتّصالات الأخرى حتّى الانتهاء، فإنّ VACUUM INTO أكثر أماناً للنسخ الاحتياطيّ الدوريّ اليوميّ.
- لماذا يكون إدخال عدد كبير من السجلّات (INSERT) في SQLite بطيئاً؟
- غالباً ما يكون السبب هو وحدة الالتزام (commit). يقوم SQLite بكتابة متزامنة إلى وحدة التخزين (fsync) عند كلّ التزام، لذا فإنّ تنفيذ INSERT سجلّاً سجلّاً بالتزام ضمنيّ يبلغ سقفه عند بضع مئات إلى بضعة آلاف في الثانية حتّى على SSD. ويكفي تجميع نفس عمليّات INSERT ضمن معاملة صريحة بوحدة 1000 سجلّ (بإحاطتها بـ BeginTransaction وإنهائها بـ Commit) للوصول إلى رتبة عشرات إلى مئات آلاف السجلّات في الثانية، أي تحسين بمقدار رتبتين أو ثلاث رُتَب. وفي المقابل، فإنّ إطالة المعاملة أكثر من اللازم تُعطِّل الكتابات الأخرى، لذا فإنّ جعل بضع مئات إلى بضعة آلاف سجلّ، أو ما يعادل بضع مئات المللي ثانية، التزاماً واحداً هو التسوية العمليّة المعتادة.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.