likes
comments
collection
share

让WCDB兼容最新版Room

作者站长头像
站长
· 阅读数 3

先直接推荐我的开源小库: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

    /**
     * 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

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 即可:

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即可:

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")
}

快速集成:

Room.databaseBuilder(...)
    .openHelperFactory(WCDBRoomX.createOpenHelperFactory("db_password"))
    .build()