Avatar ·

Android Room: как правильно использовать LIKE в запросах с параметрами?

Всем привет! Я новичок в Android-разработке на Kotlin и использую Room для работы с локальной базой данных. У меня есть сущность User, и я хочу реализовать поиск пользователей по имени с использованием SQL-оператора LIKE.

Вот моя DAO-сущность:

@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE name LIKE '%:searchQuery%'")
    suspend fun searchUsers(searchQuery: String): List<User>
}

Я вызываю этот метод из ViewModel:

class UserViewModel(private val userDao: UserDao) : ViewModel() {
    fun search(query: String) {
        viewModelScope.launch {
            val users = userDao.searchUsers(query)
            // Обработка результатов
        }
    }
}

Проблема в том, что запрос не возвращает никаких результатов, хотя в базе есть записи с именами, содержащими искомую строку. Я пробовал передавать разные значения — полные имена, части строк — ничего не работает. Подозреваю, что проблема в том, как я формирую SQL-запрос с LIKE. Может быть, Room не подставляет параметр в паттерн LIKE? Или нужно как-то экранировать символы? Буду благодарен за объяснение и рабочий пример.

Заранее спасибо!

<<>>

Проблема: Room не подставляет параметр внутрь строки паттерна LIKE

Вы правы — это типичная ошибка новичков. Room обрабатывает параметры запроса как подготовленные операторы (prepared statements). Когда вы пишете LIKE '%:searchQuery%', Room видит это как строковый литерал '%:searchQuery%', а не как паттерн с подстановкой параметра. Символ : внутри одинарных кавычек не распознаётся как привязка параметра — это просто часть строки.

Правильное решение: конкатенация в SQL или передача готового паттерна

Вам нужно формировать паттерн LIKE за пределами запроса (на стороне Kotlin) и передавать его как готовый параметр. Вот два рабочих подхода:

Вариант 1: Конкатенация в SQL-запросе (рекомендуется)

Используйте SQL-функцию конкатенации || (стандарт SQL) или специфичный для SQLite оператор ||:

@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE name LIKE '%' || :searchQuery || '%'")
    suspend fun searchUsers(searchQuery: String): List<User>
}

Теперь searchQuery будет корректно подставлен как параметр, а паттерн соберётся на уровне SQL. Можно использовать и другой синтаксис:

@Query("SELECT * FROM users WHERE name LIKE :pattern")
suspend fun searchUsers(pattern: String): List<User>

Но тогда вы должны передавать готовый паттерн из вызывающего кода:

val pattern = "%$query%"
val users = userDao.searchUsers(pattern)

Вариант 2: Экранирование спецсимволов (для безопасности)

Если пользователь может ввести символы % или _, их нужно экранировать, чтобы они не интерпретировались как wildcard-символы SQL. Добавьте вспомогательную функцию:

fun String.escapeForLike(): String {
    return this.replace("\\", "\\\\")
               .replace("%", "\\%")
               .replace("_", "\\_")
}

// В DAO:
@Query("SELECT * FROM users WHERE name LIKE :pattern ESCAPE '\\'")
suspend fun searchUsers(pattern: String): List<User>

И используйте так:

val safeQuery = query.escapeForLike()
val pattern = "%$safeQuery%"
val users = userDao.searchUsers(pattern)

Полный рабочий пример

// DAO
@Dao
interface UserDao {
    @Query("SELECT * FROM users WHERE name LIKE '%' || :query || '%'")
    suspend fun searchUsers(query: String): List<User>
}

// ViewModel
class UserViewModel(private val userDao: UserDao) : ViewModel() {
    fun search(query: String) {
        viewModelScope.launch {
            try {
                val users = userDao.searchUsers(query)
                // Обработка результатов
            } catch (e: Exception) {
                // Логирование ошибки
            }
        }
    }
}

Почему это работает?

  • Room использует SQLiteStatement для подстановки параметров — это защищает от SQL-инъекций.
  • Параметр :query связывается с переданным значением, а символы % добавляются уже в SQL.
  • При таком подходе не нужно беспокоиться о кавычках — Room обрабатывает экранирование автоматически.

Если ваша версия Room поддерживает @RawQuery, можно использовать и его, но приведённый выше способ — самый простой и надёжный для LIKE-запросов.

<<>>

Альтернативный подход: использование FTS (Full-Text Search) для сложного поиска

Если вам нужен более мощный поиск с учётом релевантности, опечаток или морфологии, рассмотрите использование FTS4 или FTS5 в SQLite. Room поддерживает FTS через специальные сущности.

Пример настройки FTS

@Entity(tableName = "users_fts")
@Fts4(contentEntity = User::class)
data class UserFts(
    val name: String
)

@Dao
interface UserDao {
    @Query("SELECT * FROM users_fts WHERE users_fts MATCH :query")
    suspend fun searchUsersFts(query: String): List<User>
}

Вызывайте так:

val ftsQuery = query.replace(" ", "* ") + "*"  // Префиксный поиск
val users = userDao.searchUsersFts(ftsQuery)

Когда использовать FTS?

  • Когда нужен поиск по нескольким полям сразу.
  • Для поддержки нечёткого поиска (например, "джон" найдёт "Джон", "джонни").
  • Для больших объёмов данных — FTS работает быстрее, чем LIKE.

Однако для простого поиска по одному полю LIKE с конкатенацией — это самый лёгкий и понятный вариант. FTS добавляет сложность в виде отдельной таблицы и необходимости синхронизации данных.