Advertisement
  1. Code
  2. Android SDK

Componentes de la arquitectura de Android: Room Persistence Library (la biblioteca de persistencia Room)

by
Read Time:16 minsLanguages:
This post is part of a series called Android Architecture Components.
Android Architecture Components: LiveData

Spanish (Español) translation by Valeria Angulo (you can also view the original English article)

En este último artículo de la serie de Componentes de Arquitectura de Android, exploraremos la librería de persistencia Room, un excelente recurso nuevo que facilita mucho el trabajo con bases de datos en Android. Esta proporciona una capa de abstracción sobre SQLite, consultas SQL comprobadas en tiempo de compilación, y también consultas asíncronas y observables. Room lleva las operaciones con bases de datos en Android a otro nivel.

Como esta es la cuarta parte de la serie, asumiré que estás familiarizado con los conceptos y componentes del paquete Architecture, como LiveData y LiveModel. Sin embargo, si no has leído ninguno de los tres últimos artículos, podrás seguir. Aun así, si no sabes mucho sobre esos componentes, tómate un tiempo para leer la serie; puedes disfrutarla.

1. El componente Room

Como se ha mencionado, Room no es un nuevo sistema de base de datos. Es una capa abstracta que envuelve la base de datos SQLite estándar adoptada por Android. Sin embargo, Room añade tantas características a SQLite que es casi imposible de reconocer. Room simplifica todas las operaciones relacionadas con la base de datos y también las hace mucho más potentes, ya que permite la posibilidad de devolver observables y consultas SQL comprobadas en tiempo de compilación.

Room se compone de tres componentes principales: la Base de datos, el DAO (Data Access Objects) y la Entidad. Cada componente tiene su responsabilidad, y todos ellos deben ser implementados para que el sistema funcione. Afortunadamente, dicha implementación es bastante sencilla. Gracias a las anotaciones proporcionadas y a las clases abstractas, el boilerplate (plantilla para implementar) para implementar Room se mantiene al mínimo.

  • La entidad es la clase que se guarda en la base de datos. Se crea una tabla de base de datos exclusiva para cada clase anotada con @Entity.
  • El DAO es la interfaz anotada con @Dao que media el acceso a los objetos de la base de datos y sus tablas. Hay cuatro anotaciones específicas para las operaciones básicas del DAO: @Insert, @Update, @Delete y @Query.
  • El componente de Base de Datos es una clase abstracta anotada con @Database, que extiende RoomDatabase. La clase define la lista de Entidades y sus DAOs.

2. Configurar el entorno

Para utilizar Room, añade las siguientes dependencias al módulo de la aplicación en Gradle:

Si estás utilizando Kotlin, debes aplicar el complemento kapt y agregar otra dependencia.

3. Entity, la tabla de la base de datos

Una Entity (Entidad) representa el objeto que se guarda en la base de datos. Cada clase Entity crea una nueva tabla de base de datos, con cada campo representando una columna. Las anotaciones se utilizan para configurar las entidades, y su proceso de creación es realmente sencillo. Fíjate en lo sencillo que es configurar una Entity utilizando las clases de datos de Kotlin.

Una vez que se anota una clase con @Entity, la biblioteca Room creará automáticamente una tabla utilizando los campos de la clase como columnas. Si necesitas ignorar un campo, simplemente anótalo con @Ignore. Cada Entity también debe definir una @PrimaryKey.

Tabla y columnas

Room utilizará la clase y los nombres de sus campos para crear automáticamente una tabla; sin embargo, tú puedes personalizar la tabla que se genera. Para definir un nombre para la tabla, utiliza la opción tableName en la anotación @Entity, y para editar el nombre de las columnas, añade una anotación @ColumnInfo con la opción name (nombre) en el campo. Es importante recordar que los nombres de la tabla y de las columnas distinguen entre mayúsculas y minúsculas.

Índices y Restricciones de Unicidad

Existen algunas restricciones SQLite útiles que Room nos permite implementar fácilmente en nuestras entidades. Para acelerar las consultas de búsqueda, se pueden crear índices SQLite en los campos que sean más relevantes para dichas consultas. Los índices harán que las consultas de búsqueda sean más rápidas; sin embargo, también harán que las consultas de inserción, borrado y actualización sean más lentas, por lo que debes usarlos con cuidado. Echa un vistazo a la documentación de SQLite para entenderlos mejor.

Hay dos formas diferentes de crear índices en Room. Puedes simplemente establecer la propiedad ColumnInfo, index, a true, dejando que Room establezca los índices por ti.

O, si necesitas más control, utiliza la propiedad indices de la anotación @Entity, listando los nombres de los campos que deben componer el índice en la propiedad value. Observa que el orden de los elementos en value es importante, ya que define la ordenación de la tabla de índices.

Otra restricción útil de SQLite es unique, que prohíbe que el campo marcado tenga valores duplicados. Desafortunadamente, en la versión 1.0.0, Room no proporciona esta propiedad como debería, directamente en el campo entity (entidad). Pero puedes crear un índice y hacerlo único, consiguiendo un resultado similar.

Otras restricciones como NOT NULL, DEFAULT y CHECK no están presentes en Room (al menos hasta ahora, en la versión 1.0.0), pero puedes crear tu propia lógica en la Entity (Entidad) para conseguir resultados similares. Para evitar los valores nulos en las entidades de Kotlin, basta con eliminar el ? al final del tipo de variable o, en Java, añadir la anotación @NonNull.

Relación entre objetos

A diferencia de la mayoría de las bibliotecas de mapeo objeto-relacional, Room no permite que una entidad haga referencia directa a otra. Esto significa que si tienes una entidad llamada NotePad (Bloc de notas) y otra llamada Note (Nota), no puedes crear una Collection (Colección) de Notes dentro de NotePad como harías con muchas bibliotecas similares. Al principio, esta limitación puede parecer molesta, pero fue una decisión de diseño para ajustar la biblioteca Room a las limitaciones de la arquitectura de Android. Para entender mejor esta decisión, echa un vistazo a la explicación de Android sobre su enfoque.

Aunque la relación de objetos de Room es limitada, sigue existiendo. Utilizando claves foráneas, es posible referenciar objetos padre e hijo y realizar modificaciones en cascada. Ten en cuenta que también se recomienda crear un índice en el objeto hijo para evitar escaneos completos de la tabla cuando se modifica el objeto padre.

Incrustación de objetos

Es posible incrustar objetos dentro de las entidades utilizando la anotación @Embedded.  Una vez incrustado un objeto, todos sus campos se añadirán como columnas en la tabla de la entidad, utilizando los nombres de los campos del objeto incrustado como nombres de columna. Considera el siguiente código.

En el código anterior, la clase Location está incrustada en la entidad Note. La tabla de la entidad tendrá dos columnas extra, correspondientes a los campos del objeto incrustado. Como estamos utilizando la propiedad prefijo en la anotación @Embedded, los nombres de las columnas serán 'note_location_lat' y 'note_location_lon', y será posible referenciar esas columnas en las consultas.

4. Objeto de acceso a los datos

Para acceder a las bases de datos de la Room, es necesario un objeto DAO. El DAO puede definirse como una interfaz o una clase abstracta. Para implementarlo, anota la clase o la interfaz con @Dao y ya puedes acceder a los datos.  Aunque es posible acceder a más de una tabla desde un DAO, se recomienda, en nombre de una buena arquitectura, mantener el principio de Separación de Intereses y crear un DAO responsable de acceder a cada entidad.

Insertar, Actualizar y Eliminar

Room proporciona una serie de anotaciones convenientes para las operaciones CRUD en el DAO: @Insert, @Update, @Delete y @Query. La operación @Insert puede recibir como parámetros una sola entidad, una matriz o una Lista de entidades.  Para entidades individuales, puedes devolver un long, que representa la fila de la inserción. Para múltiples entidades como parámetros, puede devolver un long[] o una List<Long> en su lugar.

Como puedes ver, hay otra propiedad de la cual se puede hablar: onConflict. Esta define la estrategia a seguir en caso de conflictos utilizando las constantes OnConflictStrategy. Las opciones son bastante autoexplicativas, siendo ABORT, FAIL y REPLACE las posibilidades más significativas.

Para actualizar las entidades, utiliza la anotación @Update. Sigue el mismo principio que @Insert, recibiendo entidades individuales o múltiples entidades como argumentos. Room utilizará la entidad receptora para actualizar sus valores, utilizando la PrimaryKey de la entidad como referencia. Sin embargo, el @Update solo puede devolver un int que representa el total de filas de la tabla actualizadas.

Nuevamente, siguiendo el mismo principio, la anotación @Delete puedes recibir una o varias entidades y devolver un int con el total de filas de la tabla actualizadas. También utiliza la PrimaryKey de la entidad para encontrar y eliminar el registro en la tabla de la base de datos.

Realización de consultas

Por último, la anotación @Query realiza consultas en la base de datos. Las consultas se construyen de forma similar a las consultas SQLite, siendo la mayor diferencia la posibilidad de recibir argumentos directamente desde los métodos. Pero la característica más importante es que las consultas se verifican en tiempo de compilación, lo que significa que el compilador encontrará un error tan pronto como se construya el proyecto.

Para crear una consulta, anota un método con @Query y escribe una consulta SQLite como valor. No prestaremos demasiada atención a cómo escribir consultas, ya que utilizan el estándar de SQLite. Pero en general, usarás las consultas para recuperar datos de la base de datos usando el comando SELECT. Las selecciones pueden devolver valores individuales o de colección.

Es realmente sencillo pasar parámetros a las consultas. Room deducirá el nombre del parámetro, utilizando el nombre del argumento del método. Para acceder a él, utiliza :, seguido del nombre.

Consultas LiveData

Room ha sido diseñado para trabajar con LiveData. Para que una @Query devuelva un LiveData, basta con envolver el retorno estándar con LiveData<?> y ya está.

Después de esto, será posible observar el resultado de la consulta y obtener resultados asíncronos con bastante facilidad. Si no conoces la potencia de LiveData, tómate un tiempo para leer nuestro tutorial sobre el componente.

5. Creación de la base de datos

La base de datos se crea mediante una clase abstracta, anotada con @Database y que extiende la clase RoomDatabase. Además, las entidades que serán gestionadas por la base de datos deben ser pasadas en un array (matríz) en la propiedad entities de la anotación @Database.

Una vez implementada la clase de base de datos, es el momento de construirla. Es importante destacar que la instancia de la base de datos debería construirse idealmente solo una vez por sesión, y la mejor manera de conseguirlo sería utilizar un sistema de inyección de dependencias, como Dagger. Sin embargo, no nos sumergiremos en DI ahora, ya que está fuera del alcance de este tutorial.

Normalmente, las operaciones sobre una base de datos de Room no pueden realizarse desde el UI Thread, ya que son bloqueantes y probablemente crearán problemas en el sistema. Sin embargo, si quieres forzar la ejecución en el UI Thread, añade allowMainThreadQueries a las opciones de construcción. De hecho, hay muchas opciones interesantes sobre cómo construir la base de datos, y te aconsejo que leas la documentación de RoomDatabase.Builder para entender las posibilidades.

6. Tipo de datos y conversión de datos

El tipo de datos de una columna es definido automáticamente por Room. El sistema inferirá a partir del tipo de campo qué tipo de Datatype de SQLite es más adecuado. Ten en cuenta que la mayoría de los POJO de Java se convertirán de forma inmediata; sin embargo, es necesario crear convertidores de datos para manejar objetos más complejos que Room no reconoce automáticamente, como Datey Enum.

Para que Room entienda las conversiones de datos, es necesario proporcionar TypeConverters y registrar esos conversores en Room. Es posible hacer este registro teniendo en cuenta el contexto específico; por ejemplo, si se registra el TypeConverter en la Database (Base de Datos), todas las entidades de la base de datos utilizarán el convertidor. Si se registra en una entidad, solo las propiedades de esa entidad podrán utilizarlo, y así sucesivamente.

Para convertir un objeto Date directamente en un Long durante las operaciones de guardado de Room y luego convertir un Long en un Date al consultar la base de datos, primero declara un TypeConverter.

Luego, registra el TypeConverter en la Database (Base de Datos), o en un contexto más específico si lo deseas.

7. Utilizando Room en una aplicación

La aplicación que hemos desarrollado durante esta serie utilizaba SharedPreferences para almacenar en caché los datos meteorológicos. Ahora que sabemos cómo usar Room, lo usaremos para crear una caché más sofisticada que nos permitirá obtener datos en caché por ciudad, y también considerar la fecha del tiempo durante la recuperación de los datos.

Primero, vamos a crear nuestra entidad. Guardaremos todos nuestros datos utilizando únicamente la clase WeatherMain. Solo tenemos que añadir algunas anotaciones a la clase, y ya está.

También necesitamos un DAO. El WeatherDAO gestionará las operaciones CRUD en nuestra entidad. Observa que todas las consultas devuelven LiveData.

Finalmente, es el momento de crear la Database .

Bien, ya tenemos configurada nuestra base de datos Room. Todo lo que queda por hacer es conectarla con Dagger y empezar a usarla. En el DataModule, vamos a proporcionar la Database y el WeatherDAO.

Como debes recordar, tenemos un repositorio encargado de manejar todas las operaciones de datos. Vamos a seguir utilizando esta clase para la solicitud de datos de la Room de la aplicación. Pero primero, necesitamos editar el método providesMainRepository del DataModule, para incluir el WeatherDAO durante la construcción de la clase.

La mayoría de los métodos que añadiremos al MainRepositoryson bastante sencillos. Sin embargo, vale la pena mirar más de cerca a clearOldData(). Esto borra todos los datos más antiguos de un día, manteniendo solo los datos meteorológicos relevantes guardados en la base de datos.

El MainViewModel es el encargado de realizar las consultas a nuestro repositorio. Vamos a añadir algo de lógica para dirigir nuestras operaciones a la base de datos de Room. Primero, añadimos un MutableLiveData, el weatherDB, que se encarga de consultar el MainRepository. Después, eliminamos las referencias a SharedPreferences, haciendo que nuestra caché dependa únicamente de la base de datos de Room.

Para que nuestra caché sea relevante, borraremos los datos antiguos cada vez que se realice una nueva consulta meteorológica.

Por último, guardaremos los datos en la base de datos de Room cada vez que se reciba información meteorológica nueva.

Puedes ver el código completo en el repo de GitHub de este post.

Conclusión

Finalmente, llegamos a la conclusión de la serie de Componentes de la Arquitectura Android. Estas herramientas serán excelentes compañeros en tu viaje de desarrollo de Android. Te aconsejo que sigas explorando los componentes. Intenta tomarte un tiempo para leer la documentación.

¡Y echa un vistazo a algunos de nuestros otros posts sobre el desarrollo de aplicaciones Android aquí en Envato Tuts+!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.