Cómo acceder a los datos con DAO de Room

Para acceder a los datos de tu app con la biblioteca de persistencias Room, trabaja con objetos de acceso a datos o DAO. Este conjunto de objetos Dao forma el componente principal de Room, ya que cada DAO incluye métodos que ofrecen acceso abstracto a la base de datos de tu app.

Al acceder a una base de datos con una clase DAO, en lugar de compiladores de consultas o consultas directas, puedes separar diferentes componentes de la arquitectura de tu base de datos. Además, las clases DAO te permiten simular fácilmente el acceso a la base de datos mientras pruebas tu app.

Una clase DAO puede ser una interfaz o una clase abstracta. Si es una clase abstracta, opcionalmente, puede tener un constructor que tome un elemento RoomDatabase como único parámetro. Room crea cada implementación de DAO en el momento de la compilación.

Nota: Room no admite el acceso a la base de datos en el subproceso principal a menos que hayas llamado a allowMainThreadQueries() en el compilador, ya que podría bloquear la IU durante un largo período. Las consultas asíncronas, que son aquellas que muestran instancias de LiveData o Flowable, están exentas de esta regla, ya que ejecutan la consulta de manera asíncrona en un subproceso en segundo plano cuando es necesario.

Cómo definir métodos para mayor comodidad

Hay múltiples consultas de conveniencia que puedes representar mediante una clase DAO. En este documento, se incluyen varios ejemplos comunes.

Insert

Cuando creas un método DAO y lo anotas con @Insert, Room genera una implementación que inserta todos los parámetros en la base de datos con una sola transacción.

En el siguiente fragmento de código, se muestran varias consultas de ejemplo:

Kotlin

    @Dao
    interface MyDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        fun insertUsers(vararg users: User)

        @Insert
        fun insertBothUsers(user1: User, user2: User)

        @Insert
        fun insertUsersAndFriends(user: User, friends: List<User>)
    }
    

Java

    @Dao
    public interface MyDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        public void insertUsers(User... users);

        @Insert
        public void insertBothUsers(User user1, User user2);

        @Insert
        public void insertUsersAndFriends(User user, List<User> friends);
    }
    

Si el método @Insert recibe solo 1 parámetro, puede mostrar un objeto long, que es el nuevo objeto rowId para el elemento insertado. Si el parámetro es un arreglo o una colección, debería mostrar long[] o List<Long> en su lugar.

Para obtener más información, consulta la documentación de referencia sobre la anotación @Insert, así como la documentación de SQLite para las tablas de rowid.

Update

El método de conveniencia Update modifica un conjunto de entidades, proporcionadas como parámetros, en la base de datos. Utiliza una consulta que coincide con la clave primaria de cada entidad.

En el siguiente fragmento de código, se muestra cómo definir este método:

Kotlin

    @Dao
    interface MyDao {
        @Update
        fun updateUsers(vararg users: User)
    }
    

Java

    @Dao
    public interface MyDao {
        @Update
        public void updateUsers(User... users);
    }
    

Si bien no suele ser necesario, puedes hacer que este método muestre un valor int, que indica la cantidad de filas actualizadas en la base de datos.

Delete

El método de conveniencia Delete quita un conjunto de entidades, proporcionadas como parámetros, de la base de datos. Utiliza las claves primarias para encontrar las entidades que se borrarán.

En el siguiente fragmento de código, se muestra cómo definir este método:

Kotlin

    @Dao
    interface MyDao {
        @Delete
        fun deleteUsers(vararg users: User)
    }
    

Java

    @Dao
    public interface MyDao {
        @Delete
        public void deleteUsers(User... users);
    }
    

Si bien no suele ser necesario, puedes hacer que este método muestre un valor int, que indica la cantidad de filas que se quitaron de la base de datos.

Consulta de información

@Query es la anotación principal utilizada en las clases DAO. Te permite realizar operaciones de lectura/escritura en una base de datos. Cada método @Query se verifica en el momento de la compilación, por lo que si hay un problema con la consulta, se produce un error de compilación, y no uno de tiempo de ejecución.

Room también verifica el valor que se muestra de la consulta, de modo que si el nombre del campo en el objeto que se muestra no coincide con los nombres de las columnas correspondientes en la respuesta de la consulta, Room te alerta de una de estas dos maneras:

  • Muestra una advertencia si solo coinciden algunos nombres de campos.
  • Muestra un error si no coinciden los nombres de los campos.

Consultas simples

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user")
        fun loadAllUsers(): Array<User>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user")
        public User[] loadAllUsers();
    }
    

Esta es una consulta muy simple que carga todos los usuarios. En el momento de compilación, Room sabe que está consultando todas las columnas de la tabla de usuarios. Si la consulta contiene un error de sintaxis o si la tabla de usuarios no existe en la base de datos, Room muestra un error con el mensaje apropiado a medida que se compila tu app.

Cómo pasar parámetros a la consulta

La mayoría de las veces, debes pasar parámetros a consultas con el objetivo de realizar operaciones de filtrado, como mostrar solo usuarios mayores de cierta edad. Para llevar a cabo esta tarea, usa los parámetros del método en tu anotación Room, como se muestra en el siguiente fragmento de código:

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge")
        fun loadAllUsersOlderThan(minAge: Int): Array<User>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge")
        public User[] loadAllUsersOlderThan(int minAge);
    }
    

Cuando esta consulta se procesa en el momento de la compilación, Room hace coincidir el parámetro de vinculación :minAge con el parámetro del método minAge. Room realiza la coincidencia con los nombres de los parámetros. Si hay una falta de coincidencia, se produce un error al compilar tu app.

También puedes pasar varios parámetros o hacer referencia a ellos varias veces en una consulta, como se muestra en el siguiente fragmento de código:

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
        fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

        @Query("SELECT * FROM user WHERE first_name LIKE :search " +
               "OR last_name LIKE :search")
        fun findUserWithName(search: String): List<User>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
        public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

        @Query("SELECT * FROM user WHERE first_name LIKE :search " +
               "OR last_name LIKE :search")
        public List<User> findUserWithName(String search);
    }
    

Cómo mostrar subconjuntos de columnas

En la mayoría de los casos, solo necesitas obtener algunos campos de una entidad. Por ejemplo, tu IU puede mostrar solo el nombre y el apellido de un usuario, en lugar de mostrar todos sus detalles. Gracias a que obtienes solo las columnas que aparecen en la IU de tu app, ahorras recursos valiosos y la consulta se completa más rápido.

Room te permite mostrar cualquier objeto basado en Java de tus consultas siempre que el conjunto de columnas de resultados se pueda asignar al objeto que se muestra. Por ejemplo, puedes crear el siguiente objeto antiguo y sin formato basado en Java (POJO) para obtener el nombre y apellido del usuario:

Kotlin

    data class NameTuple(
        @ColumnInfo(name = "first_name") val firstName: String?,
        @ColumnInfo(name = "last_name") val lastName: String?
    )
    

Java

    public class NameTuple {
        @ColumnInfo(name = "first_name")
        public String firstName;

        @ColumnInfo(name = "last_name")
        @NonNull
        public String lastName;
    }
    

Ahora, puedes usar este POJO en tu método de consulta de la siguiente manera:

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user")
        fun loadFullName(): List<NameTuple>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user")
        public List<NameTuple> loadFullName();
    }
    

Room entiende que la consulta muestra valores en las columnas first_name y last_name, y que esos valores pueden asignarse a los campos de la clase NameTuple. Por lo tanto, Room puede generar el código correcto. Si la consulta muestra demasiadas columnas o una columna que no existe en la clase NameTuple, Room enviará una advertencia.

Cómo pasar una colección de argumentos

Es posible que algunas de las consultas requieran que se pase una cantidad variable de parámetros y que no se conozca la cantidad exacta de parámetros hasta el tiempo de ejecución. Por ejemplo, es posible que quieras recuperar información sobre todos los usuarios de un subconjunto de regiones. Room entiende cuándo un parámetro representa una colección y lo expande automáticamente en el tiempo de ejecución en función de la cantidad de parámetros proporcionados.

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        public List<NameTuple> loadUsersFromRegions(List<String> regions);
    }
    

Consultas observables

Al realizar consultas, a menudo desearás que la IU de tu app se actualice automáticamente cuando cambien los datos. Para lograrlo, usa un valor de muestra de tipo LiveData en la descripción del método de consulta. Room genera todo el código necesario para actualizar LiveData cuando se actualiza la base de datos.

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
        public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
    }
    

Consultas reactivas con RxJava

Room proporciona la siguiente compatibilidad con valores de muestra de tipos RxJava2:

Para usar esta funcionalidad, incluye la versión más reciente del artefacto rxjava2 en el archivo build.gradle de tu app:

app/build.gradle

    dependencies {
        def room_version = "2.1.0"
        implementation 'androidx.room:room-rxjava2:$room_version'
    }
    

Para ver las versiones actuales de esta biblioteca, consulta la información sobre Room en la página de versiones.

En el siguiente fragmento de código, se muestran varios ejemplos de cómo podrías usar estos tipos de datos que se muestran:

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT * from user where id = :id LIMIT 1")
        fun loadUserById(id: Int): Flowable<User>

        // Emits the number of users added to the database.
        @Insert
        fun insertLargeNumberOfUsers(users: List<User>): Maybe<Int>

        // Makes sure that the operation finishes successfully.
        @Insert
        fun insertLargeNumberOfUsers(varargs users: User): Completable

        /* Emits the number of users removed from the database. Always emits at
           least one user. */
        @Delete
        fun deleteAllUsers(users: List<User>): Single<Int>
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * from user where id = :id LIMIT 1")
        public Flowable<User> loadUserById(int id);

        // Emits the number of users added to the database.
        @Insert
        public Maybe<Integer> insertLargeNumberOfUsers(List<User> users);

        // Makes sure that the operation finishes successfully.
        @Insert
        public Completable insertLargeNumberOfUsers(User... users);

        /* Emits the number of users removed from the database. Always emits at
           least one user. */
        @Delete
        public Single<Integer> deleteUsers(List<User> users);
    }
    

Para obtener información detallada, consulta el artículo de Google Developers Room y RxJava.

Acceso directo al cursor

Si la lógica de tu app requiere acceso directo a las filas que se muestran, puedes mostrar un objeto Cursor de tus consultas, como se muestra en el siguiente fragmento de código:

Kotlin

    @Dao
    interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
        fun loadRawUsersOlderThan(minAge: Int): Cursor
    }
    

Java

    @Dao
    public interface MyDao {
        @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
        public Cursor loadRawUsersOlderThan(int minAge);
    }
    

Precaución: No se recomienda trabajar con la API del cursor porque no garantiza que las filas existen o qué valores contienen. Usa esta funcionalidad solo si ya tienes un código que espera un cursor y que no se puede refactorizar fácilmente.

Cómo hacer consultas en varias tablas

Algunas de tus consultas pueden requerir acceso a varias tablas para calcular el resultado. Room te permite escribir cualquier consulta, por lo que también puedes unir tablas. Por otra parte, si la respuesta es un tipo de datos observables, como Flowable o LiveData, Room observa todas las tablas de referencia de la consulta en busca de invalidación.

En el siguiente fragmento de código, se muestra cómo realizar una combinación de tablas para consolidar información entre una tabla que contiene usuarios que toman prestados libros y una tabla que contiene datos sobre libros prestados actualmente:

Kotlin

    @Dao
    interface MyDao {
        @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"
        )
        fun findBooksBorrowedByNameSync(userName: String): List<Book>
    }
    

Java

    @Dao
    public interface MyDao {
        @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")
       public List<Book> findBooksBorrowedByNameSync(String userName);
    }
    

También puedes mostrar objetos POJO desde estas consultas. Por ejemplo, puedes escribir una consulta que cargue a un usuario y el nombre de su mascota de la siguiente manera:

Kotlin

    @Dao
    interface MyDao {
        @Query(
            "SELECT user.name AS userName, pet.name AS petName " +
            "FROM user, pet " +
            "WHERE user.id = pet.user_id"
        )
        fun loadUserAndPetNames(): LiveData<List<UserPet>>

        // You can also define this class in a separate file.
        data class UserPet(val userName: String?, val petName: String?)
    }
    

Java

    @Dao
    public interface MyDao {
       @Query("SELECT user.name AS userName, pet.name AS petName " +
              "FROM user, pet " +
              "WHERE user.id = pet.user_id")
       public LiveData<List<UserPet>> loadUserAndPetNames();

       // You can also define this class in a separate file, as long as you add the
       // "public" access modifier.
       static class UserPet {
           public String userName;
           public String petName;
       }
    }
    

Cómo escribir métodos asincrónicos con corrutinas de Kotlin

Puedes agregar la palabra clave de Kotlin suspend a tus métodos DAO para hacerlos asíncronos con la funcionalidad de las rutinas de Kotlin. De esta manera, te aseguras de que no se puedan ejecutar en el subproceso principal.

@Dao
    interface MyDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        suspend fun insertUsers(vararg users: User)

        @Update
        suspend fun updateUsers(vararg users: User)

        @Delete
        suspend fun deleteUsers(vararg users: User)

        @Query("SELECT * FROM user")
        suspend fun loadAllUsers(): Array<User>
    }
    

Esta guía también se aplica a los métodos DAO anotados con @Transaction. Puedes usar esta función para compilar métodos de bases de datos de suspensión a partir de otros métodos DAO. Estos métodos se ejecutan en una sola transacción de base de datos.

@Dao
    abstract class UsersDao {
        @Transaction
        open suspend fun setLoggedInUser(loggedInUser: User) {
            deleteUser(loggedInUser)
            insertUser(loggedInUser)
        }

        @Query("DELETE FROM users")
        abstract fun deleteUser(user: User)

        @Insert
        abstract suspend fun insertUser(user: User)
    }
    

Para obtener más información sobre el uso de las corrutinas de Kotlin en tu app, consulta Cómo mejorar el rendimiento de la app con corrutinas de Kotlin.