最近我一直在使用Android架构组件(更具体地说是Room),但我遇到了一个难题。
我已经成功构建了一个Room数据库,用于存储部门及其人员的列表。以前这些数据是从服务器中提取的,但并没有本地存储。搜索功能也是远程处理的,现在我也希望能够本地处理搜索功能,但是我的SQL知识有些欠缺。
查看服务器上的SQL代码,搜索语句使用了许多REGEXP
函数,基于提供的查询在两个数据库中进行搜索。这似乎不是处理搜索的最佳方法,但它运作得相当不错并且响应速度很快。因此,我尝试在本地模仿这个方法,但很快发现REGEXP
在Android上不受支持(除非使用NDK)。
至于LIKE
和GLOB
操作符,它们似乎在所能做的方面非常有限。例如,我不知道如何同时匹配多个关键字;而对于REGEXP
,我只需将空格替换为or
(|
)运算符即可实现这个功能。
因此,我在寻找替代方法时发现了全文本搜索(FTS);这是Android文档中有关实现搜索的方法中演示的方法。虽然似乎FTS适用于搜索完整文档,而不是像我的用例那样简单的数据。
无论如何,Room并不支持FTS。
因此,我自然而然地尝试强制Room创建一个FTS虚拟表而不是标准表,通过创建一个实现SupportSQLiteOpenHelper.Factory
来完成这一点。这个实现几乎是默认的FrameworkSQLiteOpenHelperFactory
及其相关框架类的直接副本。需要的代码部分在SupportSQLiteDatabase
中,在其中覆盖execSQL
以在必要时注入虚拟表代码。
class FTSSQLiteDatabase(
private val delegate: SQLiteDatabase,
private val ftsOverrides: Array<out String>
) : SupportSQLiteDatabase {
// Omitted code...
override fun execSQL(sql: String) {
delegate.execSQL(injectVirtualTable(sql))
}
override fun execSQL(sql: String, bindArgs: Array<out Any>) {
delegate.execSQL(injectVirtualTable(sql), bindArgs)
}
private fun injectVirtualTable(sql: String): String {
if (!shouldOverride(sql)) return sql
var newSql = sql
val tableIndex = sql.indexOf("TABLE")
if (tableIndex != -1) {
sql = sql.substring(0..(tableIndex - 1)) + "VIRTUAL " + sql.substring(tableIndex)
val argumentIndex = sql.indexOf('(')
if (argumentIndex != -1) {
sql = sql.substring(0..(argumentIndex - 1) + "USING fts4" + sql.substring(argumentIndex)
}
}
return newSql
}
private fun shouldOverride(sql: String): Boolean {
if (!sql.startsWith("CREATE TABLE")) return false
val split = sql.split('`')
if (split.size >= 2) {
val tableName = split[1]
return ftsOverrides.contains(tableName)
} else {
return false
}
}
}
有点凌乱,但它有效!嗯,它创建了虚拟表...
但是我接下来会得到以下SQLiteException
异常:
04-04 10:54:12.146 20289-20386/com.example.app E/SQLiteLog: (1) cannot create triggers on virtual tables
04-04 10:54:12.148 20289-20386/com.example.app E/ROOM: Cannot run invalidation tracker. Is the db closed?
android.database.sqlite.SQLiteException: cannot create triggers on virtual tables (code 1): , while compiling: CREATE TEMP TRIGGER IF NOT EXISTS `room_table_modification_trigger_departments_UPDATE` AFTER UPDATE ON `departments` BEGIN INSERT OR REPLACE INTO room_table_modification_log VALUES(null, 0); END
at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:890)
at android.database.sqlite.SQLiteConnection.prepare(SQLiteConnection.java:501)
at android.database.sqlite.SQLiteSession.prepare(SQLiteSession.java:588)
at android.database.sqlite.SQLiteProgram.<init>(SQLiteProgram.java:58)
at android.database.sqlite.SQLiteStatement.<init>(SQLiteStatement.java:31)
at android.database.sqlite.SQLiteDatabase.executeSql(SQLiteDatabase.java:1752)
at android.database.sqlite.SQLiteDatabase.execSQL(SQLiteDatabase.java:1682)
at com.example.app.data.FTSSQLiteDatabase.execSQL(FTSSQLiteDatabase.kt:164)
at android.arch.persistence.room.InvalidationTracker.startTrackingTable(InvalidationTracker.java:204)
at android.arch.persistence.room.InvalidationTracker.access$300(InvalidationTracker.java:62)
at android.arch.persistence.room.InvalidationTracker$1.run(InvalidationTracker.java:306)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
Room创建了表,但随后尝试在虚拟表上创建触发器,显然是不允许的。如果我试图覆盖这些触发器(即仅防止它们执行),我猜这将破坏大量Room的功能。这应该就是Room一开始不支持FTS的原因。
简而言之,如果Room不支持FTS(我也无法强制使用它),并且不支持REGEXP
(除非我使用NDK);那么我有没有其他方法来实现搜索功能,同时使用Room?FTS是正确的方法吗(似乎有些过度),还是有其他更适合我的用例的方法?
@Entity
,并正在修改 Room 创建的CREATE TABLE
语句。 我们刚刚收到可以使用解决方法的批准(就在过去的20分钟左右),但我不会这样做。 相反,我会使用RoomDatabase.Callback
和迁移,在SupportSQLiteDatabase
上手动创建和修改表。 - CommonsWare