Cómo acceder a los datos con DAO de Room

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 métodos 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 con mayor facilidad el acceso a la bases 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 uno o más métodos para interactuar con los datos de la base de datos de tu app.

El siguiente código es un ejemplo de un DAO simple que define métodos para insertar, borrar y seleccionar objetos User en una base de datos de Room:

Kotlin

@Dao
interface UserDao {
    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)

    @Query("SELECT * FROM user")
    fun getAll(): List<User>
}

Java

@Dao
public interface UserDao {
    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);

    @Query("SELECT * FROM user")
    List<User> getAll();
}

Existen dos tipos de métodos DAO que definen interacciones de bases de datos:

  • Métodos de conveniencia que te permiten insertar, actualizar y borrar filas en tu base de datos sin escribir ningún código de SQL.
  • Métodos 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 el modo para usar ambos tipos de métodos DAO y así definir las interacciones de la base de datos que necesita tu app.

Métodos de conveniencia

Room proporciona anotaciones de conveniencia para definir métodos que permiten realizar inserciones, actualizaciones y eliminaciones simples sin necesidad de escribir una instrucción de SQL.

Si necesitas definir inserciones, actualizaciones o eliminaciones más complejas, o bien quieres consultar los datos de la base de datos, usa un método de búsqueda.

Inserción

La anotación @Insert te permite definir métodos que insertan sus parámetros en la tabla adecuada en la base de datos. En el siguiente código, se muestran ejemplos de métodos @Insert válidos que insertan uno o más objetos User en la base de datos:

Kotlin

@Dao
interface UserDao {
    @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 UserDao {
    @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);
}

Cada parámetro para un método @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 un método @Insert, Room inserta cada instancia de entidad pasada en la tabla de base de datos correspondiente.

Si el método @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, el método debe mostrar 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. A fin de 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.

Actualización

La anotación @Update te permite definir métodos que actualizan filas específicas en una tabla de base de datos. Al igual que los métodos @Insert, los métodos @Update aceptan instancias de entidades de datos como parámetros. El siguiente código muestra un ejemplo de un método @Update que intenta actualizar uno o más objetos User en la base de datos:

Kotlin

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

Java

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

Room usa la clave primaria para hacer coincidir las instancias de entidades pasadas con las filas de la base de datos. Si no hay una fila con la misma clave primaria, Room no realiza cambios.

De manera opcional, un método @Update puede mostrar un valor int que indica la cantidad de filas que se actualizaron de forma correcta.

Eliminación

La anotación @Delete te permite definir métodos que borran filas específicas de una tabla de base de datos. Al igual que los métodos @Insert, los métodos @Delete aceptan instancias de entidades de datos como parámetros. En el siguiente código, se muestra un ejemplo de un método @Delete que intenta borrar uno o más objetos User de la base de datos:

Kotlin

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

Java

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

Room usa la clave primaria para hacer coincidir las instancias de entidades pasadas con las filas de la base de datos. Si no hay una fila con la misma clave primaria, Room no realiza cambios.

De manera opcional, un método @Delete puede mostrar un valor int que indique la cantidad de filas que se borraron de forma correcta.

Métodos de búsqueda

La anotación @Query te permite escribir instrucciones de SQL y exponerlas como métodos DAO. Usa estos métodos 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.

Búsquedas simples

Con el siguiente código, se define un método que usa una búsqueda SELECT simple para mostrar todos los objetos User de la base de datos:

Kotlin

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

Java

@Query("SELECT * FROM user")
public User[] loadAllUsers();

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

En la mayoría de los casos, solo necesitas mostrar un subconjunto de las columnas de la tabla que consultas. 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 los campos que necesitas.

Room te permite mostrar un objeto simple 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:

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

Luego, puedes mostrar ese objeto simple desde tu método de búsqueda:

Kotlin

@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>

Java

@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();

Room comprende que la búsqueda muestra valores para las columnas first_name y last_name, y que se pueden asignar esos valores a los campos de la clase NameTuple. Si la búsqueda muestra una columna que no se asigna a un campo en el objeto que se muestra, Room muestra una advertencia.

Cómo pasar parámetros simples a una búsqueda

La mayoría de las veces, los métodos DAO deben aceptar parámetros para que puedan realizar operaciones de filtrado. Room admite el uso de parámetros de métodos como parámetros de vinculación en tus búsquedas.

Por ejemplo, el siguiente código define un método que muestra todos los usuarios mayores de cierta edad:

Kotlin

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

Java

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

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:

Kotlin

@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

@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 pasar una colección de parámetros a una búsqueda

Es posible que algunos de tus métodos DAO requieran que pases una cantidad variable de parámetros que no se conocen hasta el tiempo de ejecución. 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.

Por ejemplo, el siguiente código define un método que muestra información sobre todos los usuarios de un subconjunto de regiones:

Kotlin

@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>

Java

@Query("SELECT * FROM user WHERE region IN (:regions)")
public List<User> loadUsersFromRegions(List<String> regions);

Cómo realizar búsquedas 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 un método que une tres tablas para mostrar los libros que se prestaron a un usuario específico:

Kotlin

@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

@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 definir objetos simples para mostrar un subconjunto de columnas de varias tablas unidas, como se describe en Cómo mostrar un subconjunto de columnas de una tabla. El siguiente código define un DAO con un método que muestra los nombres de los usuarios y de los libros que se prestaron:

Kotlin

interface UserBookDao {
    @Query(
        "SELECT user.name AS userName, book.name AS bookName " +
        "FROM user, book " +
        "WHERE user.id = book.user_id"
    )
    fun loadUserAndBookNames(): LiveData<List<UserBook>>

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

Java

@Dao
public interface UserBookDao {
   @Query("SELECT user.name AS userName, book.name AS bookName " +
          "FROM user, book " +
          "WHERE user.id = book.user_id")
   public LiveData<List<UserBook>> loadUserAndBookNames();

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

Cómo mostrar un multimapa

En Room 2.4 y versiones posteriores, también puedes realizar búsquedas en columnas de varias tablas sin definir una clase de datos adicional mediante la escritura de métodos de búsqueda que muestren un multimapa.

Considera el ejemplo de la sección Cómo realizar búsquedas en varias tablas. En lugar de mostrar una lista de instancias de una clase de datos personalizada que contenga pares de instancias User y Book, puedes mostrar una asignación de User y Book directamente desde tu método de búsqueda:

Kotlin

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<User, List<Book>>

Java

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id"
)
public Map<User, List<Book>> loadUserAndBookNames();

Cuando tu método 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 el método loadUserAndBookNames() para que solo se muestren usuarios recurrentes que retiraron tres o más libros:

Kotlin

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id" +
    "GROUP BY user.name WHERE COUNT(book.id) >= 3"
)
fun loadUserAndBookNames(): Map<User, List<Book>>

Java

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id" +
    "GROUP BY user.name WHERE COUNT(book.id) >= 3"
)
public Map<User, List<Book>> loadUserAndBookNames();

Si no necesitas asignar objetos enteros, también puedes mostrar asignaciones entre columnas específicas de tu búsqueda si configuras los atributos keyColumn y valueColumn en una anotación @MapInfo en tu método de búsqueda:

Kotlin

@MapInfo(keyColumn = "userName", valueColumn = "bookName")
@Query(
    "SELECT user.name AS username, book.name AS bookname FROM user" +
    "JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<String, List<String>>

Java

@MapInfo(keyColumn = "userName", valueColumn = "bookName")
@Query(
    "SELECT user.name AS username, book.name AS bookname FROM user" +
    "JOIN book ON user.id = book.user_id"
)
public Map<String, List<String>> loadUserAndBookNames();

Tipos de datos que se muestran especiales

Room proporciona algunos tipos de datos que se muestran especiales 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. En Room 2.3.0-alpha01 y versiones posteriores, los DAOs pueden mostrar objetos PagingSource para usarlos con Paging 3.

Kotlin

@Dao
interface UserDao {
  @Query("SELECT * FROM users WHERE label LIKE :query")
  fun pagingSource(query: String): PagingSource<Int, User>
}

Java

@Dao
interface UserDao {
  @Query("SELECT * FROM users WHERE label LIKE :query")
  PagingSource<Integer, User> pagingSource(String query);
}

Si quieres obtener información para elegir parámetros de tipo para un PagingSource, consulta Cómo seleccionar tipos de clave y de valor.

Cómo acceder directamente al cursor

Si la lógica de tu app requiere acceso directo a las filas que se muestran, puedes escribir tus métodos DAO para mostrar un objeto Cursor como se indica en el siguiente ejemplo:

Kotlin

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

Java

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

Recursos adicionales

Para obtener más información sobre cómo acceder a datos mediante DAO de Room, consulta los siguientes recursos adicionales:

Ejemplos

Codelabs