数据库存储

统一存储使用的数据库基于微信的 WCDB 组件,与 Android 原生的 SQLite 接口基本一致。

使用数据库

// 获取全局存储环境下的数据库接口
TMFDatabase database = globalStorge.database();

// 定义数据库回调,回调中的方法与SQLiteOpenHelper中的同名方法作用相同
public class TestDBCallback extends TMFWCDBOpenHelper.DatabaseCallback {

    /**
     * 表创建
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS person (_id INTEGER PRIMARY KEY AUTOINCREMENT , name "
                + "VARCHAR(20) , address TEXT)";
        db.execSQL(SQL_CREATE);
    }

    /**
     * 版本升级
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.d("WCDB", "数据库旧版本" + oldVersion);
        Log.d("WCDB", "数据库新版本" + newVersion);
    }
}


// 从TMFDatabase创建SQLiteOpenHelper
SQLiteOpenHelper dbHelper = database.createWCDBHelper(DEFAULT_NAME, // 数据库名称
                                        "1234".getBytes(),          // 数据库加密密钥
                                        null// CursorFactory
                                        DEFAULT_VERSION,            // 数据库版本
                                        null,                       // DatabaseErrorHandler
                                        new TestDBCallback());      // 数据库生命周期回调

// 使用dbHelper获取以及操作数据库
SQLiteDatabase readableDB = dbHelper.getReadableDatabase();
SQLiteDatabase writableDB = dbHelper.getWritableDatabase();
...

注意 注意:如果您的 App 之前使用 Android SDK 的数据库接口,需要将 import 里的 android.database. 改为 com.tencent.wcdb.\,以及 android.database.sqlite. 改为 com.tencent.wcdb.database. 。 若之前使用 SQLCipher Android Binding,也需要对应修改 import。

数据库加密

在创建SQLiteOpenHelper时您可以选择加密或者不加密数据库。传入密钥则会在后续数据库操作中加密数据库,否则将不会加密数据库。

// 不加密数据库
SQLiteOpenHelper dbHelper = database.createWCDBHelper(DEFAULT_NAME, // 数据库名称
                                        null// CursorFactory
                                        DEFAULT_VERSION,            // 数据库版本
                                        null,                       // DatabaseErrorHandler
                                        new TestDBCallback());      // 数据库生命周期回调

// 加密数据库
SQLiteOpenHelper dbHelper = database.createWCDBHelper(DEFAULT_NAME, // 数据库名称
                                        "1234".getBytes(),          // 数据库加密密钥
                                        null// CursorFactory
                                        DEFAULT_VERSION,            // 数据库版本
                                        null,                       // DatabaseErrorHandler
                                        new TestDBCallback());      // 数据库生命周期回调

获取数据库文件

TMF创建的数据库由于被保存在不同的位置,因此无法通过Context的getDatabasePath获得文件路径,请使用TMFDatabase的getDatabasePath方法获得数据库文件的路径。

TMFDatabase database = globalStorge.database();

// 传入的参数为数据库名
File dbFile = database.getDatabasePath(DATABASE_NAME);

// dropDatabase方法可以删除对应名字的数据库
database.dropDatabase(DATABASE_NAME);

数据库迁移

从非加密数据库迁移到加密数据库

如果您之前使用的是非加密数据库,接入后想迁移到加密数据库并保留原来的数据,您需要使用 SQL 函数 sqlcipher_export() 进行迁移。

File oldDbFile = mContext.getDatabasePath(OLD_DATABASE_NAME);
if (oldDbFile.exists()) {

    Log.i(TAG, "Migrating plain-text database to encrypted one.");

    db.endTransaction();

    // 将旧数据库附加到新创建的加密数据库上
    String sql = String.format("ATTACH DATABASE %s AS old KEY '';",
            DatabaseUtils.sqlEscapeString(oldDbFile.getPath()));
    db.execSQL(sql);

    // 导出旧数据库
    db.beginTransaction();
    DatabaseUtils.stringForQuery(db, "SELECT sqlcipher_export('main', 'old');", null);
    db.setTransactionSuccessful();
    db.endTransaction();

    // 获取旧数据库版本
    int oldVersion = (int) DatabaseUtils.longForQuery(db, "PRAGMA old.user_version;", null);

    // 取消旧数据库关联
    db.execSQL("DETACH DATABASE old;");

    // 此时可以安全删除旧数据库
    oldDbFile.delete();

    db.beginTransaction();

    // 根据旧数据库版本升级或者降级数据库
    if (oldVersion > DATABASE_VERSION) {
        onDowngrade(db, oldVersion, DATABASE_VERSION);
    } else if (oldVersion < DATABASE_VERSION) {
        onUpgrade(db, oldVersion, DATABASE_VERSION);
    }
}

注意 注意:WCDB 对 sqlcipher_export() 函数做了扩展,原本只接受一个导出到哪个 ATTACHED DB 的参数,现在可以接受第二个参数指定从哪个 DB 导出。因此可以反过来实现导入:

ATTACH 'old_database' AS old;
SELECT sqlcipher_export('main', 'old');     -- 从 old 导入到 main
DETACH old;

从 SQLCipher Android 迁移

如果您之前使用的是 SQLCipher 数据库,希望迁移到 WCDB 库并沿用原数据库文件,需要在代码里做一点改动。

String passphrase = "passphrase";

SQLiteCipherSpec cipher = new SQLiteCipherSpec()    // 创建加密描述对象
        .setPageSize(1024)                          // SQLCipher 默认 pagesize 为 1024
        .setSQLCipherVersion(3);                    // 1,,2,,3 分别对应 1.x,2.x,3.x 创建的 SQLCipher数据库
        // 如果以前使用过其他PRAGMA,可以添加其他选项

SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(
        new File("path/to/database"),   // DB文件
        passphrase.getBytes(),          // 密码参数类型为 byte[]
        cipher,                         // 加密描述对象
        null,                           // CursorFactory
        null                            // DatabaseErrorHandler
);

关键改动点为密码转换为 byte[] 以及 传入 SQLiteCipherSpec 描述加密方式,加密方式必须和之前的 SQLCipher 设置一致,否则会报错误,建议先行测试再上线。SQLCipher 密码与加密方式错误可能会导致 SQLite 框架认为其损坏从而触发 DatabaseErrorHandler,默认实现会重命名或删除损坏 DB,如果此行为不是您希望的,请务必自定义 DatabaseErrorHandler。

如果之前调用了 SQLCipher 的 SQLiteDatabase.loadLibs(...),可以将其删去,WCDB 在第一次引用时会自动加载动态库。

数据库修复

Repair Kit

使用 Repair Kit 可以直接从损坏的数据库里尽量读出未损坏的数据,不需要事先准备,但是先备份 Master 信息可以大大增加恢复成功率。如果有意使用 Repair Kit 恢复数据库,建议备份 Master 信息。

备份 Master 信息

  • Master 信息保存了数据库的 Schema,建议每次执行完数据库创建或升级时执行备份,可以保证备份是最新的。若不修改 Schema,Master 信息不会改变。
  • 如果您使用 SQLiteOpenHelper,最佳实践是在 SQLiteOpenHelper.onCreate(...) 和 SQLiteOpenHelper.onUpgrade(...) 的最后进行备份,备份 Master 信息只需要调用 RepairKit.MasterInfo.save(...) 即可。
  • 备份 Master 信息典型消耗为几kB - 几十kB,几毫秒 - 几十毫秒,但如果您有很多的表和索引(万数量级),这个过程可能会有点慢,建议放在子线程完成。
public class DBCallback extends TMFWCDBOpenHelper.DatabaseCallback {

    static final byte[] PASSPHRASE = "testkey".getBytes();

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 执行 CREATE TABLE 创建 Schema
        db.execSQL("CREATE TABLE t1(a,b);");
        db.execSQL("CREATE TABLE t2(c,d);");
        // ...

        // 备份 Master 信息
        RepairKit.MasterInfo.save(db, db.getPath() + "-mbak", PASSPHRASE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 执行升级
        db.execSQL("ALTER TABLE t1 ADD COLUMN x TEXT;");

        // 备份 Master 信息
        RepairKit.MasterInfo.save(db, db.getPath() + "-mbak", PASSPHRASE);
    }
}

恢复损坏数据库

恢复损坏数据库,首先加载之前备份的 Master 信息(如果有)。

if (masterFile.exists()) {
    try {
        master = RepairKit.MasterInfo.load(masterFile.getPath(), PASSPHRASE, null);
    } catch (SQLiteException e) {
        // 无法加载 master 信息,可能是不存在或者损坏
    }
}

使用 RepairKit 打开损坏的数据库,使用 SQLiteDatabase 打开新的数据库,调用 output(...) 即可将损坏数据库的内容转移到新数据库。

RepairKit repair = new RepairKit(
        dbFile.getPath(),           // 损坏的数据库文件
        PASSPHRASE,                 // 数据库密钥(不是备份文件的密钥)
        CIPHER_SPEC,                // 加密描述,与打开DB时一样
        master                      // 之前加载的 Master 信息
);

// 打开新DB用于承载修复时的数据,是否加密都可以
SQLiteDatabase newDb = SQLiteDatabase.openOrCreateDatabase(newDbFile,
        PASSPHRASE, CIPHER_SPEC, null, ERROR_HANDLER);

// 设置修复时的回调
repair.setCallback(new RepairKit.Callback() {
    @Override
    public int onProgress(String table, int root, Cursor cursor) {
        Log.d(TAG, String.format("table: %s, root: %d, count: %d",
                table, root, cursor.getColumnCount()));
        return RepairKit.RESULT_OK;
    }
});

// 输出恢复数据到新 DB
int result = repair.output(newDb, 0);
if (result != RepairKit.RESULT_OK && result != RepairKit.RESULT_CANCELED) {
    // 恢复失败
    throw new SQLiteException("Repair returns failure.");
}

newDb.setVersion(DATABASE_VERSION);
newDb.close();

// 需要release释放资源
repair.release();

恢复的过程耗时较长,请务必在子线程完成,如数据库较大请考虑持有 Wake Lock。

选择性恢复

Repair Kit 可以只恢复一部分表,只需要在 MasterInfo.load(...) 或者 MasterInfo.make(...) 里指定白名单即可。

// 白名单,只有白名单里列到的表才会恢复,表对应的索引也会相应恢复
String[] tables = new String[] {
    "t1", "t2"          //只恢复 t1 和 t2 两个表
};
RepairKit.MasterInfo master = RepairKit.MasterInfo.load(masterFile.getPath(),
                PASSPHRASE, tables);

备份和恢复

备份完整数据,损坏后使用备份恢复的方案,如没有备份则无法恢复。由于是备份数据本身而不是 Schema,备份本身需要经常更新。备份和恢复操作都非常耗时,请勿在主线程操作,备份大数据库和恢复时可以考虑持有 Wake Lock。

备份

BackupKit backup = new BackupKit(
        db,                             // 要备份的 DB
        db.getPath() + "-backup",       // 备份文件
        PASSPHRASE,                     // 加密备份文件的密钥,非 DB 密钥
        0, 
        null);

int result = backup.run();
switch (result) {
    case BackupKit.RESULT_OK:           // 成功
    case BackupKit.RESULT_CANCELED:     // 取消操作
    case BackupKit.RESULT_FAILED:       // 失败
}

// 备份完成后需要释放资源
backup.release();

恢复

RecoverKit recover = new RecoverKit(
        db,                             // 要恢复到的目标 DB
        db.getPath() + "-backup",       // 备份文件
        PASSPHRASE);                    // 加密备份文件的密钥,非 DB 密钥

int result = recover.run(false);        // 参数传 false 表示遇到错误忽略并继续,
                                        // 若传入 true 遇到错误则终止并返回FAILED
switch (result) {
    case RecoverKit.RESULT_OK:          // 成功
    case RecoverKit.RESULT_CANCELED:    // 取消操作
    case RecoverKit.RESULT_FAILED:      // 失败
}

recover.release();

取消操作

由于备份和恢复都比较耗时,WCDB 提供接口中止备份或恢复操作。只需要在另外的线程调用 BackupKit.cancel() 或 RecoverKit.cancel() 即可通知备份或恢复线程中止并尽快返回,返回码为 RESULT_CANCELED。

Copyright © 2013-2023 Tencent Cloud. all right reserved,powered by GitbookUpdate Time 2023-08-31 14:46:07

results matching ""

    No results matching ""