ذخیره کردن داده‌ها بااستفاده از SQLite

ذخیره کردن داده‌ها در پایگاه داده برای داده‌های تکراری یا ساختاریافته، مثل اطلاعات تماس، ایده‌آل است. این صفحه فرض می‌کند که شما به‌طور کلی با پایگاه‌های داده SQL آشنا هستید و به شما کمک می‌کند تا با پایگاه‌های داده SQLite در Android شروع به کار کنید. میاناهای برنامه‌سازی کاربردی که برای استفاده از پایگاه داده در Android نیاز دارید در بسته android.database.sqlite دردسترس است.

احتیاط: اگرچه این میاناهای برنامه‌سازی کاربردی قدرتمند هستند، اما نسبتاً سطح پایین هستند و استفاده از آن‌ها به زمان و تلاش زیادی نیاز دارد:

  • هیچ درستی‌سنجی زمان ترجمه‌ای برای پُرسمان‌های SQL خام وجود ندارد. با تغییر گراف داده‌هایتان، باید پُرسمان‌های SQL تحت‌تأثیرقرارگرفته را به‌صورت دستی به‌روز کنید. این فرایند می‌تواند زمان‌بر و مستعد خطا باشد.
  • برای تبدیل بین پُرسمان‌های SQL و اشیای داده باید از کد متن استاندارد زیادی استفاده کنید.

به این دلایل، اکیداً توصیه می‌کنیم از کتابخانه پایداری Room به‌عنوان لایه انتزاعی برای دسترسی به اطلاعات در پایگاه‌های داده SQLite برنامه‌تان استفاده کنید.

تعریف کردن طرح‌واره و قرارداد

یکی از اصول اصلی پایگاه‌های داده SQL، طرح‌واره است: یک اعلامیه رسمی از نحوه سازماندهی پایگاه داده. طرح‌واره در عبارت‌های SQL که برای ایجاد پایگاه داده استفاده می‌کنید منعکس می‌شود. ممکن است ایجاد یک کلاس همراه، معروف به کلاس قرارداد، که به‌طور صریح طرح‌بندی طرح‌واره شما را به روشی سیستماتیک و خودسندسازی مشخص می‌کند، برای شما مفید باشد.

کلاس قرارداد محفظه‌ای برای ثابت‌هایی است که نام‌های URI، جدول‌ها، و ستون‌ها را تعریف می‌کنند. کلاس قرارداد به شما امکان می‌دهد از ثابت‌های یکسان در همه کلاس‌های دیگر در همان بسته استفاده کنید. این کار به شما امکان می‌دهد نام ستون را در یک جا تغییر دهید و آن را در سراسر کدتان منتشر کنید.

یک راه خوب برای سازمان‌دهی کلاس قرارداد این است که تعاریفی را که برای کل پایگاه داده شما سراسری هستند در سطح ریشه کلاس قرار دهید. سپس یک کلاس داخلی برای هر جدول ایجاد کنید. هر کلاس داخلی ستون‌های جدول مربوطه را برمی‌شمارد.

توجه: با پیاده‌سازی میانای BaseColumns، کلاس داخلی شما می‌تواند فیلد کلید اصلی‌ای به‌نام _ID را به ارث ببرد که برخی کلاس‌های Android مانند CursorAdapter انتظار دارند داشته باشد. این کار الزامی نیست، اما می‌تواند به پایگاه داده شما کمک کند به‌طور هماهنگ با چارچوب Android کار کند.

برای مثال، قرارداد زیر نام جدول و نام ستون‌ها را برای یک جدول تکی که نشان‌دهنده فید RSS است تعریف می‌کند:

کاتلین

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"
    }
}

جاوا

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

وقتی ظاهر پایگاه داده‌تان را تعریف کردید، باید روش‌هایی را پیاده‌سازی کنید که پایگاه داده و جدول‌ها را ایجاد و نگهداری می‌کنند. در اینجا چند عبارت معمول برای ایجاد و حذف جدول آورده شده است:

کاتلین

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}"

جاوا

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 را می‌بینید که از برخی‌از فرمان‌های نشان‌داده‌شده در بالا استفاده می‌کند:

کاتلین

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"
    }
}

جاوا

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 را نمونه‌سازی کنید:

کاتلین

val dbHelper = FeedReaderDbHelper(context)

جاوا

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

وارد کردن اطلاعات در پایگاه داده

با ارسال شیء ContentValues به روش insert()، داده‌ها را در پایگاه داده درج کنید:

کاتلین

// 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)

جاوا

// 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 به شما برگردانده می‌شود.

کاتلین

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
)

جاوا

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 استفاده کنید که همیشه باید قبل‌از شروع خواندن مقادیر آن را فراخوانی کنید. ازآنجایی‌که مکان‌نما از موقعیت ۱- شروع می‌شود، فراخوانی moveToNext() «موقعیت خواندن» را روی اولین ورودی در نتایج قرار می‌دهد و برمی‌گرداند که آیا مکان‌نما از آخرین ورودی در مجموعه نتایج عبور کرده است یا نه. برای هر ردیف، می‌توانید مقدار ستون را با فراخوانی یکی از Cursor روش‌های دریافت، مانند getString() یا getLong() بخوانید. برای هرکدام از روش‌های دریافت، باید موقعیت نمایه ستون موردنظرتان را ارسال کنید، که می‌توانید آن را با فراخوانی getColumnIndex() یا getColumnIndexOrThrow() دریافت کنید. وقتی تکرار نتایج تمام شد، close() را در مکان‌نما فراخوانی کنید تا منابع آن آزاد شود. برای مثال، در زیر نشان داده شده است که چگونه می‌توان همه شناسه‌های مورد ذخیره‌شده در مکان‌نما را دریافت کرد و آن‌ها را به فهرست اضافه کرد:

کاتلین

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

جاوا

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

حذف اطلاعات از پایگاه داده

برای حذف ردیف‌ها از جدول، باید معیارهای انتخابی ارائه دهید که ردیف‌ها را برای روش delete() شناسایی کند. سازوکار به همان روش آرگومان‌های انتخاب برای روش query() کار می‌کند. این روش مشخصات انتخاب را به یک عبارت انتخاب و آرگومان‌های انتخاب تقسیم می‌کند. بند ستون‌های موردنظر را تعریف می‌کند و همچنین به شما امکان می‌دهد آزمایش‌های ستون را ترکیب کنید. آرگومان‌ها مقادیری برای آزمایش هستند که به عبارت محدود شده‌اند. چون با نتیجه مانند بیانیه SQL معمولی رفتار نمی‌شود، دربرابر تزریق SQL مصون است.

کاتلین

// 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)

جاوا

// 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() ترکیب می‌کند.

کاتلین

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)

جاوا

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() «فعالیت» فراخواننده ببندید.

کاتلین

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

جاوا

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

اشکال‌زدایی پایگاه داده

کیت توسعه نرم‌افزار Android شامل ابزار پوسته sqlite3 است که به شما امکان می‌دهد محتوای جدول را مرور کنید، فرمان‌های SQL را اجرا کنید، و عملکردهای مفید دیگری را در پایگاه‌های داده SQLite انجام دهید. برای اطلاعات بیشتر، نحوه صدور فرمان‌های پوسته را ببینید.