Android 數據存儲——SQLite

0 前言

SQLite,是一款輕型的數據庫,是遵照ACID的關係型數據庫管理系統,它包含在一個相對小的C庫中。它是D.RichardHipp創建的公有領域項目。它的設計目標是嵌入式的,並且目前已經在不少嵌入式產品中使用了它,它佔用資源很是的低,在嵌入式設備中,可能只須要幾百K的內存就夠了。它可以支持Windows/Linux/Unix等等主流的操做系統,同時可以跟不少程序語言相結合,好比 Tcl、C#、PHP、Java等,還有ODBC接口,一樣比起Mysql、PostgreSQL這兩款開源的世界著名數據庫管理系統來說,它的處理速度比他們都快。(摘自百度百科html

鑑於 SQLite 具有如此優秀的業務處理能力,Android 平臺天然使用它做爲內置的數據庫存儲模塊。存儲在 SD 卡上,只是一個文件的形式,能夠方便的備份、移植。有興趣的小夥伴,能夠至SQLite官網下載 C 源碼研究研究。android

另外 Android 平臺提供了一整套的 SQLite 操做 API。能夠方便快速的建立表、更新表、對錶數據進行增刪改查。git

1 建立數據庫

建立數據庫方法,分四類。github

  1. create/createInMemory 在內存中建立數據庫,當數據庫關閉時即銷燬。
  2. openDatabase 打開數據庫,能夠指定打開方式。
  3. openOrCreateDatabase 建立並打開數據庫。
  4. getWritableDatabase/getReadableDatabase 打開讀寫,或只讀數據庫。

上述四個方式,均可以指定 CursorFactoryFlagsDatabaseErrorHandler(API 11)、OpenParams(API 27) 四個基本參數。最終都 執行到 openDatabase 方法。算法

SQLite 對數據庫文件添加四種訪問權限,作 flags 參數:sql

  1. OPEN_READWRITE:讀寫方式打開數據庫文件,能夠進行增刪改查等操做。
  2. OPEN_READONLY:只讀方式打開數據庫文件,只能執行查詢操做,插入數據會拋出異常 android.database.sqlite.SQLiteReadOnlyDatabaseException
  3. CREATE_IF_NECESSARY:打開數據庫時,如沒有數據庫文件,便會自動建立,並容許增刪改查。
  4. NO_LOCALIZED_COLLATORS: 使 setLocale 方法不生效。
// 執行 query 方法時,使用該對象,用於生成 cursor
SQLiteDatabase.CursorFactory factory = new SQLiteDatabase.CursorFactory() {
    @Override
    public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, String editTable, SQLiteQuery query) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            return new SQLiteCursor(masterQuery, editTable, query);
        }else
            return new SQLiteCursor(db,masterQuery, editTable, query);
    }
};
// 數據庫發生異常時,執行該對象。
DatabaseErrorHandler errorHandler = new DatabaseErrorHandler() {
    @Override
    public void onCorruption(SQLiteDatabase dbObj) {
        // 作關閉數據庫的操做
        dbObj.close();
    }
};
// 打開數據庫的參數,API 27 才支持。
SQLiteDatabase.OpenParams openParams = new SQLiteDatabase.OpenParams.Builder()
        .setCursorFactory(factory) // 指定 CursorFactory
        .setErrorHandler(errorHandler)// 指定 DatabaseErrorHandler
        .addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY) // 指定打開權限。
        .build();
複製代碼

1.1 create

直接在內存中建立數據庫,create/createInMemory 兩個方法,後者在 API 27 才支持,建議使用 create。數據庫

/** * 在內存中建立 SQLite 數據庫,當數據庫關閉時,即銷燬。 * 適合臨時保存數據。 * * @param factory 可選參數,建議傳入 NULL 或者構建 factory 實例。 * @return 返回數據庫對象 * @throws 數據庫建立失敗時,拋出 SQLiteException */
@NonNull
public static SQLiteDatabase create(@Nullable CursorFactory factory) {
    // This is a magic string with special meaning for SQLite.
    return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH,
            factory, CREATE_IF_NECESSARY);
}
/** * 在內存中建立 SQLite 數據庫,當數據庫關閉時,即銷燬。 * 適合臨時保存數據。 * * @param openParams 配置打開數據庫的參數 * @return 返回數據庫對象 * @throws 數據庫建立失敗時,拋出 SQLiteException */
@NonNull
public static SQLiteDatabase createInMemory(@NonNull OpenParams openParams) {
    return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH,
            openParams.toBuilder().addOpenFlags(CREATE_IF_NECESSARY).build());
}
複製代碼

都執行到 openDatabase 方法,Flags = CREATE_IF_NECESSARY。緩存

1.2 openDatabase

/** * 根據 flags 打開數據庫 * * @param path 建立或打開數據庫的文件路徑。可使用相對地址或絕對地址。 * 相對地址存在應用緩存 database 目錄,絕對地址能夠存在 sd 卡目錄下。 * @param factory 可選參數,建議傳入 NULL 或者構建 factory 實例。 * @param flags 控制數據的方式方式, * @return 返回打開新打開的數據庫 * @throws 數據庫建立失敗時,拋出 SQLiteException */
public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory, @DatabaseOpenFlags int flags) {
    return openDatabase(path, factory, flags, null);
}

/** * 根據 flags 打開數據庫 * * @param path 打開或建立數據庫的文件 * @param factory 可選參數,建議傳入 NULL 或者構建 factory 實例。 * @param flags 控制數據庫文件的訪問方式 * @param errorHandler 當 SQLite 報出數據庫出錯時,使用 DatabaseErrorHandler 處理錯誤。如關閉數據庫。 * @return 返回打開新打開的數據庫 * @throws 數據庫建立失敗時,拋出 SQLiteException */
public static SQLiteDatabase openDatabase(@NonNull String path, @Nullable CursorFactory factory, @DatabaseOpenFlags int flags, @Nullable DatabaseErrorHandler errorHandler) {
    SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler, -1, -1, -1, null,
            null);
    db.open();
    return db;
}

/** * 根據指定的參數,打開數據庫 * * @param path 建立或打開數據庫的文件路徑。 * 使用絕對路徑,或者經過context#getDatabasePath(String) * @param openParams 配置打開數據庫的參數 * @return 返回打開新打開的數據庫 * @throws 數據庫建立失敗時,拋出 SQLiteException */
public static SQLiteDatabase openDatabase(@NonNull File path, @NonNull OpenParams openParams) {
    return openDatabase(path.getPath(), openParams);
}

private static SQLiteDatabase openDatabase(@NonNull String path, @NonNull OpenParams openParams) {
    Preconditions.checkArgument(openParams != null, "OpenParams cannot be null");
    SQLiteDatabase db = new SQLiteDatabase(path, openParams.mOpenFlags,
            openParams.mCursorFactory, openParams.mErrorHandler,
            openParams.mLookasideSlotSize, openParams.mLookasideSlotCount,
            openParams.mIdleConnectionTimeout, openParams.mJournalMode, openParams.mSyncMode);
    db.open();
    return db;
}

複製代碼

四個方法中,能夠分爲兩類。app

  1. 前兩個分別指定 Flags、CursorFactory 和 DatabaseErrorHandler(API 11)支持。
  2. 後兩個統一由 OpenParams 指定打開數據庫的參數。包含但不限於上面三個屬性。

1.3 openOrCreateDatabase

/** * 至關於 openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY). */
public static SQLiteDatabase openOrCreateDatabase(@NonNull File file, @Nullable CursorFactory factory) {
    return openOrCreateDatabase(file.getPath(), factory);
}

/** * 至關於 openDatabase(path, factory, CREATE_IF_NECESSARY). */
public static SQLiteDatabase openOrCreateDatabase(@NonNull String path, @Nullable CursorFactory factory) {
    return openDatabase(path, factory, CREATE_IF_NECESSARY, null);
}

/** * 至關於 openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler). */
public static SQLiteDatabase openOrCreateDatabase(@NonNull String path, @Nullable CursorFactory factory, @Nullable DatabaseErrorHandler errorHandler) {
    return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler);
}
複製代碼

建立並打開數據庫,三個方法最後都會執行到 SQLiteDatabase openDatabase(String ,CursorFactory,int,DatabaseErrorHandler) 方法。ide

1.4 SQLiteOpenHelper

SQLiteOpenHelper 是 Android 封裝的最人性化的工具。方便開發者自行管理數據庫表目錄和結構。

/** * 管理數據庫建立和版本 */
public class DBOpenHelper extends SQLiteOpenHelper {

    // 至關於 SQLiteDatabase openDatabase(String, CursorFactory)
    public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    // 至關於 SQLiteDatabase openDatabase(String, CursorFactory, DatabaseErrorHandler)
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
        super(context, name, factory, version, errorHandler);
    }

    // 至關於 SQLiteDatabase openDatabase(String , OpenParams);
    @TargetApi(Build.VERSION_CODES.P)
    public DBOpenHelper(Context context, String name, int version, SQLiteDatabase.OpenParams openParams) {
        super(context, name, version, openParams);
    }

    // 建立數據文件時調用,此時適合建立新表
    @Override
    public void onCreate(SQLiteDatabase db) {
    }

    // 更新數據庫版本時調用,適合更新表結構或建立新表
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

// 生成 helper 對象,能夠打開數據庫文件。文件名能夠是相對路徑或絕對路徑
DBOpenHelper dbHelper = new DBOpenHelper(this, "test.db", null, 1);
// 用讀寫的方式打開數據庫文件
SQLiteDatabase database = dbHelper.getWritableDatabase();
複製代碼

1.5 使用場景

openDatabase 適合打開已經存在的數據庫文件。而且表目錄和結構固定,通常只有對數據庫表的常規操做。如,已有一個全國城市信息數據庫文件,須要在項目中打開。

SQLiteOpenHelper 須要本身從新建立數據庫文件,根據後期的需求可能須要對數據庫的表目錄及結構作修改。升級數據版本,在onUpgrade方法中處理便可。openDatabase 也能夠實現這個需求,只是稍微麻煩些。

2 插入數據

插入操做方法總結爲兩類,insert 和 replace ,最後都執行 insertWithOnConflict 方法。

/** * 向數據庫插入一行數據 * * @param table 指定表名,插入一行數據 * @param nullColumnHack 可選參數,建議是 null * 若是設置 null,將不容許向表中插入空數據,即 values = null 時沒法正確執行插入操做。 * 若是不設置 null,那麼須要設置表中能夠爲空的屬性列的名稱。 * 當 values = null 時,能夠向表中插入空數據。 * 而其實是插入一行數據,只有屬性列名 nullColumnHack 的值是 null。 * @param values map 集合,包含須要插入的一行數據。至少須要包含一個屬性的 key 和 value。 * key 是屬性列名稱,value 是屬性值。 * @return 返回新插入的行序號, 發生錯誤時,返回 -1 */
public long insert(String table, String nullColumnHack, ContentValues values) {
    try {
        return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
    } catch (SQLException e) {
        Log.e(TAG, "Error inserting " + values, e);
        return -1;
    }
}

/** * 向數據庫插入一行數據。只是在插入操做異常時,須要手動捕獲異常。 * 參數都同上 * @param table * @param nullColumnHack * @param values * @throws SQLException * @return */
public long insertOrThrow(String table, String nullColumnHack, ContentValues values) throws SQLException {
    return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
}
複製代碼
/** * 根據主鍵替換表中的一行數據,若是替換的主鍵在表中不存在,則插入一行數據。 * 參數都同上 * @param table * @param nullColumnHack * @param initialValues * @return 返回新插入的行序號, 發生錯誤時,返回 -1 */
public long replace(String table, String nullColumnHack, ContentValues initialValues) {
    try {
        return insertWithOnConflict(table, nullColumnHack, initialValues,
                CONFLICT_REPLACE);
    } catch (SQLException e) {
        Log.e(TAG, "Error inserting " + initialValues, e);
        return -1;
    }
}

/** * 根據主鍵替換表中的一行數據,若是替換的主鍵在表中不存在,則插入一行數據。 * 參數都同上 * @param table * @param nullColumnHack * @param initialValues * @throws SQLException * @return */
public long replaceOrThrow(String table, String nullColumnHack, ContentValues initialValues) throws SQLException {
    return insertWithOnConflict(table, nullColumnHack, initialValues,
            CONFLICT_REPLACE);
}
複製代碼
/** * 向數據庫中插入一行數據的通用方法。 * 參數同上。 * @param table * @param nullColumnHack * @param initialValues * @param conflictAlgorithm 解決衝突的處理算法 * @return 返回插入數據的行序號。當 conflictAlgorithm 錯誤或者發生插入異常時,返回-1。 */
public long insertWithOnConflict(String table, String nullColumnHack, ContentValues initialValues, int conflictAlgorithm) {
    acquireReference();
    try {
        StringBuilder sql = new StringBuilder();
        sql.append("INSERT");
        // 衝突處理算法,插入時只用第一個和最後一個。
        // {"", "OR ROLLBACK", "OR ABORT", "OR FAIL", "OR IGNORE", "OR REPLACE"};
        sql.append(CONFLICT_VALUES[conflictAlgorithm]);
        sql.append(" INTO ");
        sql.append(table);
        sql.append('(');

        Object[] bindArgs = null;
        int size = (initialValues != null && !initialValues.isEmpty())
                ? initialValues.size() : 0;
        // 拼接 sql 語句
        if (size > 0) { 
            bindArgs = new Object[size];
            int i = 0;
            for (String colName : initialValues.keySet()) {
                sql.append((i > 0) ? "," : "");
                // 拼接列名稱
                sql.append(colName);
                // 保存屬性值參數
                bindArgs[i++] = initialValues.get(colName);
            }
            sql.append(')');
            sql.append(" VALUES (");
            // 將全部的值用 ? 作佔位符
            for (i = 0; i < size; i++) {
                sql.append((i > 0) ? ",?" : "?");
            }
        } else {
                // 插入的數據集合是空時,至少須要有一列數據,列名稱是 nullColumnHack,值是 NULL
            sql.append(nullColumnHack + ") VALUES (NULL");
        }
        sql.append(')');
        // 執行插入操做
        SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
        try {
            return statement.executeInsert();
        } finally {
            statement.close();
        }
    } finally {
        releaseReference();
    }
}
複製代碼

演示代碼,不掩飾拋出異常的方法:

ContentValues values = new ContentValues();
    values.put("id", 1);
    values.put("name", "flueky");
    values.put("age", 27);
    // 插入完整的數據,id name age
    database.insert("user", null, values);
    // 插入空數據,將 name 設置爲 null
    database.insert("user", "name", null);
    // 替換 id = 1 的數據
    values.put("id", 1);
    values.put("name", "xiaofei");
    values.put("age", 27);
    database.replace("user", null, values);
複製代碼

replace 方法相似於 update 操做。

3 刪除數據

刪除數據庫表數據,只有一個方法。支持指定刪除條件

/** * 刪除數據庫中一行的方法 * * @param table 須要刪除的表名 * @param whereClause 傳入 null 時表示刪除表中所有數據。 * 或者指定刪除條件,只會刪除知足條件的行。 * @param whereArgs 指定刪除條件的值,按照順序替換在刪除條件中的 ? 。 * @return 刪除知足條件的行時,返回刪除的行數。找不到知足條件刪除的時候,返回 0 。 */
public int delete(String table, String whereClause, String[] whereArgs) {
    acquireReference();
    try {
        SQLiteStatement statement =  new SQLiteStatement(this, "DELETE FROM " + table +
                (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
        try {
            return statement.executeUpdateDelete();
        } finally {
            statement.close();
        }
    } finally {
        releaseReference();
    }
}
複製代碼

4 更新數據

更新表數據方法有兩個,建議只用第一個,conflictAlgorithm 參數默認 CONFLICT_NONE

/** * 更新數據庫表中的一行數據 * * @param table 須要更新的表名 * @param values 包含屬性名和新屬性值的 map 集合。 * @param whereClause 可選的 WHERE 條件決定須要更新的行。 * 若是是空,則更新全部的行。 * @param whereArgs 替換在 where 條件中包含的 ? 。 * @return 返回更新的行數 */
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
    return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE);
}

/** * 更新數據庫表中的一行數據 * 參數同上 * * @param table * @param values * @param whereClause * @param whereArgs * @param conflictAlgorithm 決定更新衝突的算法。 * @return */
public int updateWithOnConflict(String table, ContentValues values, String whereClause, String[] whereArgs, int conflictAlgorithm) {
    // 官方源碼中說,null is valid,此處卻拋出異常,有點費解 
    if (values == null || values.isEmpty()) {
        throw new IllegalArgumentException("Empty values");
    }

    acquireReference();
    try {
        // 拼接更新的 sql 語句
        StringBuilder sql = new StringBuilder(120);
        sql.append("UPDATE ");
        sql.append(CONFLICT_VALUES[conflictAlgorithm]);
        sql.append(table);
        sql.append(" SET ");

        // 將 values 和 whereArgs 值拼接在一塊兒
        int setValuesSize = values.size();
        int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length);
        Object[] bindArgs = new Object[bindArgsSize];
        int i = 0;
        // 拼接須要更新的列屬性
        for (String colName : values.keySet()) {
            sql.append((i > 0) ? "," : "");
            sql.append(colName);
            // 保存屬性值
            bindArgs[i++] = values.get(colName);
            sql.append("=?");
        }
        // 添加 where 條件的值
        if (whereArgs != null) {
            for (i = setValuesSize; i < bindArgsSize; i++) {
                bindArgs[i] = whereArgs[i - setValuesSize];
            }
        }
        if (!TextUtils.isEmpty(whereClause)) {
            sql.append(" WHERE ");
            sql.append(whereClause);
        }

        SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
        try {
            // 執行 sql 語句
            return statement.executeUpdateDelete();
        } finally {
            statement.close();
        }
    } finally {
        releaseReference();
    }
}
複製代碼

5 查詢數據

Android 給數據庫的表查詢操做提供了豐富的 API,一共10個方法,去除重載方法,共四個。下表中列出,四個方法和使用的參數。

  1. ● 表示必傳參數
  2. ○ 表示可傳參數
  3. - 表示不用的參數
參數 query queryWithFactory rawQuery rawQueryWithFactory
String table - -
String[] columns - -
String selection - -
String[] selectionArgs
String groupBy - -
String having - -
String orderBy - -
String limit - -
boolean distinct - -
CancellationSignal cancellationSignal
CursorFactory cursorFactory - -
String sql - -
String editTable - - -

參數說明:

  1. table 須要查詢的表名。
  2. columns 列出須要查詢的列。傳遞 null 將返回全部的列。不建議這樣作,防止從不用的存儲中讀取數據。
  3. selection 聲明須要返回表數據的過濾器,同 where 條件子句格式同樣(不包含 where 自身)。傳入 null 將返回表中全部的數據。
  4. selectionArgs 替換在 selection 中使用的 ?sql 中使用的 ?
  5. groupBy 申明返回的表數據的分組規則,同 GROUP BY 子句同樣(不包含 GROUP BY)傳入 null 將不分組。
  6. having 決定哪些分組會包含在 cursor 中。若是使用了分組條件將按照 HAVING 子句(不包含 HAVING)去格式化分組。傳入 null 會將全部的分組都包含在 cursor 中。且不使用分組時,必須用 null
  7. orderBy 對錶數據進行排序。同 ORDER BY 的條件語句同樣(不包含 ORDER BY)。傳入 null 將不排序。
  8. limit 限定查詢數據的條數。同 LIMIT 子句。傳入 null 不限定查詢條數。
  9. distinct 若是想每行數據不同,用true,不然用 false
  10. cancellationSignal 取消查詢進程中的操做信號。若是不須要手動取消,使用 null 。若是操做被取消會拋出 OperationCanceledException
  11. cursorFactory 生成 Cursor 對象,若是是 null ,則使用打開數據庫時使用的 cursorFactory 。若是打開數據庫時也使用 null ,那麼自動生成 Cursor
  12. sql 全部的 query 方法,最後都會合並出 sql 執行 rawquery 方法。
  13. editTable 指定須要編輯的表名。
/** * 最簡單也最經常使用的 query 方法。最後拼接好 sql 語句,去執行 rawQuery 方法。 */
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) {

    return query(false, table, columns, selection, selectionArgs, groupBy,
            having, orderBy, null /* limit */);
}

/** * 參數最全的 query 方法,全部的參數能夠手動指定,或使用 null。除 table。 */
public Cursor queryWithFactory(CursorFactory cursorFactory, boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit, CancellationSignal cancellationSignal) {
    acquireReference();
    try {
        String sql = SQLiteQueryBuilder.buildQueryString(
                distinct, table, columns, selection, groupBy, having, orderBy, limit);

        return rawQueryWithFactory(cursorFactory, sql, selectionArgs,
                findEditTable(table), cancellationSignal);
    } finally {
        releaseReference();
    }
}

/** * 最簡單也最經常使用的 rawQuery 方法。能夠自定義好查詢的 sql 語句後,自主查詢。適用於複雜的關係表關聯查詢。 */
public Cursor rawQuery(String sql, String[] selectionArgs) {
    return rawQueryWithFactory(null, sql, selectionArgs, null, null);
}

/** * 參數最全的 rawQuery 方法,使用場景很少。 */
public Cursor rawQueryWithFactory( CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable, CancellationSignal cancellationSignal) {
    acquireReference();
    try {
        SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable,
                cancellationSignal);
        return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory,
                selectionArgs);
    } finally {
        releaseReference();
    }
}
複製代碼

6 事務

事務(Transaction)是一個對數據庫執行工做單元。事務(Transaction)是以邏輯順序完成的工做單位或序列,能夠是由用戶手動操做完成,也能夠是由某種數據庫程序自動完成。

Android 中,SQLite 事務主要提供了三個 API。在結束事務前,如想將事務提交到數據庫,須要設置事務完成標記。不然,在事務開啓時候作的數據庫操做將不會保留。

database.beginTransaction();
/* 此處執行數據庫操做 */
database.setTransactionSuccessful();
database.endTransaction();
複製代碼

7 結束語

本文到此,已經介紹了 SQLite 經常使用的操做。如想自定義 sql 語句實現更豐富的數據庫操做,使用 database.execSQL 方法。使用自定義 sql 查詢數據庫請使用 database.rawQuery 方法。

示例代碼

爲避免篇幅過長, 暫時介紹到這裏。關於 SQLite 更多的操做語句和用法,請見後續文章。

以爲有用?那打賞一個唄。[去打賞]({{ site.url }}/donate/)

此處是廣告Flueky的技術小站

相關文章
相關標籤/搜索