Lưu dữ liệu bằng SQLite

Lưu dữ liệu vào cơ sở dữ liệu là lựa chọn lý tưởng dành cho dữ liệu lặp hoặc dữ liệu có cấu trúc, chẳng hạn như thông tin liên hệ. Trang này giả định rằng bạn đã quen dùng cơ sở dữ liệu SQL nói chung và hướng dẫn bạn cách bắt đầu sử dụng cơ sở dữ liệu SQLite trên Android. Những API mà bạn cần có để sử dụng cơ sở dữ liệu trên Android được cung cấp trong gói android.database.sqlite.

Thận trọng: Mặc dù những API này rất mạnh mẽ, nhưng lại ở cấp khá thấp và đòi hỏi nhiều thời gian cũng như công sức khi sử dụng:

  • Không có phương thức xác minh thời gian biên dịch đối với các truy vấn SQL thô. Khi biểu đồ dữ liệu thay đổi, bạn cần phải cập nhật các truy vấn SQL bị ảnh hưởng theo cách thủ công. Quá trình này có thể tốn nhiều thời gian và dễ xảy ra lỗi.
  • Bạn cần phải dùng nhiều mã nguyên mẫu để chuyển đổi giữa các đối tượng dữ liệu và truy vấn SQL.

Vì những lý do này, bạn nên dùng Thư viện lưu trữ Phòng làm lớp trừu tượng để truy cập thông tin trong cơ sở dữ liệu SQLite của ứng dụng.

Xác định giản đồ và hợp đồng

Một trong những nguyên tắc chính của cơ sở dữ liệu SQL là giản đồ: nội dung khai báo chính thức về cách sắp xếp cơ sở dữ liệu. Giản đồ này được phản ánh trong câu lệnh SQL mà bạn dùng để tạo cơ sở dữ liệu. Bạn nên tạo một lớp đồng hành, còn gọi là lớp hợp đồng. Lớp này chỉ định rõ bố cục của giản đồ theo cách có hệ thống và tự ghi lại.

Lớp hợp đồng là một vùng chứa cho các hằng số xác định tên của URI, bảng và cột. Lớp hợp đồng cho phép bạn sử dụng cùng một hằng số trên tất cả các lớp khác trong cùng gói. Nhờ vậy, bạn có thể thay đổi tên cột ở một nơi và tự động áp dụng thay đổi đó trên toàn bộ mã của mình.

Một cách hiệu quả để sắp xếp lớp hợp đồng là đặt các định nghĩa dùng chung cho toàn bộ cơ sở dữ liệu của bạn vào cấp độ gốc của lớp. Sau đó, tạo một lớp bên trong cho từng bảng. Mỗi lớp bên trong sẽ liệt kê các cột của bảng tương ứng.

Lưu ý: Khi triển khai giao diện BaseColumns, lớp bên trong của bạn có thể kế thừa trường khoá chính có tên _ID mà một số lớp Android như CursorAdapter muốn có. Tuy không bắt buộc nhưng trường này có thể giúp cơ sở dữ liệu hoạt động hài hoà với khung Android.

Ví dụ: hợp đồng sau đây xác định tên bảng và tên cột cho một bảng duy nhất biểu thị nguồn cấp dữ liệu 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";
    }
}

Tạo cơ sở dữ liệu bằng trình trợ giúp SQL

Sau khi xác định giao diện của cơ sở dữ liệu, bạn nên triển khai các phương thức giúp tạo cũng như duy trì cơ sở dữ liệu và bảng. Dưới đây là một số câu lệnh thường dùng để tạo và xoá bảng:

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;

Cũng giống như các tệp bạn lưu vào bộ nhớ trong của thiết bị, Android lưu trữ cơ sở dữ liệu của bạn trong thư mục riêng tư của ứng dụng. Dữ liệu của bạn được bảo mật vì theo mặc định, các ứng dụng khác hoặc người dùng không thể truy cập vào khu vực này.

Lớp SQLiteOpenHelper chứa tập hợp API hữu ích để quản lý cơ sở dữ liệu. Khi bạn dùng lớp này để lấy thông tin tham chiếu đến cơ sở dữ liệu của mình, hệ thống sẽ thực hiện các thao tác tạo và cập nhật cơ sở dữ liệu có khả năng kéo dài chỉ khi cần thiết, chứ không phải lúc khởi động ứng dụng. Tất cả những gì bạn cần làm là gọi getWritableDatabase() hoặc getReadableDatabase().

Lưu ý: Vì các thao tác này có thể kéo dài, hãy nhớ gọi getWritableDatabase() hoặc getReadableDatabase() trên luồng trong nền. Bạn có thể xem phần Tạo luồng trên Android để biết thêm thông tin.

Để sử dụng SQLiteOpenHelper, hãy tạo một lớp con ghi đè phương thức gọi lại onCreate()onUpgrade(). Ngoài ra, bạn cũng nên triển khai các phương thức onDowngrade() hoặc onOpen(), nhưng không bắt buộc.

Ví dụ: dưới đây là cách triển khai SQLiteOpenHelper sử dụng một số lệnh nêu trên:

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);
    }
}

Để truy cập vào cơ sở dữ liệu của bạn, hãy tạo bản sao lớp con của SQLiteOpenHelper:

Kotlin

val dbHelper = FeedReaderDbHelper(context)

Java

FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());

Đưa thông tin vào cơ sở dữ liệu

Chèn dữ liệu vào cơ sở dữ liệu bằng cách chuyển đối tượng ContentValues vào phương thức 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);

Đối số đầu tiên của insert() chỉ đơn thuần là tên bảng.

Đối số thứ hai sẽ cho khung biết việc nên làm trong trường hợp ContentValues bị trống (tức là bạn không put giá trị nào). Nếu bạn chỉ định tên của một cột, khung sẽ chèn một hàng và đặt giá trị của cột đó thành rỗng. Nếu bạn chỉ định null, như trong mã mẫu này, khung sẽ không chèn hàng khi không có giá trị.

Phương thức insert() sẽ trả về mã nhận dạng cho hàng mới tạo hoặc sẽ trả về -1 nếu xảy ra lỗi khi chèn dữ liệu. Trường hợp này có thể xảy ra nếu bạn gặp xung đột với dữ liệu có sẵn từ trước trong cơ sở dữ liệu.

Đọc thông tin từ cơ sở dữ liệu

Để đọc từ một cơ sở dữ liệu, hãy dùng phương thức query(), chuyển phương thức đó vào tiêu chí lựa chọn và cột mong muốn. Phương thức này kết hợp các thành phần của insert()update(), ngoại trừ danh sách cột xác định dữ liệu bạn muốn tìm nạp ("phép chiếu"), thay vì dữ liệu cần chèn. Kết quả của truy vấn sẽ được trả về cho bạn trong đối tượng 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
    );

Đối số thứ ba và thứ tư (selectionselectionArgs) được kết hợp để tạo mệnh đề WHERE. Vì được cung cấp riêng biệt với truy vấn lựa chọn nên các đối số này sẽ được thoát trước khi kết hợp. Nhờ vậy, các câu lệnh lựa chọn của bạn sẽ miễn nhiễm với lệnh chèn SQL. Để biết thêm thông tin chi tiết về tất cả đối số, hãy xem tài liệu tham khảo về query().

Để xem xét một hàng trong con trỏ, hãy dùng một trong các phương thức di chuyển Cursor. Bạn phải luôn gọi phương thức này trước khi bắt đầu đọc giá trị. Vì con trỏ bắt đầu ở vị trí -1, nên lệnh gọi moveToNext() sẽ đặt "vị trí đọc" ở mục đầu tiên trong kết quả và trả về thông tin cho biết liệu con trỏ đã đi qua mục cuối cùng trong tập kết quả hay chưa. Đối với mỗi hàng, bạn có thể đọc giá trị của một cột bằng cách gọi một trong các phương thức get Cursor, chẳng hạn như getString() hoặc getLong(). Đối với mỗi phương thức get, bạn phải chuyển vị trí chỉ mục của cột mong muốn. Bạn có thể lấy vị trí này bằng cách gọi getColumnIndex() hoặc getColumnIndexOrThrow(). Khi bạn hoàn tất quá trình lặp theo kết quả, hãy gọi close() trên con trỏ để giải phóng tài nguyên. Ví dụ: nội dung sau đây cho biết cách lấy tất cả mã mục được lưu trữ trong con trỏ và thêm các mã đó vào danh sách:

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();

Xoá thông tin khỏi cơ sở dữ liệu

Để xoá hàng khỏi bảng, bạn cần cung cấp tiêu chí lựa chọn (giúp xác định hàng) cho phương thức delete(). Cơ chế này hoạt động giống như các đối số lựa chọn cho phương thức query(). Cơ chế này chia quy cách lựa chọn thành một mệnh đề lựa chọn và các đối số lựa chọn. Mệnh đề này xác định cột cần xem xét, đồng thời cho phép bạn kết hợp các quá trình kiểm thử cột. Đối số là các giá trị cần kiểm thử dựa trên đối số được đưa vào mệnh đề. Vì được xử lý không giống như câu lệnh SQL thông thường, nên kết quả này miễn nhiễm với lệnh chèn 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);

Giá trị trả về của phương thức delete() cho biết số hàng đã bị xoá khỏi cơ sở dữ liệu.

Cập nhật cơ sở dữ liệu

Khi bạn cần sửa đổi một nhóm nhỏ giá trị cơ sở dữ liệu, hãy dùng phương thức update().

Thao tác cập nhật bảng sẽ kết hợp cú pháp ContentValues của insert() với cú pháp WHERE của 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);

Giá trị trả về của phương thức update() là số hàng bị ảnh hưởng trong cơ sở dữ liệu.

Duy trì kết nối cơ sở dữ liệu

getWritableDatabase()getReadableDatabase() khá tốn kém nếu gọi khi cơ sở dữ liệu bị đóng. Vì thế, bạn nên duy trì kết nối cơ sở dữ liệu trong suốt thời gian cần truy cập (nếu có thể). Thông thường, bạn nên đóng cơ sở dữ liệu trong onDestroy() của Hoạt động gọi.

Kotlin

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

Java

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

Gỡ lỗi cơ sở dữ liệu

SDK Android có chứa công cụ shell sqlite3 cho phép bạn duyệt xem nội dung trong bảng, chạy lệnh SQL và thực hiện các hàm hữu ích khác trên cơ sở dữ liệu SQLite. Để biết thêm thông tin, hãy xem cách gửi lệnh shell.