使用Room实现搜索

11

最近我一直在使用Android架构组件(更具体地说是Room),但我遇到了一个难题。

我已经成功构建了一个Room数据库,用于存储部门及其人员的列表。以前这些数据是从服务器中提取的,但并没有本地存储。搜索功能也是远程处理的,现在我也希望能够本地处理搜索功能,但是我的SQL知识有些欠缺。

查看服务器上的SQL代码,搜索语句使用了许多REGEXP函数,基于提供的查询在两个数据库中进行搜索。这似乎不是处理搜索的最佳方法,但它运作得相当不错并且响应速度很快。因此,我尝试在本地模仿这个方法,但很快发现REGEXP在Android上不受支持(除非使用NDK)。

至于LIKEGLOB操作符,它们似乎在所能做的方面非常有限。例如,我不知道如何同时匹配多个关键字;而对于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是正确的方法吗(似乎有些过度),还是有其他更适合我的用例的方法?


1
也许问题在于您创建表的方式。 您不希望 Room 认为它与表有太多关系。 我的理解是,您仍然拥有一个 @Entity,并正在修改 Room 创建的 CREATE TABLE 语句。 我们刚刚收到可以使用解决方法的批准(就在过去的20分钟左右),但我不会这样做。 相反,我会使用 RoomDatabase.Callback 和迁移,在 SupportSQLiteDatabase 上手动创建和修改表。 - CommonsWare
1
有没有其他更适合我的使用情况的方法?不完全清楚你的使用情况。你的关键词在哪里?这个列表是否在表格的单独一列中?如果是的话,也许答案是部门和关键词之间存在M:N的关系。 - CommonsWare
1
@CommonsWare 是的,很抱歉我没有更详细地阐述我的用例。例如,部门数据库有六列(代码、标题、前缀、位置、电话和传真)。标题、前缀和位置列可以有多个单词,我希望能够将提供的任何关键字与所有列中的任何单词匹配(尽管我对电话和传真字段不是太担心)。所以是的,这是一个M:N关系。 - Bryan
1
啊,好的,我以为“关键字”是指像标签之类的东西。在我看来,全文搜索(Full-Text Search)并不是一个坏选择。 - CommonsWare
哦,不,我所说的“关键词”是指用户提供的查询(无论他们在搜索字段中输入什么单词),这可能是错误的术语。你提到的解决方法看起来非常有前途!正在努力实施它。 - Bryan
房间不支持正则表达式?真遗憾! - Bugs Happen
2个回答

6
我可以确认这个方法可行。虽然有些烦人,但它确实可行。
首先,您需要创建表格。对于初始数据库创建,您可以使用RoomDatabase.Callback
RoomDatabase.Builder<BookDatabase> b=
  Room.databaseBuilder(ctxt.getApplicationContext(), BookDatabase.class,
    DB_NAME);

b.addCallback(new Callback() {
  @Override
  public void onCreate(@NonNull SupportSQLiteDatabase db) {
    super.onCreate(db);

    db.execSQL("CREATE VIRTUAL TABLE booksearch USING fts4(sequence, prose)");
  }
});

BookDatabase books=b.build();

(另外:如果您需要在迁移中对其进行更改,请记住此表!)

然后,您可以为此设置一个@Dao。您所有实际的数据库操作DAO方法都需要使用@RawQuery进行注释,因为其他所有内容都希望使用实体来处理。而且,由于@RawQuery方法仅接受SupportSQLiteQuery参数,因此您可能希望将其包装在创建SupportSQLiteQuery对象的其他方法中。

例如,要将数据插入虚拟表中,您可以执行以下操作:

  @RawQuery
  protected abstract long insert(SupportSQLiteQuery queryish);

  void insert(ParagraphEntity entity) {
    insert(new SimpleSQLiteQuery("INSERT INTO booksearch (sequence, prose) VALUES (?, ?)",
      new Object[] {entity.sequence, entity.prose}));
  }

要进行搜索,你可以执行以下操作:

  @RawQuery
  protected abstract List<BookSearchResult> _search(SupportSQLiteQuery query);

  List<BookSearchResult> search(String expr) {
    return _search(query(expr));
  }

  private SimpleSQLiteQuery query(String expr) {
    return new SimpleSQLiteQuery("SELECT sequence, snippet(booksearch) AS snippet FROM booksearch WHERE prose MATCH ? ORDER BY sequence ASC",
      new Object[] {expr});
  }

在这两种情况下,我的@RawQuery方法是protected的,并且使用前导下划线_来强调“这些本来应该是private的,但你不能有privateabstract方法,请不要使用它们,好吧?”
请注意,您的FTS搜索表达式需要遵循SQLite FTS文档

1
@Bryan:好的,我刚刚提交了一个问题(https://issuetracker.google.com/issues/78842400),希望能够澄清`@RawQuery`写入场景的文档。我不确定你认为SQLite FTS支持缺少什么,所以在那方面我无法提供太多建议。 - CommonsWare
@CommonsWare 我的RawQuery返回了一个空列表。db.execSQL("CREATE VIRTUAL TABLE NameSearch USING fts4(Name)"); 用于创建虚拟数据库。SimpleSQLiteQuery simpleQuery = new SimpleSQLiteQuery("INSERT INTO NameSearch (Name) VALUES ?", new Object[]{entity.getName()}); 用于填充数据库。SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM NameSearch WHERE Name MATCH ? ORDER BY Name ASC", new Object[]{name}); 用于查询数据库。 - Ayokunle Paul
@AyokunlePaul:也许name的值存在问题。这是一个完整的示例项目,演示了如何在Room中使用FTS。这个示例有点奇怪,因为我必须打包要搜索的数据,以便示例是自包含的,但它可能对你在确定自己代码出现了什么问题时有所帮助。 - CommonsWare
谢谢。现在它可以工作了。我面临的问题相当棘手。 - Ayokunle Paul
这个解决方案可能已经过时,因为您使用原始查询的陈述与文档 https://developer.android.com/training/data-storage/room/defining-data 不一致。 - Johann
显示剩余2条评论

5
我们终于得到了,从版本2.1.0-alpha01开始,Room支持具有映射FTS3或FTS4表的实体。如需获取更多信息和示例使用,请参阅它们的文档:@Fts3@Fts4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接