حفظ البيانات باستخدام SQLite

يعد حفظ البيانات في قاعدة بيانات مثاليًا للبيانات المكررة أو المهيكلة، مثل معلومات الاتصال. تفترض هذه الصفحة أنك على دراية بقواعد بيانات SQL بشكل عام وتساعدك على البدء في قواعد بيانات SQLite على Android. إنّ واجهات برمجة التطبيقات التي ستحتاج إليها لاستخدام قاعدة بيانات على Android متوفرة في حزمة android.database.sqlite.

تنبيه: على الرغم من فعالية واجهات برمجة التطبيقات هذه، إلا أنّها منخفضة المستوى إلى حد ما وتتطلّب قدرًا كبيرًا من الوقت والجهد عند استخدامها:

  • لا يوجد تحقق في وقت التجميع لاستعلامات SQL الأولية. ومع تغيُّر الرسم البياني للبيانات، عليك تعديل طلبات بحث SQL المتأثرة يدويًا. ويمكن أن تستغرق هذه العملية وقتًا طويلاً وتكون عرضة للخطأ.
  • تحتاج إلى استخدام الكثير من الرموز النموذجية للتحويل بين طلبات لغة الاستعلامات البنيوية (SQL) وكائنات البيانات.

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

تعريف المخطط والعقد

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

فئة العقد هي حاوية للثوابت التي تحدد أسماء معرّفات الموارد المنتظمة (URI) والجداول والأعمدة. تسمح لك فئة العقد باستخدام نفس الثوابت عبر جميع الفئات الأخرى في نفس الحزمة. يتيح لك هذا تغيير اسم عمود في مكان واحد ونشره في جميع أنحاء التعليمة البرمجية.

من الطرق الجيدة لتنظيم فئة العقد وضع التعريفات العالمية لقاعدة البيانات بأكملها في المستوى الجذر للفئة. ثم قم بإنشاء فئة داخلية لكل جدول. تقوم كل فئة داخلية بتعداد أعمدة الجدول المقابلة.

ملاحظة: من خلال تنفيذ واجهة BaseColumns، يمكن أن تكتسب الفئة الداخلية حقل مفتاح أساسي يُسمى _ID تتوقعه بعض فئات Android مثل CursorAdapter. ليس مطلوبًا، ولكن يمكن أن يساعد ذلك في عمل قاعدة البيانات بشكل متناغم مع إطار عمل Android.

على سبيل المثال، يحدد العقد التالي اسم الجدول وأسماء الأعمدة لجدول واحد يمثل خلاصة RSS:

Kotlin

object FeedReaderContract {
    // Table contents are grouped together in an anonymous object.
    object FeedEntry : BaseColumns {
        const val TABLE_NAME = "entry"
        const val COLUMN_NAME_TITLE = "title"
        const val COLUMN_NAME_SUBTITLE = "subtitle"
    }
}

Java

public final class FeedReaderContract {
    // To prevent someone from accidentally instantiating the contract class,
    // make the constructor private.
    private FeedReaderContract() {}

    /* Inner class that defines the table contents */
    public static class FeedEntry implements BaseColumns {
        public static final String TABLE_NAME = "entry";
        public static final String COLUMN_NAME_TITLE = "title";
        public static final String COLUMN_NAME_SUBTITLE = "subtitle";
    }
}

إنشاء قاعدة بيانات باستخدام مساعد SQL

بمجرد تحديد كيف تبدو قاعدة البيانات الخاصة بك، يجب عليك تنفيذ الطرق التي تنشئ وتحافظ على قاعدة البيانات والجداول. فيما يلي بعض العبارات النموذجية التي تنشئ جدولاً وتحذفه:

Kotlin

private const val SQL_CREATE_ENTRIES =
        "CREATE TABLE ${FeedEntry.TABLE_NAME} (" +
                "${BaseColumns._ID} INTEGER PRIMARY KEY," +
                "${FeedEntry.COLUMN_NAME_TITLE} TEXT," +
                "${FeedEntry.COLUMN_NAME_SUBTITLE} TEXT)"

private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${FeedEntry.TABLE_NAME}"

Java

private static final String SQL_CREATE_ENTRIES =
    "CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
    FeedEntry._ID + " INTEGER PRIMARY KEY," +
    FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
    FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)";

private static final String SQL_DELETE_ENTRIES =
    "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;

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

تحتوي الفئة SQLiteOpenHelper على مجموعة مفيدة من واجهات برمجة التطبيقات لإدارة قاعدة البيانات. عند استخدام هذا الفئة للحصول على مراجع لقاعدة بياناتك، لا ينفِّذ النظام سوى العمليات التي يُحتمل أن تكون طويلة الأمد لإنشاء قاعدة البيانات وتحديثها عند الحاجة وليس أثناء بدء تشغيل التطبيق. ما عليك سوى الاتصال برقم getWritableDatabase() أو getReadableDatabase().

ملاحظة: نظرًا لأن هذه الخدمات قد تستمر لفترة طويلة، احرص على الاتصال بـ getWritableDatabase() أو getReadableDatabase() في سلسلة محادثات في الخلفية. لمزيد من المعلومات، يُرجى الاطّلاع على سلسلة المحادثات على Android.

لاستخدام SQLiteOpenHelper، يجب إنشاء فئة فرعية تلغي طريقتي استدعاء onCreate() وonUpgrade(). يمكنك أيضًا تنفيذ طريقة onDowngrade() أو onOpen()، إلا أنّها ليست مطلوبة.

على سبيل المثال، في ما يلي تنفيذ SQLiteOpenHelper يستخدم بعض الأوامر الموضحة أعلاه:

Kotlin

class FeedReaderDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL(SQL_CREATE_ENTRIES)
    }
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // This database is only a cache for online data, so its upgrade policy is
        // to simply to discard the data and start over
        db.execSQL(SQL_DELETE_ENTRIES)
        onCreate(db)
    }
    override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        onUpgrade(db, oldVersion, newVersion)
    }
    companion object {
        // If you change the database schema, you must increment the database version.
        const val DATABASE_VERSION = 1
        const val DATABASE_NAME = "FeedReader.db"
    }
}

Java

public class FeedReaderDbHelper extends SQLiteOpenHelper {
    // If you change the database schema, you must increment the database version.
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "FeedReader.db";

    public FeedReaderDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_ENTRIES);
    }
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // This database is only a cache for online data, so its upgrade policy is
        // to simply to discard the data and start over
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }
}

للوصول إلى قاعدة البيانات، أنشئ مثيلاً للفئة الفرعية SQLiteOpenHelper:

Kotlin

val dbHelper = FeedReaderDbHelper(context)

Java

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

وضع المعلومات في قاعدة بيانات

أدخِل البيانات في قاعدة البيانات من خلال تمرير كائن ContentValues إلى طريقة insert():

Kotlin

// Gets the data repository in write mode
val db = dbHelper.writableDatabase

// Create a new map of values, where column names are the keys
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
    put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle)
}

// Insert the new row, returning the primary key value of the new row
val newRowId = db?.insert(FeedEntry.TABLE_NAME, null, values)

Java

// Gets the data repository in write mode
SQLiteDatabase db = dbHelper.getWritableDatabase();

// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);

// Insert the new row, returning the primary key value of the new row
long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);

الوسيطة الأولى لـ insert() هي اسم الجدول ببساطة.

تخبر الوسيطة الثانية إطار العمل بما يجب فعله في حال كانت قيمة ContentValues فارغة (أي لم يتم عرض put أي قيم). إذا حدَّدت اسم عمود، سيدرج إطار العمل صفًا ويضبط قيمة هذا العمود على قيمة فارغة. إذا حددت null، كما هو الحال في عينة التعليمات البرمجية هذه، لن يدرج إطار العمل صفًا في حال عدم وجود قيم.

تعرض طرق insert() المعرف للصف الذي تم إنشاؤه حديثًا، أو ستعرض -1 إذا حدث خطأ أثناء إدخال البيانات. يمكن أن يحدث هذا إذا كان لديك تعارض مع البيانات الموجودة مسبقًا في قاعدة البيانات.

قراءة المعلومات من قاعدة بيانات

للقراءة من قاعدة بيانات، استخدِم الإجراء query()، واضبط معايير الاختيار والأعمدة المطلوبة له. تجمع الطريقة عناصر insert() وupdate()، باستثناء أن قائمة الأعمدة تحدِّد البيانات التي تريد جلبها ("الإسقاط")، بدلاً من البيانات المراد إدراجها. ويتم إرجاع نتائج الاستعلام إليك في كائن Cursor.

Kotlin

val db = dbHelper.readableDatabase

// Define a projection that specifies which columns from the database
// you will actually use after this query.
val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE)

// Filter results WHERE "title" = 'My Title'
val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?"
val selectionArgs = arrayOf("My Title")

// How you want the results sorted in the resulting Cursor
val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC"

val cursor = db.query(
        FeedEntry.TABLE_NAME,   // The table to query
        projection,             // The array of columns to return (pass null to get all)
        selection,              // The columns for the WHERE clause
        selectionArgs,          // The values for the WHERE clause
        null,                   // don't group the rows
        null,                   // don't filter by row groups
        sortOrder               // The sort order
)

Java

SQLiteDatabase db = dbHelper.getReadableDatabase();

// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
    };

// Filter results WHERE "title" = 'My Title'
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// How you want the results sorted in the resulting Cursor
String sortOrder =
    FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query
    projection,             // The array of columns to return (pass null to get all)
    selection,              // The columns for the WHERE clause
    selectionArgs,          // The values for the WHERE clause
    null,                   // don't group the rows
    null,                   // don't filter by row groups
    sortOrder               // The sort order
    );

يتم دمج الوسيطتين الثالثة والرابعة (selection وselectionArgs) لإنشاء عبارة WHERE. نظرًا لأنه يتم توفير الوسيطات بشكل منفصل عن استعلام التحديد، يتم تخطيها قبل دمجها. وهذا يجعل عبارات التحديد الخاصة بك محمية من حقن SQL. لمزيد من التفاصيل حول جميع الوسيطات، اطّلِع على مرجع query().

لإلقاء نظرة على صفّ في المؤشر، استخدِم إحدى طرق النقل Cursor، والتي يجب دائمًا طلبها قبل البدء في قراءة القيم. بما أنّ المؤشر يبدأ من الموضع -1، يؤدي استدعاء moveToNext() إلى وضع "موضع القراءة" على الإدخال الأول في النتائج ويعرض ما إذا كان المؤشر قد تجاوز آخر إدخال في مجموعة النتائج أم لا. بالنسبة إلى كلّ صف، يمكنك قراءة قيمة العمود عن طريق استدعاء إحدى طرق الحصول على Cursor، مثل getString() أو getLong(). يجب ضبط موضع الفهرس للعمود المطلوب لكل طريقة من طرق الالحصول على الفهرس من خلال استدعاء getColumnIndex() أو getColumnIndexOrThrow(). عند الانتهاء من التكرار خلال النتائج، استدعِ close() على المؤشر لتحرير موارده. على سبيل المثال، يوضّح ما يلي كيفية الحصول على جميع معرّفات العناصر المخزّنة في مؤشر وإضافتها إلى قائمة:

Kotlin

val itemIds = mutableListOf<Long>()
with(cursor) {
    while (moveToNext()) {
        val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID))
        itemIds.add(itemId)
    }
}
cursor.close()

Java

List itemIds = new ArrayList<>();
while(cursor.moveToNext()) {
  long itemId = cursor.getLong(
      cursor.getColumnIndexOrThrow(FeedEntry._ID));
  itemIds.add(itemId);
}
cursor.close();

حذف المعلومات من قاعدة بيانات

لحذف الصفوف من جدول، عليك توفير معايير اختيار تحدّد الصفوف من خلال طريقة delete(). تعمل الآلية نفسها التي تعمل بها وسيطات الاختيار على الطريقة query(). حيث تقسم مواصفات الاختيار إلى عبارة التحديد ووسيطات الاختيار. تحدد العبارة الأعمدة التي يجب النظر فيها، وتسمح لك أيضًا بدمج اختبارات الأعمدة. الوسيطات هي قيم يجب اختبارها وتكون مرتبطة بالعبارة. نظرًا لأنه لا يتم التعامل مع النتيجة بنفس التعامل مع عبارة SQL العادية، فإنها محمية من حقن SQL.

Kotlin

// Define 'where' part of query.
val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
// Specify arguments in placeholder order.
val selectionArgs = arrayOf("MyTitle")
// Issue SQL statement.
val deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs)

Java

// Define 'where' part of query.
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
// Specify arguments in placeholder order.
String[] selectionArgs = { "MyTitle" };
// Issue SQL statement.
int deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);

تشير القيمة المعروضة لطريقة delete() إلى عدد الصفوف التي تم حذفها من قاعدة البيانات.

تعديل قاعدة بيانات

لتعديل مجموعة فرعية من قيم قاعدة البيانات، استخدِم أسلوب update().

يؤدي تعديل الجدول إلى دمج بنية ContentValues لـ insert() مع بنية WHERE لـ delete().

Kotlin

val db = dbHelper.writableDatabase

// New value for one column
val title = "MyNewTitle"
val values = ContentValues().apply {
    put(FeedEntry.COLUMN_NAME_TITLE, title)
}

// Which row to update, based on the title
val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
val selectionArgs = arrayOf("MyOldTitle")
val count = db.update(
        FeedEntry.TABLE_NAME,
        values,
        selection,
        selectionArgs)

Java

SQLiteDatabase db = dbHelper.getWritableDatabase();

// New value for one column
String title = "MyNewTitle";
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);

// Which row to update, based on the title
String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
String[] selectionArgs = { "MyOldTitle" };

int count = db.update(
    FeedReaderDbHelper.FeedEntry.TABLE_NAME,
    values,
    selection,
    selectionArgs);

والقيمة المعروضة لطريقة update() هي عدد الصفوف المتأثرة في قاعدة البيانات.

اتصال قاعدة البيانات المستمر

نظرًا لأن الاتصال باللغتين getWritableDatabase() وgetReadableDatabase() مكلف عند إغلاق قاعدة البيانات، يجب ترك اتصال قاعدة البيانات مفتوحًا طالما أنك ربما تحتاج إلى الوصول إليه. عادةً ما يكون من الأفضل إغلاق قاعدة البيانات في onDestroy() لنشاط الاتصال.

Kotlin

override fun onDestroy() {
    dbHelper.close()
    super.onDestroy()
}

Java

@Override
protected void onDestroy() {
    dbHelper.close();
    super.onDestroy();
}

تصحيح أخطاء قاعدة البيانات

تتضمّن حزمة تطوير البرامج (SDK) لنظام التشغيل Android أداة هيكل sqlite3 تتيح لك تصفُّح محتوى الجدول وتشغيل أوامر SQL وتنفيذ دوال أخرى مفيدة في قواعد بيانات SQLite. لمعرفة مزيد من المعلومات، يمكنك الاطّلاع على طريقة كيفية إصدار أوامر واجهة المستخدم.