Как писать асинхронные запросы DAO

Чтобы запросы не блокировали интерфейс, Room не поддерживает доступ к базе данных в основном потоке. Это ограничение означает, что вы должны сделать запросы DAO асинхронными. Библиотека Room включает интеграции с несколькими фреймворками, позволяющими выполнять асинхронные запросы.

Запросы DAO делятся на три категории:

  • Однократные запросы на запись, которые вставляют, обновляют или удаляют данные в базе данных.
  • Однократные запросы на чтение, которые считывают данные из базы данных только один раз и возвращают результат со снимком базы данных на тот момент.
  • Запросы на чтение наблюдаемых данных, которые считывают данные из вашей базы данных каждый раз, когда изменяются базовые таблицы базы данных, и передают новые значения, отражающие эти изменения.

Языки и фреймворки

Room поддерживает интеграцию для обеспечения совместимости с определенными языковыми функциями и библиотеками. В таблице ниже показаны допустимые типы возвращаемых значений в зависимости от типа запроса и фреймворка:

Тип запроса Функции языка Kotlin (нативные) RxJava Гуава Jetpack Lifecycle*
Однократная запись Корутины (suspend) Single<T>, Maybe<T>, Completable ListenableFuture<T> Н/Д
Однократное чтение Корутины (suspend) Single<T>, Maybe<T> ListenableFuture<T> Н/Д
Наблюдаемый доступ Flow<T> Flowable<T>, Publisher<T>, Observable<T> Н/Д LiveData<T>

В этом руководстве показано, как использовать эти интеграции для реализации асинхронных запросов в объектах доступа к данным.

Kotlin с Flow и сопрограммами

В Kotlin есть встроенные языковые функции, которые позволяют писать асинхронные запросы без сторонних фреймворков:

  • Room напрямую поддерживает Flow из Kotlin, чтобы писать наблюдаемые запросы.
  • Для выполнения однократных запросов DAO в Room требуется ключевое слово suspend, чтобы сделать их асинхронными с помощью сопрограмм Kotlin.

Поддержка сопрограмм и потоков встроена непосредственно в основную среду выполнения Room, поэтому дополнительные артефакты не требуются.

RxJava для Kotlin и Java

Room 3.0 поддерживает типы возвращаемых значений RxJava 3. Чтобы использовать типы возвращаемых значений RxJava, необходимо зарегистрировать конвертеры типов возвращаемых значений RxJava в базе данных или DAO:

  1. Добавьте артефакт androidx.room3:room3-rxjava3 в конфигурацию сборки.
  2. Добавьте к декларации @Database или @Dao аннотацию @DaoReturnTypeConverters(RxDaoReturnTypeConverters::class).

Room поддерживает следующие типы возвращаемых значений RxJava 3:

LiveData и Guava

Room 3.0 поддерживает типы возвращаемых значений LiveData и Guava ListenableFuture с помощью конвертеров:

  • LiveData. Добавьте артефакт androidx.room3:room3-livedata и аннотируйте базу данных или DAO с помощью @DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class).
  • Guava. Добавьте артефакт androidx.room3:room3-guava и аннотируйте базу данных или объект доступа к данным с помощью @DaoReturnTypeConverters(GuavaDaoReturnTypeConverter::class).

Как писать асинхронные однократные запросы

Однократные запросы – это операции с базой данных, которые выполняются только один раз и получают моментальный снимок данных на момент выполнения. Вот несколько примеров асинхронных однократных запросов:

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    suspend fun loadUserById(id: Int): User

    @Query("SELECT * from user WHERE region IN (:regions)")
    suspend fun loadUsersByRegion(regions: List<String>): List<User>
}

Как писать наблюдаемые запросы

Наблюдаемые запросы – это операции чтения, которые возвращают новые значения при каждом изменении таблиц, на которые они ссылаются. Например, вы можете использовать это поведение, чтобы поддерживать актуальность списка элементов, отображаемого на экране, при изменении базы данных. Вот несколько примеров запросов, которые можно отслеживать:

@Dao
interface ObservableUserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flow<User>

    @Query("SELECT * from user WHERE region IN (:regions)")
    fun loadUsersByRegion(regions: List<String>): Flow<List<User>>
}

Как отслеживать аннулирование базы данных вручную

Если вам нужно вручную создать наблюдаемые операции с базами данных, вы можете использовать API createFlow InvalidationTracker. Этот API позволяет создать Flow, который отслеживает изменения в определенных таблицах и отправляет уведомления при их изменении.

fun getArtistTours(db: RoomDatabase, from: Date, to: Date): Flow<Map<Artist, TourState>> {
    return db.invalidationTracker.createFlow("Artist").map { _ ->
        val artists = artistsDao.getAllArtists()
        val tours = tourService.fetchStates(artists.map { it.id })
        associateTours(artists, tours, from, to)
    }
}

По умолчанию возвращаемый объект Flow испускает начальное значение, содержащее все зарегистрированные таблицы, чтобы запустить поток. Чтобы отключить эту функцию, задайте для параметра emitInitialState значение false.

Конвертеры типов возвращаемых значений для собственных объектов доступа к данным

Для типов, которые напрямую не поддерживаются Room или его библиотеками расширений, можно определить конвертеры возвращаемых значений DAO, чтобы поддерживать дополнительные типы возвращаемых значений. Чтобы преобразовать результат функции DAO в пользовательский тип, добавьте аннотацию @DaoReturnTypeConverter к функции преобразования.

Например, можно определить конвертер, который использует androidx.tracing, чтобы добавить разделы трассировки вокруг выполнения запроса для мониторинга запросов, чувствительных к производительности, путем обертывания выполнения в пользовательский тип TracedQuery:

class TracedQuery<T>(val result: T)

object TracingDaoReturnTypeConverter {
    @DaoReturnTypeConverter([OperationType.READ])
    suspend fun <T> convert(
        rawQuery: RoomRawQuery,
        executeAndConvert: suspend () -> T
    ): TracedQuery<T> {
        val result = trace("TracedQuery: ${rawQuery.sql}") {
            executeAndConvert()
        }
        return TracedQuery(result)
    }
}

Чтобы использовать конвертер, добавьте в базу данных или DAO аннотацию @DaoReturnTypeConverters:

@Dao
@DaoReturnTypeConverters(TracingDaoReturnTypeConverter::class)
interface MusicDao {
    @Query("SELECT * FROM Song")
    suspend fun getAllSongs(): TracedQuery<List<Song>>
}

Как управлять инициализацией конвертера возвращаемых типов DAO

Обычно Room обрабатывает создание конвертеров типов возвращаемых значений DAO. Однако если вам нужно передать дополнительные зависимости классам конвертеров, инициализацией этих классов должно управлять ваше приложение. В этом случае добавьте к классу конвертера аннотацию @ProvidedDaoReturnTypeConverter:

@ProvidedDaoReturnTypeConverter
class TracingDaoReturnTypeConverter(val tracer: Tracer) {
    @DaoReturnTypeConverter([OperationType.READ])
    suspend fun <T> convert(
        rawQuery: RoomRawQuery,
        executeAndConvert: suspend () -> T
    ): TracedQuery<T> {
        val result = tracer.trace("TracedQuery: ${rawQuery.sql}") {
            executeAndConvert()
        }
        return TracedQuery(result)
    }
}

Затем, помимо объявления класса конвертера в @DaoReturnTypeConverters, используйте функцию RoomDatabase.Builder.addDaoReturnTypeConverter, чтобы передать экземпляр класса конвертера в конструктор RoomDatabase:

val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name")
    .addDaoReturnTypeConverter(TracingDaoReturnTypeConverter(myLoggerInstance))
    .build()

Требования к функции конвертера

Функция @DaoReturnTypeConverter должна соответствовать нескольким требованиям:

  • Последним аргументом должна быть функциональная переменная, обычно с именем executeAndConvert. Этот параметр представляет собой лямбда-функцию suspend, которую Room создает для выполнения запроса и обработки результата.
    • Если конвертеру нужно преобразовать запрос, например для разбивки на страницы, лямбда-функция может принимать параметр RoomRawQuery.
  • Перед лямбда-выражением можно указать следующие параметры:
    • db: RoomDatabase: получает доступ к экземпляру базы данных, что полезно для получения области действия сопрограммы или выполнения дополнительных операций.
    • tableNames: Array<String> или List<String> – предоставляет названия таблиц, к которым обращается запрос. Это полезно для наблюдаемых типов.
    • rawQuery: RoomRawQuery: предоставляет экземпляр запроса во время выполнения.
    • inTransaction: Boolean – указывает, выполняется ли запрос в рамках транзакции.