Недавно я возился с компонентами архитектуры 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 даже правильным путем (кажется излишним) или есть какой-то другой метод, более подходящий для моего варианта использования?
@Entity
и вы взламываете оператор Room-createdCREATE TABLE
. Мы только что получили рекомендацию найти обходной путь (например, в последние 20 минут или около того), но я бы не стал так поступать. Вместо этого я бы использовалRoomDatabase.Callback
и миграции, вручную создавая и изменяя таблицу вSupportSQLiteDatabase
. - person CommonsWare   schedule 04.04.2018