使用 SQLite 儲存資料

對於重複資料或結構化資料 (如聯絡資訊),將資料儲存至資料庫是理想的選擇。本頁面假設您熟悉 SQL 資料庫的一般知識,並協助您開始使用 Android 上的 SQLite 資料庫。android.database.sqlite 套件提供在 Android 上使用資料庫所需的 API。

注意:這些 API 雖然非常強大,卻非常低階,且需要大量時間和精力:

  • 原始 SQL 查詢無法進行編譯時間驗證。資料圖發生變更時,必須手動更新受影響的 SQL 查詢。這個程序可能很耗時且容易出錯。
  • 您必須使用大量樣板程式碼,才能在 SQL 查詢與資料物件之間進行轉換。

因此,我們強烈建議使用 Room 持續性資料庫做為抽象層,用於存取應用程式 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 helper 建立資料庫

定義資料庫的外觀之後,請實作建立及維護資料庫和資料表的方法。以下列出了一些建立及刪除資料表的常見陳述式:

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 類別包含一組實用 API,可用於管理您的資料庫。使用此類別取得資料庫的參考資料時,僅在必要時,而不是在應用程式啟動期間,才會執行可能需要長時間執行的資料庫建立和更新作業。您只需要呼叫 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() 方法會傳回新建立的資料列 ID,如果插入資料時發生錯誤,則會傳回 -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
    );

第三個引數和第四個引數 (selectionselectionArgs) 會合併以建立 WHERE 子句。由於引數與選項查詢分開提供,所以會在合併之前逸出。這會讓選擇陳述式不受 SQL 插入的影響。如要進一步瞭解所有引數,請參閱 query() 參考資料。

如要查看遊標中的資料列,請使用其中一種 Cursor 移動方法,您必須在讀取值前一律呼叫此方法。由於遊標從位置 -1 開始,呼叫 moveToNext() 會將「讀取位置」放置在結果的第一個項目,並傳回游標是否已超過結果集中的最後一個項目。針對每個資料列,您可以呼叫其中一種 Cursor get 方法 (如 getString()getLong()) 來讀取資料欄的值。針對每種 get 方法,您必須傳遞所需資料欄的索引位置。如要取得該資料欄,請呼叫 getColumnIndex()getColumnIndexOrThrow()。反覆查看結果之後,呼叫遊標上的 close() 即可釋出資源。例如,以下範例說明了如何取得遊標中的所有項目 ID,並將其加入清單中:

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() 方法。

更新資料表時,系統會將 insert()ContentValues 語法與 delete()WHERE 語法相結合。

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

對資料庫進行偵錯

Android SDK 包含 sqlite3 殼層工具,可讓您瀏覽資料表內容、執行 SQL 指令,以及在 SQLite 資料庫中執行其他實用函式。詳情請參閱如何發出殼層指令