قواعد البيانات المحلية مع SQLite
قواعد البيانات المحلية مع SQLite
يأتي كل جهاز Android مزوّدًا بمحرك SQLite كامل مدمج في نظام التشغيل. عندما يحتاج تطبيقك إلى تخزين بيانات منظّمة وعلائقية — كقائمة مهام بفئات، أو سجل رسائل بطوابع زمنية، أو كتالوج منتجات بأسعار — فإن قاعدة بيانات SQLite المحلية هي الأداة المناسبة. SharedPreferences مخصصة لإعدادات المفتاح والقيمة؛ أما SQLite فهي لكل ما يشبه الجداول.
يعلّمك هذا الدرس كيفية إدارة قاعدة بيانات SQLite في Android باستخدام فئة SQLiteOpenHelper وواجهة برمجة التطبيقات الخام android.database.sqlite. في الدرس التالي ستضع طبقة Room فوق ذلك؛ وفهم ما تخفيه Room هو ما يمكّنك من تشخيص أخطائها حين تتصرف بشكل غير متوقع.
كيف تدير Android ملف قاعدة البيانات
تخزّن Android قاعدة بيانات SQLite لكل تطبيق كملف ضمن دليل البيانات الخاصة بالتطبيق: /data/data/<package>/databases/<name>.db. لا يمكن لأي تطبيق آخر قراءته. يُنشأ الملف في أول مرة تفتح فيها قاعدة البيانات — لا تنشئه يدويًا أبدًا.
نقطة الدخول إلى دورة الحياة هي SQLiteOpenHelper. تُنشئ منها فئة فرعية، وتحدد رقم إصدار المخطط، وتُعيد تعريف طريقتين:
onCreate(SQLiteDatabase db)— تُستدعى مرة واحدة فقط، في المرة الأولى التي يُنشأ فيها ملف قاعدة البيانات. ضع جملCREATE TABLEهنا.onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)— تُستدعى في كل مرة ترفع فيها رقم الإصدار في المُنشئ. نفّذ هجرة المخطط هنا؛ لا تحذف جداول دون توفير طريقة للحفاظ على البيانات.
super() يُخزَّن داخل ملف قاعدة البيانات. إن نشرت APK جديدًا برقم إصدار أعلى، تستدعي Android تلقائيًا onUpgrade على جهاز المستخدم. إن غيّرت المخطط دون رفع رقم الإصدار، لن يرى المستخدمون الحاليون التغيير أبدًا.
إنشاء فئة المساعد
أدناه مساعد كامل لقاعدة بيانات إدارة مهام بسيطة. فيها جدول واحد tasks مع مفتاح أساسي يزداد تلقائيًا، وعنوان، ومستوى أولوية، وراية منطقية للإنجاز.
TaskDbHelper.COL_TITLE في كل الكود يعني أن أي خطأ إملائي يتحوّل إلى خطأ في وقت الترجمة وليس تعطّلًا في وقت التشغيل. لا تكتب أسماء الأعمدة كسلاسل نصية حرفية في أماكن متعددة أبدًا.
إدراج البيانات
لكتابة صف تحصل على مقبض قاعدة بيانات قابل للكتابة، تملأ خريطة ContentValues، ثم تستدعي insert(). القيمة المُعادة هي معرّف _id للصف الجديد، أو -1 عند الفشل.
db.close() بعد كل عملية. تخزّن SQLiteOpenHelper مقبض قاعدة البيانات داخليًا. إغلاقه بعد كل استدعاء يُجبر على إعادة فتح مكلفة في المرة التالية. بدلًا من ذلك، دع المساعد يعيش كـ singleton طويل الأمد (نطاق Application أو مستودع مُحقَن بالاعتماديات) وأغلقه فقط في Application.onTerminate() — الذي نادرًا ما يُستدعى على الأجهزة الحقيقية. يُستعيد نظام التشغيل مقبض الملف عند موت العملية.
الاستعلام عن البيانات
يُعدّ db.query() طريقة مساعدة منظّمة تبني جملة SELECT نيابةً عنك. تُعيد كائن Cursor — مؤشر على مجموعة النتائج تتكرر عليه صفًا بصف. أغلق المؤشر دائمًا في كتلة finally أو باستخدام try-with-resources.
? ومرّر القيم في selectionArgs. تسلسل مدخلات المستخدم مباشرةً في سلسلة الاختيار يُعرّض تطبيقك لحقن SQL — حتى في قاعدة بيانات محلية، يمكن للمدخلات المشوّهة أن تُفسد البيانات أو تحذفها.
تحديث الصفوف وحذفها
كلتا العمليتين تتبعان نفس نمط الإدراج: احصل على مقبض قابل للكتابة، وصف ما تريد تغييره، ومرّر معايير الاختيار عبر المحددات ?.
تنفيذ SQL خام
للاستعلامات المعقدة — الانضمامات متعددة الجداول، والاستعلامات الفرعية، والتجميعات — استخدم db.rawQuery(). ما زالت تقبل المحددات ? للسلامة وتُعيد Cursor مثل query().
هجرات المخطط في الممارسة
لنفترض أن الإصدار 2 من تطبيقك يضيف عمود due_date. ترفع رقم الإصدار إلى 2 وتتعامل مع كلا مسارَي الترقية:
if (oldVersion < N) المتسلسل، وليس switch. مستخدم يرقّي من الإصدار 1 إلى 3 يجب أن يمرّ بجميع الهجرات الوسيطة. كل كتلة if تراكمية، فتُنفَّذ كلها بالتسلسل لذلك المستخدم.
اعتبارات الخيوط
تعدّ getWritableDatabase() وgetReadableDatabase() آمنتَين للخيوط في حد ذاتهما، لكن كائن SQLiteDatabase المُعاد ليس آمنًا للخيوط إن شاركته عبر خيوط متعددة دون تزامن. القاعدة الأسلم للـ SQLite الخام: نفّذ جميع عمليات قاعدة البيانات خارج الخيط الرئيسي. استخدم Executor أو أنماط LiveData التي ستتعلمها مع Room. إعاقة الخيط الرئيسي بعملية قاعدة بيانات تُسبّب أخطاء ANR (التطبيق لا يستجيب) تحت الحمل.
الخلاصة
تمنحك SQLite على Android قاعدة بيانات علائقية كاملة في ملف واحد. أنشئ فئة فرعية من SQLiteOpenHelper، وعرّف مخططك في onCreate، وهاجره في onUpgrade، ونفّذ عمليات CRUD عبر واجهة ContentValues وCursor. استخدم المحددات ? دائمًا، وأغلق المؤشرات دائمًا، وانقل عمل قاعدة البيانات دائمًا خارج الخيط الرئيسي. في الدرس التالي سترى كيف تلفّ Room هذا المحرك نفسه بأمان الأنواع، والتحقق من الاستعلامات في وقت الترجمة، وتكامل LiveData.