Реализация поиска с комнатой

Недавно я возился с компонентами архитектуры Android (точнее Room), но я столкнулся с проблемой.

Я успешно создал базу данных Room, в которой хранится список отделов и их персонала. Ранее эти данные извлекались с сервера, но не сохранялись локально. Функциональность поиска также выполнялась удаленно, поэтому теперь я планирую использовать ее и локально, но мне немного не хватает знаний SQL.

Глядя на код SQL на сервере, оператор поиска использует набор функций REGEXP для поиска в обеих базах данных на основе предоставленного запроса. Это не похоже на лучший способ обработки поиска, но он работал довольно хорошо и давал быстрый ответ. Поэтому я попытался имитировать это локально, но быстро обнаружил, что REGEXP не поддерживается на Android (без использования NDK).

Что касается операторов LIKE и GLOB, они кажутся очень ограниченными в своих возможностях. Например, я не вижу способа сопоставить сразу несколько ключевых слов; тогда как с REGEXP я могу просто заменить пробел оператором or (|) для достижения этой функциональности.

Итак, в поисках альтернативы я наткнулся на полнотекстовый поиск (FTS); этот метод продемонстрирован в документации Android по реализации поиска. Хотя кажется, что FTS предназначен для поиска полных документов, а не простых данных, как в моем случае использования.

В любом случае 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.

TLDR

Итак, если Room не поддерживает FTS (и я не могу его заставить) и REGEXP не поддерживается (если я не использую NDK); есть ли другой способ реализовать поиск при использовании Room? Является ли FTS даже правильным путем (кажется излишним) или есть какой-то другой метод, более подходящий для моего варианта использования?


person Bryan    schedule 04.04.2018    source источник
comment
Возможно, проблема в том, как вы собираетесь создавать таблицу. Вы же не хотите, чтобы Рум думал, что это как-то связано со столом. Моя интерпретация заключается в том, что у вас все еще есть @Entity и вы взламываете оператор Room-created CREATE TABLE. Мы только что получили рекомендацию найти обходной путь (например, в последние 20 минут или около того), но я бы не стал так поступать. Вместо этого я бы использовал RoomDatabase.Callback и миграции, вручную создавая и изменяя таблицу в SupportSQLiteDatabase.   -  person CommonsWare    schedule 04.04.2018
comment
есть ли другой метод, который больше подходит для моего варианта использования? -- не совсем понятно, каков ваш вариант использования. У вас где-то есть ключевые слова. Этот список находится в одном столбце таблицы? Если да, то, возможно, ответ заключается в том, что между отделом и ключевым словом существует отношение M:N.   -  person CommonsWare    schedule 04.04.2018
comment
@CommonsWare Да, извините, что не рассказал больше о моем варианте использования. База данных отдела, например, имеет шесть столбцов (код, должность, префикс, местоположение, телефон и факс). Столбцы заголовка, префикса и местоположения могут содержать несколько слов, и я хочу иметь возможность сопоставлять любое из ключевых слов с любым словом во всех столбцах (хотя я не слишком беспокоюсь о полях телефона и факса). Так что да, и отношения М:Н.   -  person Bryan    schedule 04.04.2018
comment
Ах, хорошо, я подумал, что, возможно, ключевые слова означают теги или что-то в этом роде. FTS не плохой выбор для этого, ИМХО.   -  person CommonsWare    schedule 04.04.2018
comment
О, нет, я говорю ключевые слова, как в запросе, предоставленном пользователем (какие бы слова он ни вводил в поле поиска); что вполне может быть неправильной терминологией. И обходной путь, о котором вы упомянули, выглядит очень многообещающе! Сейчас работаем над его реализацией.   -  person Bryan    schedule 04.04.2018
comment
Комната не поддерживает регулярное выражение? Какой позор!   -  person Bugs Happen    schedule 24.03.2020


Ответы (2)


Наконец-то мы его получили, и начиная с версии 2.1.0-alpha01 Room поддерживает объекты с сопоставлением таблицы FTS3 или FTS4. Для получения дополнительной информации и примеров использования вы можете перейти к их документации: @Fts3 и @Fts4

person Michał Baran    schedule 10.10.2018

Я могу подтвердить, что это работает. Это усугубляет, но это работает.

Во-первых, вам нужно будет создать таблицу. Для первоначального создания базы данных вы можете использовать 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, но у вас не может быть private abstract методов, поэтому, пожалуйста, не используйте их, ладно?".

Обратите внимание, что ваши поисковые выражения FTS должны соответствовать документации SQLite FTS.

person CommonsWare    schedule 29.04.2018
comment
Это отлично работает! Хотя FTS не кажется таким надежным, как я надеялся; но я могу справиться. Кроме того, немного странно, что ваша функция insert() работает, учитывая документацию явно указывает, что RawQuery методы могут использоваться только для запросов на чтение. Для запросов на запись используйте RoomDatabase.getOpenHelper().getWritableDatabase(). - person Bryan; 30.04.2018
comment
@Bryan: Хорошо, я только что зарегистрировал проблему для ясности документации по сценарию @RawQuery записи. Я не уверен, чего, по вашему мнению, не хватает в поддержке SQLite FTS, поэтому я не могу дать много советов. - person CommonsWare; 30.04.2018
comment
@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}); Для запросов к базе данных. - person Ayokunle Paul; 06.08.2018
comment
@AyokunlePaul: Возможно, есть проблемы со значением name. Вот полный пример проекта, демонстрирующий использование ФТС с номером. Это немного странно, так как мне нужно упаковать данные для поиска, чтобы образец был автономным, но это может помочь, когда вы пытаетесь определить, что происходит с вашим собственным кодом. - person CommonsWare; 06.08.2018
comment
Спасибо. Теперь это работает. Проблема, с которой я столкнулся, была довольно сложной. - person Ayokunle Paul; 06.08.2018
comment
Это решение должно быть устаревшим, поскольку ваше заявление об использовании необработанных запросов не соответствует документации: developer.android.com/training/data-storage/room/defining-data - person AndroidDev; 12.06.2019
comment
@AndroidDev: я не уверен в вашей конкретной проблеме, поскольку на этой странице не обсуждается @RawQuery. Если вы можете использовать Room 2.1.0 или выше, обязательно используйте родной материал, описанный в другом ответе. Однако по состоянию на июнь 2019 года Room 2.1.0 не является окончательным, и не все могут использовать предварительные версии библиотек. - person CommonsWare; 12.06.2019