La información es tal vez uno de los recursos más importantes para los usuarios de una app. La misma puede ser sensible y exponer datos privados de los usuarios. Es por ello, que es importante una adecuada gestión de la misma para garantizar su integridad, privacidad y almacenamiento adecuado.
Cuando una aplicación gestiona datos repetitivos o estructurados lo recomendado es grabar los mismos en una base de datos local en el dispositivo. En estas condiciones, el usuario dispone como alternativa de acceder al contenido de manera local, si el dispositivo esta imposibilitado para conectarse a la red o bien esta desconectado.
Para almacenar información de modo persistente en Android, la opción disponible es utilizar SQLite, un sistema de gestión de bases de datos relacionales cuya principal ventaja es que no funciona como un proceso independiente, sino que forma parte de la aplicación que lo utiliza. Por tanto, no necesita ser instalado independientemente, ejecutado o detenido, ni tiene archivo de configuración. Si bien SQLite es potente, se la considera una API de bajo nivel. Su uso requiere de tiempo y esfuerzo debido a lo siguiente:
Disponer realmente de una buena implementación de base de datos usando SQLite implica tener que afrontar los inconvenientes arriba mencionados. Alternativamente, podríamos utilizar un ORM (Object Relational Mapping), pero aún deberíamos definir y crear manualmente la base de datos extendiendo desde SQLiteOpenHelper y crear las clases de contrato.
Entonces, el dilema podría ser: ¿Es Room nuestra solución para simplificar el proceso?
La recomendación general para soslayar las desventajas arriba descriptas, es la utilización de bibliotecas de persistencia como una capa de abstracción para el acceso de datos entre nuestras apps y la base de datos SQLite.
En el Google I/O 2017, el equipo Android presentó Room, una biblioteca de persistencia, que nos permite crear y gestionar bases de datos SQLite más fácilmente. Desde entonces, Room evolucionó para transformarse en una de las herramientas más potentes y versátiles del mercado para gestionar información en las apps Android, simplificando el modo de trabajo y garantizando a la vez seguridad e integridad en los datos.
En esencia, Room tiene las siguientes características que permiten optimizar las desventajas de SQLite:
Para usar Room en una app, se deben agregar las siguientes dependencias al archivo build.gradle de la misma:
Arquitectura: Componentes de Arquitectura recomendados. Componentes de Room. Desarrollo de los mismos
No es posible disponer de un único modo de implementación de apps que funcione bien para cada escenario posible.
Las arquitecturas recomendadas por el Android Team son un punto de partida para la mayoría de las situaciones.
Sin embargo, si su experiencia practica en la implementación de apps le ha llevado a desarrollar una arquitectura diferente, pero que aplica los Principios de Arquitectura Comunes (Separation of Concerns (SoC) [1], Drive UI from a Model, etc) no tiene porque modificarlos si le ha dado buenos resultados.
Dicho lo anterior, la arquitectura de una app puede estructurarse utilizando Architecture Components [2] de la siguiente manera:
“Observa que cada componente solo depende del componente que está un nivel más abajo. Por ejemplo, las actividades y los fragmentos solo dependen de un modelo de vista. El repositorio es la única clase que depende de otras clases. En este ejemplo, el repositorio depende de un modelo de datos persistente y de una fuente de datos de backend remota.”
– Documentación de Android
En este artículo nos enfocamos en la arquitectura del Modelo de Datos Persistente (Room)
Desde Android siempre se alienta a estructurar una aplicación respetando mínimamente los Componentes de Arquitectura aquí descriptos. Luego, en función de la menor o mayor complejidad de una app se la podrá conformar a través de la implementación de diversos Patrones de Arquitectura como MVP (Model View Presenter), MVVM (ModelView View Model), Clean Architecture, etc.
[1] SoC – Es un principio de diseño que tiene por objetivo dividir, separar una aplicación en diferentes secciones de manera tal que cada sección aborde un aspecto, tema o asunto (concern) diferente (separado). Un concern es un conjunto de información que afecta el código de una app y puede ser tan general como “los detalles de hardware para una aplicación”, o bien tan especifico como; “el nombre de la clase que se debe instanciar”
[2]: Architecture Components: Subconjunto de bibliotecas incluidas en Android Jetpack [3] que se destacan aparte para el desarrollo de apps que incluyan:
Arquitectura de Room / Architecture Components
Se presenta una síntesis sobre Architecture Components [2] poniendo foco en un subconjunto de los componentes: LiveData, ViewModel y Repository en donde el siguiente diagrama muestra la forma básica de esta arquitectura,
ViewModel: Actúa como un centro de comunicaciones entre el Repository (datos) y la UI. La UI ya no necesita preocuparse por el origen de los datos. Las instancias del ViewModel sobreviven a la recreación del Activity/Fragment
Repository: Se trata de una clase intermedia cuya función es gestionar múltiples fuente de datos.
LiveData: Una clase data holder que puede ser Observada. Siempre contiene/mantiene en cache la última versión de los datos y notifica a sus Observadores cuando los datos han sido modificados. Por ello, LiveData es lifecycle aware.
Room es parte del Android Architecture Components, lo cual significa que trabaja bien con ViewModel, LiveData y Paging, pero no depende de estos módulos. Desde Google I/O 2018 también forma parte del Android Jetpack [3].
A través del uso de anotaciones, podemos definir nuestra bases de datos, tablas y operaciones.
Room automáticamente traducirá estas anotaciones en queries SQLite para ejecutar las operaciones en el motor de base de datos. Room está constituido por 3 componente principales:
SQLite database: Base de datos en el dispositivo. Room crea y mantiene esta base de datos por nosotros.
[3]: Android Jetpack: Conjunto de bibliotecas recomendadas para desarrollar apps que incorporan las buenas prácticas sugeridas por Android e incluyen compatibilidad para atrás para versiones previas de Android.
3.Anotaciones.
Room trabaja con una arquitectura cuyas clases se especifican con anotaciones predefinidas. Las principales son las de siguientes:
Adictos al Trabajo tiene una explicación más detallada del propósito de cada uno en este tutorial. Adicionalmente, podemos consultarle a la documentación de Android para más detalles.
Entidad
Una Entidad es simplemente una clase Kotlin o Java anotada con @Entity que modeliza a una tabla de la base de datos, donde sus variables de instancia establecen una correspondencia (mapeo) con cada una de las columnas de la tabla de la base de datos y el nombre de la clase mapea o se corresponde con el nombre de la tabla. Uno o más atributos pueden conformar la clave primaria (PK)
que posibilita la unicidad de las instancias/registros de la tabla.
Para poder persistir un atributo, Room debe tener acceso al mismo, por lo cual debemos asegurarnos que Room tenga acceso
a los mismos ya sea declarándolos como públicos o bien proporcionando métodos setters/getters.
¿Qué es un DAO? – Su Estructura
Utilizamos Room para acceder e interactuar con los datos persistidos en una base de datos desde nuestra app.
La manera más conveniente de hacerlo es definiendo DAOs (Data Access Object) porque constituyen una forma modular de acceder a una base de datos en comparación con los query builders o la escritura directa de queries.
Podemos definir un DAO como una interfaz o como clase abstracta lo cual dependerá de la complejidad de la app que se está desarrollando. Para las apps más simples, en general podemos usar una interfaz.
Cualquiera sea el caso debemos anotar los DAOs con la anotación @DAO. En los DAOs no se definen atributos, solo se definen uno o más métodos para acceder a la base los datos de nuestra app.
¿Qué nos provee un DAO?
Hay dos tipos de métodos que se definen en un DAO para interactuar con la base de datos:
Un ejemplo de DAO para la entidad Empleado podría ser:
( 1 ) – @insert: Permite definir un método para guardar datos en una tabla específica.
El ejemplo muestra cómo insertar uno o más objetos de tipo Empleado. Si un método anotado @Insert recibe un solo parámetro, puede retornar un valor long, el cual es el nuevo rowId [4] para el ítem insertado. Si el parámetro es un array o una colección, luego el método debe retornar un array o una colección de valores long.
( 2 ) – @Update: Permite definir métodos para actualizar registros específicos de una tabla de la base de datos.
El ejemplo del método @Update actualiza uno o más objetos de tipo Empleado en la base de datos.
( 3 ) – @Delete: Permite definir métodos para borrar registros de una tabla de la base de datos.
El ejemplo muestra un método @Delete que borrar uno o más objetos de tipo Empleado de la tabla.
Cada parámetro de un método anotado con @Insert, @Update, @Delete debe ser una instancia de una clase de datos anotada con @Entity (Entidad Room) o una colección de las mismas. Los métodos anotados con @Update, @Delete utilizan la PK para establecer coincidencia con las instancias de las entidades pasadas como parámetros con los registros de la base de datos.
Si no existe un registro con la misma PK, Room no realiza ningún cambio. Además, pueden opcionalmente retornar un valor entero que indica el número de filas que fueron actualizadas o borradas exitosamente.
( 4 ) – @Query: Nos permite escribir sentencias SQL y exponerlas como métodos DAO.
El ejemplo define un método que usa un query simple SELECT para retornar todos los objetos Empleado de la base.
( 5 ) – @Query: Pasaje de parámetros simples a un query
Es común que surja la necesidad en los métodos DAO de aceptar parámetros para ejecutar operaciones de filtrado.
Room soporta bind parameters en los queries. El ejemplo define un método que retorna todos los empleados cuyas edades estén por encima de cierta edad.
Relaciones entre Entidades (1:1, 1:M y M:M)
Como SQLite es una base de datos relacional, podemos definir relaciones entre las entidades. Con Room, Google decidió que “Las Entidades no pueden contener Entidades”. Este enfoque intenta eliminar la posibilidad de ejecutar operaciones de base de datos en el main thread.
Trabajar con las relaciones en Room es quizás el tema más complejo de esta tecnología y por razones de espacio, se desarrollará una breve introducción.
Los ORM comunes permiten usar referencias de objetos y, por lo tanto, los ORM implementan una carga diferida (lazy) que se considera potencialmente peligrosa en las aplicaciones Android.
La razón de esta decisión se debe a lo siguiente: La mayoría de las bibliotecas ORM posibilitan el mapeo/correspondencia de relaciones desde una base de datos al respectivo modelo de objetos. Esto es una práctica común y funciona bien server-side, dado que la carga de los campos que se realizan bajo demanda o diferida (lazily) tiene mejor rendimiento. Dado que es altamente probable que todas estas tareas tengan lugar en UI thread, para no violar la regla que establece que no debe bloquearse el mismo (y por lo tanto afectar el rendimiento), no se recomienda ejecutar aquellas en el lado cliente.
Aunque no puede usar relaciones directas, Room aún nos permite definir restricciones de clave externa (foreign key) entre entidades. En Room existen 3 enfoques que pueden ser utilizados para definir relaciones entre entidades.
No hay que considerar estos 3 enfoques como alternativas excluyentes. Es decir, en Room las relaciones 1:1, 1:M y M:M entre entidades, NO se establecen haciendo uso únicamente del enfoque de la opción 1 o solo la opción 2 u opción 3 [ver anotaciones @Relation/@Embedded ], sino por una combinación de las opciones 2 y 3.
[4] rowId – Para mayor información acerca de los valores rowId, ver la documentación de referencia para la anotación @Insert, así como el siguiente link: https://www.sqlite.org/rowidtable.html)
Anotación @Embedded: El uso aislado de la anotación @Embedded, tiene por objetivo anidar clases POJOs a una entidad Room, cuando Room, no puede mapear tipos de datos complejos definidos por el usuario (por ejemplo una composición entre los objetos Empleado y Dirección) a SQLite.
La anotación @ForeignKey: Se utiliza cuando todos los objetos son almacenados en tablas separadas o cuando se necesita un comportamiento en cascada; que es precisamente para lo que se utiliza una foreign key. Podríamos decir, que esta anotación nos permite “crear parte de la relación” entre las entidades a persistir, porque para poder insertar, actualizar, consultar dichas entidades (a través de query methods) vamos a tener que usar también las anotaciones @Embedded/@Relation para crear “la otra parte” de la relación entre las entidades que se referencian en lo queries methods.
Anotaciones @Relation/@Embedded: La documentación oficial de Android expone esta opción como la alternativa para establecer relaciones entre Entidades. Por lo cual, representa la única opción de los 3 enfoques mencionados que se puede utilizar para mapear relaciones entre entidades y ejecutar operaciones CRUD sin combinarla con ninguna otra opción (por ej la opción 2); si bien al utilizarla de esta manera NO se dispondría del comportamiento que ofrece la anotación @ForeignKey. Es por esta última razón, que más arriba se mencionó que el uso de la opción 3, no resuelve completamente las relaciones entre entidades
Creación de la relación 1:M
Para poder vincular a la entidad Student y Vehicle debemos definir una Foreign Key (FK) entre las 2 tablas, para ello necesitamos especificar 3 aspectos:
Consultando una Relación 1:M
Vemos cómo generar la estructura necesaria para poder recuperar a un estudiante determinado y sus vehículos asociados. Vamos a implementar un query que retorne la siguiente clase de respuesta.
A los efectos de retornar esta entidad desde un query, necesitamos proporcionar dos anotaciones
El código resultante es el siguiente:
Luego, dentro de nuestro DAO escribimos el query para poder recuperar cada estudiantes con sus vehículos.
Observar las dos anotaciones que se utilizan aquí:
Conclusiones