Написание асинхронных запросов DAO

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

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

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

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

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

тип запроса Особенности языка Kotlin (нативные) RxJava Гуава Жизненный цикл реактивного ранца*
Однократная запись Корутины ( 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>

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

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

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

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

Поддержка сопрограмм и потоков встроена непосредственно в основную среду выполнения 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 и аннотируйте свою базу данных или DAO с помощью @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 .

Пользовательские преобразователи типов возврата DAO

Для типов, которые не поддерживаются напрямую 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)
    }
}

Для использования конвертера добавьте аннотацию ` @DaoReturnTypeConverters к вашей базе данных или DAO:

@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 : Указывает, выполняется ли запрос в рамках транзакции.