数据库存储
统一存储使用的数据库基于微信的 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。