Escribe consultas DAO asíncronas

Para evitar que las consultas bloqueen la IU, Room no admite el acceso a la base de datos en el subproceso principal. Esta restricción implica que tus búsquedas DAO deben ser asíncronas. La biblioteca de Room incluye integraciones con varios frameworks para permitir la ejecución de consultas asíncronas.

Las búsquedas DAO se dividen en tres categorías:

  • Búsquedas de escritura única que insertan, actualizan o borran datos en la base de datos.
  • Búsquedas de lectura única que leen datos de tu base de datos solo una vez y muestran un resultado con la instantánea de la base de datos en ese momento.
  • Búsquedas de lectura observable que leen datos de tu base de datos cada vez que cambian las tablas de la base de datos subyacentes y emiten valores nuevos para reflejar esos cambios.

Opciones de framework y lenguaje

Room admite la interoperabilidad con funciones de lenguaje y bibliotecas específicas. En la siguiente tabla, se muestran los tipos de devolución aplicables según el tipo de búsqueda y el framework:

Tipo de consulta Funciones del lenguaje Kotlin (nativas) RxJava Guayaba Lifecycle de Jetpack*
Escritura por única vez Corrutinas (suspend) Single<T>, Maybe<T>, Completable ListenableFuture<T> N/A
Lectura de una toma Corrutinas (suspend) Single<T>, Maybe<T> ListenableFuture<T> N/A
Lectura observable Flow<T> Flowable<T>, Publisher<T>, Observable<T> N/A LiveData<T>

En esta guía, se muestran tres formas de usar estas integraciones para implementar búsquedas asíncronas en tus DAO.

Kotlin con Flow y corrutinas

Kotlin proporciona funciones de lenguaje integradas que te permiten escribir consultas asíncronas sin frameworks de terceros:

  • Room admite directamente el Flow de Kotlin para escribir búsquedas observables.
  • Room requiere la palabra clave suspend para que tus búsquedas DAO únicas sean asíncronas con corrutinas de Kotlin.

La compatibilidad con corrutinas y Flow está integrada directamente en el tiempo de ejecución principal de Room, por lo que no se requieren artefactos adicionales.

RxJava para Kotlin y Java

Room 3.0 admite tipos de datos que se muestran de RxJava 3. Para usar los tipos de datos que se devuelven de RxJava, debes registrar los convertidores de tipo de datos que se devuelven de RxJava en tu base de datos o DAO:

  1. Incluye el artefacto androidx.room3:room3-rxjava3 en tu configuración de compilación.
  2. Anota tu @Database o @Dao declaración con @DaoReturnTypeConverters(RxDaoReturnTypeConverters::class).

Room admite los siguientes tipos de datos que se muestran de RxJava 3:

LiveData y Guava

Room 3.0 admite tipos de datos que se muestran de LiveData y Guava ListenableFuture con convertidores:

  • LiveData: Incluye el artefacto androidx.room3:room3-livedata y anota tu base de datos o DAO con @DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class).
  • Guava: Incluye el artefacto androidx.room3:room3-guava y anota tu base de datos o DAO con @DaoReturnTypeConverters(GuavaDaoReturnTypeConverter::class).

Escritura de búsquedas asíncronas únicas

Las búsquedas únicas son operaciones de base de datos que solo se ejecutan una vez y toman una instantánea de los datos en el momento de la ejecución. Estos son algunos ejemplos de búsquedas asíncronas únicas:

@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>
}

Escritura de búsquedas observables

Las búsquedas observables son operaciones de lectura que emiten valores nuevos cada vez que cambian las tablas a las que se hace referencia. Por ejemplo, puedes usar este comportamiento para mantener actualizada una lista de elementos a medida que cambia la base de datos. Los siguientes son ejemplos comunes de búsquedas observables:

@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>>
}

Realiza el seguimiento de la invalidación de la base de datos de forma manual

Cuando necesites compilar operaciones de base de datos observables de forma manual, puedes usar la createFlow API de InvalidationTracker. Esta API te permite crear un Flow que realiza un seguimiento de las modificaciones en tablas específicas y emite una notificación cada vez que cambian esas tablas.

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)
    }
}

De forma predeterminada, el Flow que se muestra emite un valor inicial que contiene todas las tablas registradas para iniciar la transmisión. Para inhabilitar este comportamiento, configura el parámetro emitInitialState como false.

Convertidores de tipo de datos que se devuelve de DAO personalizados

Para los tipos que Room o sus bibliotecas de extensión no admiten directamente, puedes definir convertidores de tipo de datos que se muestran de DAO personalizados para admitir tipos de datos que se muestran adicionales. Para transformar el resultado de una función DAO en tu tipo personalizado, anota una función de convertidor con @DaoReturnTypeConverter.

Por ejemplo, puedes definir un convertidor que use androidx.tracing para agregar secciones de seguimiento en torno a la ejecución de una búsqueda para supervisar las búsquedas sensibles al rendimiento envolviendo la ejecución en un tipo TracedQuery personalizado:

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)
    }
}

Para usar el convertidor, anota tu base de datos o DAO con @DaoReturnTypeConverters:

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

Cómo controlar la inicialización de convertidores de tipo de datos que se muestran de DAO

Por lo general, Room se ocupa de la creación de instancias de convertidores de tipo de datos que se muestran de DAO. Sin embargo, si debes pasar dependencias adicionales a las clases de convertidores, tu app debe controlar directamente su inicialización. Si es así, anota tu clase de convertidor con @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)
    }
}

Luego, además de declarar tu clase de convertidor en @DaoReturnTypeConverters, usa la RoomDatabase.Builder.addDaoReturnTypeConverter función para pasar una instancia de tu clase de convertidor al compilador RoomDatabase:

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

Requisitos de la función de convertidor

Una función @DaoReturnTypeConverter debe cumplir varios requisitos:

  • Debe tener un parámetro funcional como su último argumento, que suele llamarse executeAndConvert. Este parámetro es una expresión lambda suspend que Room genera para ejecutar la búsqueda y analizar el resultado.
    • Si el convertidor necesita transformar la búsqueda, como Paging, la expresión lambda puede tomar un parámetro RoomRawQuery.
  • De manera opcional, puede aceptar los siguientes parámetros antes de la expresión lambda:
    • db: RoomDatabase: Accede a la instancia de la base de datos, lo que resulta útil para obtener el alcance de la corrutina o realizar operaciones adicionales.
    • tableNames: Array<String> o List<String>: Proporciona los nombres de las tablas a las que accede la búsqueda, lo que resulta útil para los tipos observables.
    • rawQuery: RoomRawQuery: Proporciona la instancia de tiempo de ejecución de la búsqueda.
    • inTransaction: Boolean: Indica si la búsqueda se está ejecutando dentro de una transacción.