Cuando usas la biblioteca de persistencias Room para almacenar los datos de tu app, interactúas con los datos almacenados mediante la definición de objetos de acceso a datos o DAO. Cada DAO incluye funciones que ofrecen acceso abstracto a la base de datos de tu app. En el tiempo de compilación, Room genera automáticamente implementaciones de los DAOs que definas.
Si usas DAOs para acceder a la base de datos de tu app en lugar de compiladores de búsquedas o búsquedas directas, puedes conservar la separación de problemas, un principio arquitectónico importante. Los DAOs también te permiten simular el acceso a la base de datos cuando pruebas tu app.
Anatomía de un DAO
Puedes definir cada DAO como una interfaz o una clase abstracta. Por lo general, debes usar una interfaz para casos de uso básicos. En cualquier caso, siempre debes
anotar tus DAOs con @Dao. Los DAOs no tienen propiedades, pero definen una o más funciones para interactuar con los datos de la base de datos de tu app.
El siguiente código es un ejemplo de un DAO que define funciones para insertar, borrar y seleccionar objetos User en una base de datos de Room:
@Dao interface UserDao { @Insert suspend fun insertAll(vararg users: User) @Delete suspend fun delete(user: User) @Query("SELECT * FROM user") suspend fun getAll(): List<User> }
Existen dos tipos de funciones DAO que definen las interacciones de la base de datos:
- Funciones de conveniencia que te permiten insertar, actualizar y borrar filas en tu base de datos sin escribir ningún código de SQL.
- Funciones de búsqueda que te permiten escribir tu propia consulta en SQL para interactuar con la base de datos.
En las siguientes secciones, se muestra cómo usar ambos tipos de funciones DAO para definir las interacciones de la base de datos que necesita tu app.
Funciones de conveniencia
Room proporciona anotaciones de conveniencia para definir funciones que permiten realizar inserciones, actualizaciones y eliminaciones sin necesidad de escribir una instrucción de SQL.
Si necesitas definir inserciones, actualizaciones o eliminaciones más complejas, o bien si quieres consultar los datos de la base de datos, usa una función de búsqueda en su lugar.
Insertar
La anotación @Insert te permite definir funciones que insertan sus
parámetros en la tabla adecuada en la base de datos. En el siguiente código, se muestran ejemplos de funciones @Insert válidas que insertan uno o más objetos User en la base de datos:
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUsers(vararg users: User) @Insert suspend fun insertBothUsers(user1: User, user2: User) @Insert suspend fun insertUsersAndFriends(user: User, friends: List<User>) }
Cada parámetro para una función @Insert debe ser una instancia de una clase de entidad de datos de Room
con @Entity como anotación o una colección de instancias de clase de entidad de datos
. Cuando se llama a una función @Insert, Room inserta cada instancia de entidad pasada en la tabla de base de datos correspondiente.
Si la función @Insert recibe un solo parámetro, puede mostrar un valor Long, que es el nuevo rowId para el elemento insertado. Si el parámetro es un array o una colección, muestra un array o una colección de valores Long en su lugar, donde cada valor debe ser el rowId de uno de los elementos insertados.
Si quieres obtener más información sobre los valores rowId que se muestran, consulta la documentación de referencia de la anotación @Insert y la documentación de SQLite para tablas de rowid.
Actualizar
La anotación @Update te permite definir funciones que actualizan filas específicas
en una tabla de base de datos. Al igual que las funciones @Insert, las funciones @Update aceptan instancias de entidades de datos como parámetros. El siguiente código muestra un ejemplo de una función @Update que intenta actualizar uno o más objetos User en la base de datos:
@Dao interface UserDao { @Update suspend fun updateUsers(vararg users: User) }
Room usa la clave primaria para hacer coincidir las instancias de entidades en argumentos con las filas en la base de datos. Si no hay una fila con la misma clave primaria, Room no realiza cambios.
De manera opcional, una función @Update puede mostrar un valor Int que indica la cantidad de filas que se actualizaron de forma correcta.
Borrar
La anotación @Delete te permite
definir funciones que borran filas específicas de una tabla de base de datos. Al igual que las funciones @Insert, las funciones @Delete aceptan instancias de entidades de datos como parámetros. En el siguiente código, se muestra un ejemplo de una función @Delete que intenta borrar uno o más objetos User de la base de datos:
@Dao interface UserDao { @Delete suspend fun deleteUsers(vararg users: User) }
Room usa la clave primaria para hacer coincidir las instancias de entidades en argumentos con las filas en la base de datos. Si no hay una fila con la misma clave primaria, Room no realiza cambios.
De manera opcional, una función @Delete puede mostrar un valor Int que indique la cantidad de filas que se borraron de forma correcta.
Actualizar o insertar
La anotación @Upsert te permite
definir funciones que insertan instancias de entidades cuando no hay una fila coincidente o
las actualizan si ya existe una fila con la misma clave primaria.
Al igual que las funciones @Insert y @Update, las funciones @Upsert aceptan instancias de entidades de datos como parámetros. El siguiente código muestra un ejemplo de una función @Upsert que intenta actualizar o insertar uno o más objetos User en la base de datos:
@Dao interface UserDao { @Upsert suspend fun upsertUsers(vararg users: User) }
Si la función @Upsert recibe un solo parámetro, puede mostrar un valor Long. Si se inserta una fila nueva, muestra el rowId de la fila recién insertada. Si se actualiza una fila existente, muestra -1. Si el parámetro es un array o una colección, debería mostrar un array o una colección de valores Long.
Funciones de búsqueda
La anotación @Query te permite
escribir instrucciones de SQL y exponerlas como funciones DAO. Usa estas funciones de búsqueda para consultar datos desde la base de datos de tu app o cuando necesites realizar inserciones, actualizaciones y eliminaciones más complejas.
Room valida las consultas en SQL en el tiempo de compilación. Esto significa que, si hay un problema con tu búsqueda, se produce un error de compilación en lugar de una falla del tiempo de ejecución.
Consultas simples
El siguiente código define una función que usa una consulta SELECT para mostrar todos los objetos User de la base de datos:
@Query("SELECT * FROM user") suspend fun loadAllUsers(): List<User>
En las siguientes secciones, se muestra cómo modificar este ejemplo para casos de uso típicos.
Cómo mostrar un subconjunto de columnas de una tabla
La mayoría de las veces, solo necesitas mostrar un subconjunto de las columnas de la tabla que estás consultando. Por ejemplo, tu IU podría mostrar solo el nombre y el apellido de un usuario, en lugar de todos los detalles sobre ese usuario. Para ahorrar recursos y optimizar la ejecución de tu búsqueda, solo debes consultar las propiedades que necesitas.
Room te permite mostrar un objeto de datos de cualquiera de tus búsquedas, siempre y cuando puedas asignar el conjunto de columnas de resultados al objeto que se muestra. Por ejemplo, puedes definir el siguiente objeto para conservar el nombre y apellido de un usuario:
data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String )
Luego, puedes mostrar ese objeto de datos desde tu función de búsqueda:
@Query("SELECT first_name, last_name FROM user") suspend fun loadFullName(): List<NameTuple>
Debido a que la búsqueda muestra valores para las columnas first_name y last_name, Room asigna estos valores a las propiedades de la clase NameTuple. Si la búsqueda muestra una columna que no se asigna a una propiedad en el objeto que se muestra, Room muestra una advertencia.
Aunque el ejemplo anterior usa una clase de datos personalizada para recuperar un subconjunto de columnas, Room también admite el uso de kotlin.Pair y kotlin.Triple para mayor comodidad cuando una búsqueda muestra exactamente dos o tres columnas. Cuando se usan estos tipos, las columnas se asignan según el orden en que se definen en la instrucción de búsqueda, por lo que el orden de las columnas en la instrucción SELECT debe coincidir con el orden de los tipos en Pair o Triple.
Cómo pasar parámetros simples a una búsqueda
La mayoría de las veces, las funciones DAO deben aceptar parámetros para que puedan realizar operaciones de filtrado. Room admite el uso de parámetros de funciones como parámetros de vinculación en tus búsquedas.
Por ejemplo, el siguiente código define una función que muestra todos los usuarios mayores de cierta edad:
@Query("SELECT * FROM user WHERE age > :minAge") suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>
También puedes pasar varios parámetros o hacer referencia al mismo parámetro varias veces en una búsqueda, como se muestra en el siguiente código:
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User> @Query( """ SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search """ ) suspend fun findUserWithName(search: String): List<User>
Cómo pasar una colección de parámetros a una búsqueda
Es posible que algunas de tus funciones DAO requieran que pases una cantidad variable de parámetros que no se conocen hasta el tiempo de ejecución. Si un parámetro representa una colección, se expande automáticamente en el tiempo de ejecución en función de la cantidad de valores.
Por ejemplo, el siguiente código define una función que muestra información sobre todos los usuarios de un subconjunto de regiones:
@Query("SELECT * FROM user WHERE region IN (:regions)") suspend fun loadUsersFromRegions(regions: List<String>): List<User>
Cómo realizar consultas en varias tablas
Algunas de tus búsquedas pueden requerir acceso a varias tablas para calcular el resultado. Puedes usar cláusulas JOIN en tus consultas en SQL para hacer referencia a más de una tabla.
El siguiente código define una función que une tres tablas para mostrar los libros que se prestaron a un usuario específico:
@Query( """ SELECT * FROM book INNER JOIN loan ON loan.book_id = book.id INNER JOIN user ON user.id = loan.user_id WHERE user.name LIKE :userName """ ) suspend fun findBooksBorrowedByName(userName: String): List<Book>
También puedes definir objetos de datos para mostrar un subconjunto de columnas de varias tablas unidas. Para obtener más información, consulta Cómo mostrar un subconjunto de columnas de una tabla. El siguiente código define un DAO con una función que muestra los nombres de los usuarios y los nombres de los libros que tomaron prestados:
interface UserBookDao { @Query( """ SELECT user.name AS userName, book.name AS bookName FROM user, book WHERE user.id = book.user_id """ ) fun loadUserAndBookNames(): Flow<List<UserBook>> } data class UserBook(val userName: String, val bookName: String)
Cómo mostrar un multimapa
Para las operaciones de unión, también puedes realizar búsquedas en columnas de varias tablas sin definir una clase de datos adicional mediante la escritura de funciones de búsqueda que muestren un multimapa.
Considera el ejemplo de Cómo realizar búsquedas en varias tablas. En lugar de mostrar una lista de instancias de una clase de datos personalizada que contiene pares de instancias User y Book, puedes mostrar una asignación de User y Book directamente desde tu función de búsqueda:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNames(): Map<User, List<Book>>
Cuando tu función de búsqueda muestra un multimapa, puedes escribir búsquedas que usen cláusulas GROUP BY, lo que te permite aprovechar las capacidades de SQL para los cálculos avanzados y el filtrado. Por ejemplo, puedes modificar tu función loadUserAndBookNames para que solo muestre los usuarios con tres o más libros prestados:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id GROUP BY user.name HAVING COUNT(book.id) >= 3 """ ) suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>
Si no necesitas asignar objetos enteros, también puedes mostrar asignaciones entre
columnas específicas de tu búsqueda si usas la anotación @MapColumn en los
parámetros genéricos del tipo que se muestra.
@Query( """ SELECT user.name AS username, book.name AS bookname FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNamesColumns(): Map< @MapColumn(columnName = "username") String, List<@MapColumn(columnName = "bookname") String> >
Tipos de datos especiales que se muestran
Room proporciona algunos tipos de datos especiales que se muestran para la integración con otras bibliotecas de API.
Búsquedas paginadas con la biblioteca de Paging
Room admite búsquedas paginadas a través de la integración con la biblioteca de Paging. Para usar los tipos de datos que se devuelven de Paging 3, debes registrar los conversores de tipos de datos que se devuelven de Paging en tu base de datos o DAO:
- Incluye el artefacto
androidx.room3:room3-pagingen la configuración de tu compilación. - Anota tu
@Databaseo@Daodeclaración con@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class).
Una vez registrados, tus DAOs pueden mostrar PagingSource objetos para usarlos con
Paging 3:
@Dao @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
Si quieres obtener información para elegir parámetros de tipo para un PagingSource, consulta
Cómo seleccionar tipos de clave y de valor.
Acceso directo a la conexión de la base de datos
Si la lógica de tu app requiere acceso directo y de bajo nivel a la conexión de la base de datos, puedes usar las APIs de conexión de Room. Puedes obtener una
conexión con
useReaderConnection
para operaciones de solo lectura o
useWriterConnection
para operaciones de escritura en tu RoomDatabase instancia y usar
usePrepared
para ejecutar instrucciones:
val result: List<Pair<Long, String>> = roomDatabase.useReaderConnection { connection -> connection.usePrepared( "SELECT * FROM user WHERE age > :minAge LIMIT 5" ) { stmt -> // Bind arguments if needed stmt.bindLong(1, minAge.toLong()) buildList { // Step through the results while (stmt.step()) { add(stmt.getLong(0) to stmt.getText(1)) } } } }
Si necesitas realizar transacciones de base de datos de bajo nivel directamente en la
conexión, puedes usar las funciones auxiliares immediateTransaction,
deferredTransaction o exclusiveTransaction en
una instancia Transactor dentro de un bloque useWriterConnection:
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
Como alternativa, si solo necesitas ejecutar operaciones DAO de alto nivel en una
transacción, usa las withReadTransaction o withWriteTransaction
funciones de extensión auxiliares en tu instancia RoomDatabase:
// Perform transactional read operations (DEFERRED transaction) val userCount = roomDatabase.withReadTransaction { userDao.countUsers() } // Perform transactional write operations (IMMEDIATE transaction) roomDatabase.withWriteTransaction { userDao.insert(newUser) userDao.update(existingUser) }