先直接推荐我的开源小库:WCDBRoomX ,如果它的README就已经让你知道库的核心作用,这篇文章就不需要看了。

WCDB是腾讯微信团队开源的客户端数据库框架,拥有高性能和支持加密等重要特性,并且可用于Android、iOS、Windows、macOS等多个平台。 我们知道,原生的加密数据库框架SQLCipher和不加密的SQLite相比,性能差距还是很大的,加密会使得读写效率严重下降,而WCDB很好地兼顾了性能和安全问题。

对使用了Google官方Jetpack Room库的开发者,WCDB 1.x版本也提供了完美支持:Use WCDB with Room ,然而,WCDB 2.x版本后变成了一个纯ORM框架,虽然也支持Java、Kotlin等语言(本质上就是一层封装,底层接口都一样),但是暂时没有计划支持兼容Room,从官方文档看,2.x版本更纯粹,一套代码跨全平台,所以也不关注各平台的其他框架兼容了。

跟WCDB的开发者也聊了聊,他们团队对Room的支持兴趣不大:issues#1052 。其实浏览一下2.x版本的更新日志,看似开发团队是iOS研发主导,很多更新也和Android无关。

不过问题不大,1.x版本的性能和稳定性已经非常好,连微信客户端自己都用了好多年。唯一的一个小问题是,Room最新版本支持了 @Upsert 注解,如果还继续使用 com.tencent.wcdb:room:1.x 库的话,在插入数据时没问题,但在更新数据时会崩溃,抛出异常:SQLiteConstraintException

解决方案有两种:

  • 摆烂,不再使用Room的Upsert注解,把Upsert拆成Insert和Update,这样需要改动很多现有代码。
  • Read the fucking code,看看为什么崩溃。

如果你选择第一种方案,后面就不用看了,但是为什么不看看第二种方案呢?/狗头

其实Upsert的实现很简单,最终执行插入或更新操作的源码在 EntityUpsertAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Upserts the given entities into the database and returns the row ids.
*
* @param entities Entities to upsert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
fun upsertAndReturnIdsArray(entities: Array<out T>): LongArray {
return LongArray(entities.size) { index ->
try {
insertionAdapter.insertAndReturnId(entities[index])
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entities[index])
-1
}
}
}

可以看出,每次Upsert操作都会先进行插入,如果数据已经存在,会抛异常,在catch捕获异常后,再进行更新数据的操作。所以理论上Room是可以捕获异常的,为什么集成WCDB之后就捕获不到了呢?

原因是WCDB对SQLite的很多类都重新进行了封装,一些基本的代码虽然没有改动,但是包名却变成了 com.tencent.wcdb.database

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.tencent.wcdb.database;

/**
* An exception that indicates that an integrity constraint was violated.
*/
@SuppressWarnings("serial")
public class SQLiteConstraintException extends SQLiteException {
public SQLiteConstraintException() {}

public SQLiteConstraintException(String error) {
super(error);
}
}

而Room只能捕获原生的 android.database.sqlite 包下的异常。知道原因后,这就好改了,只需要改造 WCDBStatement 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.tencent.wcdb.database.SQLiteConstraintException;
// ...

class WCDBStatement implements SupportSQLiteStatement {
// ...

@Override
public long executeInsert() {
try {
return mDelegate.executeInsert();
} catch (SQLiteConstraintException ex) {
// 兼容新版Room的Upsert注解
throw new android.database.sqlite.SQLiteConstraintException(ex.getMessage());
}
}
}

捕获WCDB自定义的异常后,转换一次,抛出原生SQLite库的异常,这样就能被Room的EntityUpsertAdapter捕获到,顺利兼容Upsert操作。

当然, 我已经把这些兼容处理好了,并且迁移了相关的依赖库为AndroidX,同时集成最新版本的SQLCipher,才有了:WCDBRoomX

可以删掉所有WCDB、SQLCipher、SQLite相关的依赖,直接引入WCDBRoomX即可:

1
2
3
4
5
6
7
8
9
10
11
dependencies { 
// implementation("androidx.sqlite:sqlite-ktx:2.4.0")
// implementation("net.zetetic:sqlcipher-android:4.5.6@aar")
// implementation("com.tencent.wcdb:wcdb-android:1.1-19")
implementation("com.github.ysy950803:WCDBRoomX:1.0.0")

// 当然,Room还是要保留的,方便独立更新,WCDBRoomX为了轻便,不会包含Room
val roomVersion = "2.6.1"
kapt("androidx.room:room-compiler:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
}

快速集成:

1
2
3
Room.databaseBuilder(...)
.openHelperFactory(WCDBRoomX.createOpenHelperFactory("db_password"))
.build()