Android 数据存储——SQLite

3,279 阅读13分钟

0 前言

SQLite,是一款轻型的数据库,是遵守ACID的关系型数据库管理系统,它包含在一个相对小的C库中。它是D.RichardHipp建立的公有领域项目。它的设计目标是嵌入式的,而且目前已经在很多嵌入式产品中使用了它,它占用资源非常的低,在嵌入式设备中,可能只需要几百K的内存就够了。它能够支持Windows/Linux/Unix等等主流的操作系统,同时能够跟很多程序语言相结合,比如 Tcl、C#、PHP、Java等,还有ODBC接口,同样比起Mysql、PostgreSQL这两款开源的世界著名数据库管理系统来讲,它的处理速度比他们都快。(摘自百度百科

鉴于 SQLite 具备如此优秀的业务处理能力,Android 平台自然使用它作为内置的数据库存储模块。存储在 SD 卡上,只是一个文件的形式,可以方便的备份、移植。有兴趣的小伙伴,可以至SQLite官网下载 C 源码研究研究。

另外 Android 平台提供了一整套的 SQLite 操作 API。可以方便快速的创建表、更新表、对表数据进行增删改查。

1 创建数据库

创建数据库方法,分四类。

  1. create/createInMemory 在内存中创建数据库,当数据库关闭时即销毁。
  2. openDatabase 打开数据库,可以指定打开方式。
  3. openOrCreateDatabase 创建并打开数据库。
  4. getWritableDatabase/getReadableDatabase 打开读写,或只读数据库。

上述四个方式,都可以指定 CursorFactoryFlagsDatabaseErrorHandler(API 11)、OpenParams(API 27) 四个基本参数。最终都 执行到 openDatabase 方法。

SQLite 对数据库文件添加四种访问权限,做 flags 参数:

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

四个方法中,可以分为两类。

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

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的技术小站