El desarrollo de una aplicación compleja es imposible sin el uso de bases de datos que proporcionan una potente funcionalidad para almacenar, ordenar y recuperar información. Su aplicación en el desarrollo de Android tiene sus propias especificidades causadas por las características de los dispositivos móviles: menos recursos de hardware, ahorro de batería, arquitectura de la aplicación móvil. Por lo tanto, el uso de bases de datos en el desarrollo de Android es un tema aparte que requiere un estudio minucioso.
AA de 2020, SQLite, Realm y ObjectBox siguen siendo los sistemas de gestión de bases de datos (DBMS) más populares para las aplicaciones Android. Cada uno de ellos ocupa su propio nicho en el mercado de las aplicaciones móviles, ofreciendo a los desarrolladores diversos enfoques para el almacenamiento y el uso de datos estructurados.
SQLite: el líder en popularidad
SQLite se presentó por primera vez en el año 2000 y desde entonces se ha convertido en uno de los SGBD más populares. Se basa en un enfoque relacional del almacenamiento de datos y utiliza el lenguaje de consulta SQL para gestionar la información. A diferencia de otros SGBD conocidos, como MySQL y PostgreSQL, SQLite no requiere un servidor de bases de datos independiente, almacena toda la información en un archivo y ocupa poco espacio en el disco. Estas características lo convierten en el más óptimo entre los SGBD relacionales para su uso en dispositivos móviles. Actualmente, SQLite es el SGBD más utilizado en el desarrollo de aplicaciones Android. Actualmente, SQLite puro se utiliza muy poco en el desarrollo de aplicaciones Android. Para simplificar el trabajo, se utiliza ampliamente el ORM Room de Google, que evita escribir código boilerplate. Las tablas de datos se crean mediante anotaciones en la clase modelo, que determinan la transformación de los atributos de la clase en nombres y propiedades de las columnas de la tabla.
Realm – Alternativa orientada a objetos
Una alternativa a SQLite es Realm. Su primera versión estable para Android apareció en 2014. Los creadores concibieron Realm como una base de datos que evitará escribir el engorroso código boilerplate en SQL, permitirá trabajar con datos como objetos y aumentará la velocidad de las operaciones CRUD. En 2019, Realm fue adquirida por MongoDB Inc. y añadió la opción de plataforma sin servidor a la funcionalidad original. Realm pertenece al grupo de los DBMS noSQL, ya que utiliza clases de modelos para describir la estructura de los datos, no las tablas y las relaciones entre ellas. Esto elimina el problema del mapeo objeto-relacional, relevante para las bases de datos relacionales, y reduce el coste de los recursos para la conversión de datos.
ObjectBox – Líder en rendimiento
ObjectBox – el más nuevo de los SGBD considerados. Fue creado por Green Robot, un conocido desarrollador de Android por sus productos GreenDao, EventBus y Essentials. ObjectBox se desarrolló originalmente como un SGBD para dispositivos móviles y del IoT; por lo tanto, se compara favorablemente con sus competidores en cuanto a la velocidad de funcionamiento y la comodidad de integración en las aplicaciones móviles. Al igual que Realm, implementa un enfoque noSQL en el que los atributos de las clases del modelo y las relaciones entre ellas se escriben directamente en la base de datos.
El uso de la base de datos en su aplicación Android en la mayoría de los casos significa que tiene que tratar con información estructurada e interconectada. El almacenamiento simple de datos primitivos es sencillo. Sin embargo, las manipulaciones más complejas con la información pueden ser un verdadero reto para un desarrollador, que requiere un buen conocimiento de las herramientas de su DBMS elegido. Intentemos considerar cómo resuelven los problemas complejos las bases de datos móviles más populares: SQLite, Realm y ObjectBox:
- almacenando objetos complejos
- recibir datos con muchas condiciones
- obtener entidades relacionadas (uno a uno, uno a muchos, muchos a muchos).
Como ejemplo, tomemos una aplicación para una biblioteca que trabajará con datos sobre libros. Cada libro tiene uno o varios autores, título, lugar y año de publicación, editorial, estado actual (emitido en mano, disponible, para restaurar), etc. Considere qué opciones de organización de los datos pueden utilizarse en el caso de utilizar cada uno de los SGBD anteriores.
Almacenamiento de objetos complejos
SQLite y Room
Una de las características de las bases de datos relacionales es el soporte de tipos de datos estrictamente definidos que pueden asignarse a las columnas de las tablas. En el caso de SQLite, son INTEGER, REAL, TEXT, BLOB y NULL. Para guardar los objetos, es necesario convertirlos en un conjunto de atributos de los tipos correspondientes. Room le permite hacer esto de varias maneras:
- Añadiendo la anotación @Entity a una clase que describe un modelo de datos. En este caso, Room creará una tabla independiente en la base de datos SQLite y guardará sus objetos como filas de esta tabla. Mediante las anotaciones, puede especificar los nombres de las columnas, los campos necesarios para guardar, las propiedades de los datos, etc.
1234567891011121314import androidx.room.ColumnInfoimport androidx.room.Entityimport androidx.room.PrimaryKey@Entity(tableName = "books")data class Book(@PrimaryKey val id: Int,val title: String?,val city: String?,val year: Int?,@ColumnInfo(name = "publishing_house") val publishingHouse: String?) - Utilizando TypeConverters, que describen la lógica para convertir un objeto en un tipo de datos simple, adecuado para su almacenamiento en 1 celda. Un ejemplo clásico es que el tipo Fecha puede guardarse en la base de datos como una marca de tiempo.
12345678910111213class Converters {@TypeConverterfun fromUnix(unixTime: Long?): Date? {return if (unixTime == null) null else Date(unixTime)}@TypeConverterfun toUnix(date: Date?): Long? {return date?.time}}La conversión a valores primitivos puede ser de objetos más complejos, como matrices y listas.
1234567891011121314@TypeConverterpublic static ArrayList fromString(String value) {Type listType = new TypeToken<ArrayList>() {}.getType();return new Gson().fromJson(value, listType);}@TypeConverterpublic static String fromArrayList(ArrayList list) {Gson gson = new Gson();String json = gson.toJson(list);return json;} - Utilizando la anotación
@Embedded
. Al utilizarla, Room creará automáticamente campos adicionales para los atributos del objeto anidado en la tabla padre. La principal limitación de este enfoque es obvia: sólo es adecuado para guardar objetos anidados.
Realm
Dado que Realm se desarrolló originalmente como una base de datos orientada a objetos, la conversión de los modelos de aplicación para guardarlos en la base de datos requiere un esfuerzo mínimo por parte del desarrollador. En primer lugar, la clase del modelo debe ser accesible para la herencia (en Java, no hay que cambiar nada, en Kotlin – añadir el modificador open). Además, esta clase debe ser descendiente de la clase abstracta RealmObject, que encapsula la lógica de interacción con la base de datos.
1 2 3 4 5 6 7 8 9 10 11 12 | import io.realm.RealmObject import io.realm.annotations.PrimaryKey open class Book( @PrimaryKey var id: Int, var title: String?, var city: String?, var year: Int?, var publishingHouse: String? ) : RealmObject() |
Guardar objetos anidados tampoco es un problema. Los requisitos para los objetos anidados son los mismos que para la clase modelo.
1 2 3 4 5 6 7 8 9 10 | open class Author( @PrimaryKey var id: Int, var firstName: String?, var lastName: String?, var dateOfBirth: Date?, var dateOfDeath: Date?, var books: RealmList ) : RealmObject() |
ObjectBox
El principio de funcionamiento de ObjectBox es similar al de Realm, por lo que también requiere una mínima manipulación de las clases modelo para guardarlas en la base de datos: añadir anotaciones @Entity y @Id, atributos públicos o sus getters y setters.
1 2 3 4 5 6 7 8 9 10 | @Entity data class Book( @Id var id: Long = 0, var title: String?, var city: String?, var year: Int?, var publishingHouse: String? ) |
Para guardar enlaces a otros objetos es necesario especificar el tipo de conexión: ToOne, si sólo hay un objeto asociado, o ToMany si hay varios. En nuestro caso, podemos complementar nuestra clase Libro con un enlace a los autores.
1 2 3 | lateinit var authors: ToMany |
Obtención de datos con muchas condiciones
SQLite y Room
Proporcionan una potente funcionalidad para generar consultas complejas basadas en expresiones SQL. Para utilizarla, debe tener un buen conocimiento de SQL, así como de las características de su implementación en SQLite.
Por ejemplo, una consulta tiene el siguiente aspecto, que devuelve todos los libros posteriores a un año determinado, ordenados alfabéticamente.
1 2 3 4 | @Query("SELECT * FROM books WHERE year > :minYear ORDER BY title") fun loadAllBooksNewerThan(minYear: Int): List |
Una consulta que devuelve todos los libros con un título específico publicados entre los años especificados.
1 2 3 4 5 | @Query("SELECT * FROM books WHERE title LIKE :search " + "AND year BETWEEN :dateFrom AND :dateTo") fun findBooksByTitleForPeriod(search: String, dateFrom: Int, dateTo: Int): List |
Una consulta que devuelve una lista de las editoriales registradas en una base de datos para una ciudad determinada.
1 2 3 4 | @Query("SELECT DISTINCT publishing_house from books WHERE city = :city") fun getPublishingHousesForCity(city: String): List |
Ámbito
Este SGBD proporciona un rico conjunto de operadores para obtener y filtrar datos. Utilizando sus combinaciones, puede crear una consulta de cualquier complejidad. Vamos a reescribir los ejemplos de SQLite para Realm.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | fun loadAllBooksNewerThan(minYear: Int): List { val books = realm .where(Book::class.java) .greaterThan("year", minYear) .sort("title") .findAll() return realm.copyFromRealm(books) } fun findBooksByTitleForPeriod(search: String, dateFrom: Int, dateTo: Int): List? { val books = realm .where(Book::class.java) .like("title", search) .and() .between("year", dateFrom, dateTo) .findAll() return realm.copyFromRealm(books) } fun getPublishingHousesForCity(city: String): List { val books = realm .where(Book::class.java) .equalTo("city", city) .distinct("publishingHouse") .findAll() return realm.copyFromRealm(books).map { it.publishingHouse } } |
ObjectBox
En este SGBD se utiliza QueryBuilder para crear consultas, que cuenta con un amplio arsenal de funciones para crear la selección de datos deseada. El principio de creación de consultas es muy similar al de Realm. La principal diferencia es que en Realm la consulta devuelve datos envueltos en la clase RealmResults. Su principal tarea es mantener actualizados los enlaces a los datos y proporcionar la funcionalidad para acceder a ellos. Para obtener los datos en bruto, debemos copiarlos de la base de datos mediante el método copyFromRealm (). ObjectBox devuelve inmediatamente los datos cuando se le solicitan. Su segunda característica es la capacidad de almacenar en caché y reutilizar las solicitudes. Los ejemplos que ya nos resultan familiares al utilizar un ObjectBox tendrán el siguiente aspecto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | fun loadAllBooksNewerThan(minYear: Long): List { return bookBox.query() .greater(Book_.year, minYear) .order(Book_.title) .build() .find() } fun findBooksByTitleForPeriod(search: String, dateFrom: Long, dateTo: Long): List { return bookBox.query() .contains(Book_.title, search) .and() .between(Book_.year, dateFrom, dateTo) .build() .find() } fun getPublishingHousesForCity(city: String): List { return bookBox.query() .equal(Book_.city, city) .build() .property(Book_.publishingHouse) .distinct() .findStrings() .toList() } |
Obtención de entidades relacionadas (uno-a-uno, uno-a-muchos, muchos-a-muchos)
SQLite y Sala
A pesar de que la implementación de relaciones entre entidades es un punto fuerte de las bases de datos relacionales, su implementación en SQLite y Room requiere una cantidad significativa de código y esfuerzos por parte del desarrollador. La razón es la prohibición del uso de referencias a objetos. Como explican los creadores de Room, en la aplicación Android, el acceso a los objetos anidados conlleva un retraso en la carga del hilo principal con todas las consecuencias que ello conlleva: retraso en la renderización de la UI o consumo excesivo de recursos. Por lo tanto, no se admite la forma más obvia de implementar las relaciones entre entidades.
Para implementar relaciones en Room, es necesario crear una clase adicional que será devuelta al solicitar los datos correspondientes.
Un ejemplo de relación uno-a-uno y uno-a-muchos. Vamos a crear una clase Meta que contenga información de servicio sobre cada libro: código y estado. Cada libro sólo puede tener un metadato y cada metadato se refiere a un solo libro.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Entity(tableName = "meta") data class Meta( @PrimaryKey val code: Long, val status: Int ) data class BookWithMeta( @Embedded val book: Book, @Relation( parentColumn = "id", entityColumn = "bookId" ) val meta: Meta ) |
Los libros con metadatos se reciben en forma de varias solicitudes, por lo que requieren la anotación @Transaction
.
1 2 3 4 5 | @Transaction @Query("SELECT * FROM books") fun getBooksWithMeta(): List |
La relación de uno a muchos en Room se implementa de forma idéntica, pero en lugar de una referencia a un único objeto, se indica un enlace a una lista.
Un ejemplo de relación muchos-a-muchos. Vamos a crear una versión de la clase Autor, que ya nos resulta familiar por el ejemplo de Reino.
1 2 3 4 5 6 7 8 9 10 | class Author( @PrimaryKey var authorId: Int, var firstName: String?, var lastName: String?, var dateOfBirth: Date?, var dateOfDeath: Date?, var books: List ) |
Para implementar la relación, es necesario crear tres clases adicionales: un enlace y dos resultados de consulta.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @Entity(primaryKeys = ["bookId", "authorId"]) data class AuthorBookCrossRef( val bookId: Int, val authorId: Int ) data class AuthorWithBooks( @Embedded val author: Author, @Relation( parentColumn = "authorId", entityColumn = "bookId", associateBy = @Junction(AuthorBookCrossRef::class) ) val books: List ) data class BookWithAuthors( @Embedded val book: Book, @Relation( parentColumn = "bookId", entityColumn = "authorId", associateBy = @Junction(AuthorBookCrossRef::class) ) val authors: List ) |
Las solicitudes de datos serán las siguientes.
1 2 3 4 5 6 7 8 9 | @Transaction @Query("SELECT * FROM books") fun getBooksWithAuthors(): List @Transaction @Query("SELECT * FROM authors") fun getAuthorsWithBooks(): List |
Ámbito
La diferencia en la obtención de datos relacionados entre Sala y Ámbito es enorme. La obtención ocurre de la misma manera que con un simple acceso a la propiedad del objeto. Un punto importante: cuando se implementa una comunicación de muchos a muchos, la conexión entre objetos es unidireccional por defecto. Para una dependencia bidireccional entre objetos, debe utilizar la anotación @LinkingObjects
.
1 2 3 4 5 6 7 | val book = realm .where(Book::class.java) .equalTo("id", bookId) .findFirst() val authors = book?.authors |
ObjectBox
La obtención de entidades relacionadas es muy similar a la de Realm y ocurre en una sola línea.
Ejemplo de uno a uno
1 2 3 | val meta = bookBox[bookId].meta.target |
Ejemplo de muchos a muchos
1 2 3 | val authors = bookBox[bookId].authors |
Conclusión
Criterio | |||
---|---|---|---|
Tipo | Relacional con ORM | Orientado a objetos | Orientado a objetos |
Umbral de entrada | Bajo | Medio | Bajo |
Almacenamiento de objetos complejos | Es conveniente cuando se trabaja con tipos simples, pero requiere un tiempo adicional para implementar las relaciones entre los objetos. | El almacenamiento de objetos complejos requiere un esfuerzo mínimo. | Almacenar objetos complejos requiere un esfuerzo mínimo. |
Obtención de datos con muchas condiciones | Un gran conjunto de herramientas para la formación de consultas complejas de varios niveles. Es necesario conocer SQL. | Un gran conjunto de funciones incorporadas para seleccionar y ordenar los datos. | Un gran conjunto de funciones incorporadas para seleccionar y ordenar datos. |
Recuperación de entidades relacionadas | Requiere una importante inversión de tiempo de implementación | Se produce en unas pocas líneas. | Ocurre en una línea. |
Tipo | Relacional con ORM |
Nivel de entrada | Bajo |
Almacenamiento de objetos complejos | Es conveniente cuando se trabaja con tipos simples, pero requiere tiempo adicional para implementar las relaciones entre los objetos. |
Obtención de datos con muchas condiciones | Gran herramienta para la formación de consultas complejas de varios niveles.Es necesario conocer SQL. |
Recuperación de entidades relacionadas | Requiere una importante inversión de tiempo de implementación |
Tipo | Orientado a objetos |
Nivel de entrada | Medio |
Almacenamiento de objetos complejos | El almacenamiento de objetos complejos requiere un esfuerzo mínimo. |
Obtención de datos con muchas condiciones | Un amplio conjunto de funciones incorporadas para seleccionar y ordenar los datos. |
Recuperación de entidades relacionadas | Se realiza en unas pocas líneas. |
Tipo | Orientado a objetos |
Umbral de entrada | Bajo |
Almacenamiento de objetos complejos | El almacenamiento de objetos complejos requiere un esfuerzo mínimo. |
Obtención de datos con muchas condiciones | Un amplio conjunto de funciones incorporadas para seleccionar y ordenar los datos. |
Recuperación de entidades relacionadas | Se realiza en una sola línea. |
Así pues, las complejas operaciones de almacenamiento y recuperación de datos en las bases de datos más populares para aplicaciones Android tienen sus propias especificidades. La combinación de SQLite y Room, a pesar de ser la solución más popular para el almacenamiento de datos estructurados, requiere importantes manipulaciones para almacenar y recuperar objetos relacionados, así como un buen conocimiento de los fundamentos de SQL para trabajar con eficacia. Realm y ObjectBox, basados en un enfoque orientado a objetos para la organización de datos, son significativamente superiores a sus competidores tanto en velocidad como en usabilidad.