Desde que Android publicó las nuevas librerías que forman parte de los «Architecture Components» hemos tenido ganas de poder ponerlas a prueba. Entre estas librerías nos podemos encontrar la de Room Persistence Library. Si has trabajado ya con bases de datos en Android, y sobre todo, si has trabajado con SQLite, sabrás lo tedioso que es generar una base de datos de tamaño intermedio. Kilos y kilos de código «boilerplate» para generar las tablas, relaciones, etc. Aquí es donde entra nuestra nueva librería para facilitarnos este trabajo entre otras muchas mejoras.

Room – Diagrama
¿Qué es ROOM?
Room, es una abstracción de SQLite que permite trabajar con ella de manera rápida y fluída. Está compuesta por 3 elementos básico que son los siguientes:
- Database – Contiene la referencia a la base de datos, es nuestro punto de acceso con los datos que se quieran persistir o ya estén en persistencia.
- Entity – Este componente representa una tabla en la base de datos.
- DAO – Este componente contiene los métodos de acceso a la base de datos (insert, update, delete, query)
¿Cómo puedo usarlo?
Como primer y único requisito para poder empezar a usar la librería es importarla en nuestro build.gradle (Module: app).
dependencies {
...
compile "android.arch.persistence.room:runtime:1.1.0-alpha1"
annotationProcessor "android.arch.persistence.room:compiler:1.1.0-alpha1"
kapt "android.arch.persistence.room:compiler:1.1.0-alpha1"
...
}
Hemos creado una pequeña aplicación de ejemplo donde nos vamos a encontrar con lo siguiente:
- Entidades: Bill, Customer y Provider
- DAOs: BillDAO, CustomerDAO y ProviderDAO.
- AppDatabase: será la encargada de manejar la base de datos (versiones, política de migración, etc) así como de facilitarnos los DAOs que queramos.
Por partes, por favor
Como hemos comentado antes, lo principal sería identificar nuestras tablas. En este caso facturas (Bill.kt), clientes (Customer.kt) y proveedores (Provider.kt). Estas 3 clases están anotadas con su @Entity para que ROOM sepa que se corresponden con tablas que internamente deberá crear.
Como todo en esta vida se explica mejor con ejemplos, aquí os pongo uno sobre como definir la estructura de una tabla en Android «the old way«.
public final class CustomerContract {
// To prevent someone from accidentally instantiating the contract class,
// make the constructor private.
private CustomerContract() {}
/* Inner class that defines the table contents */
public static class CustomerEntry implements BaseColumns {
public static final String TABLE_NAME = "provider";
public static final String COLUMN_NAME_FIRST_NAME = "first_name";
public static final String COLUMN_NAME_LAST_NAME = "last_name";
}
}
private static final String TEXT_TYPE = " TEXT";
private static final String COMMA_SEP = ",";
private static final String SQL_CREATE_CUSTOMERS =
"CREATE TABLE " + CustomerEntry.TABLE_NAME + " (" +
CustomerEntry._ID + " INTEGER PRIMARY KEY," +
CustomerEntry.COLUMN_NAME_FIRST_NAME + TEXT_TYPE + COMMA_SEP +
CustomerEntry.COLUMN_NAME_LAST_NAME + TEXT_TYPE + " )";
private static final String SQL_DELETE_CUSTOMERS =
"DROP TABLE IF EXISTS " + CustomerEntry.TABLE_NAME;
Si quisiéramos hacer esto mismo con Room el resultado sería el siguiente:
@Entity
class Customer constructor(uid: Int, firstName: String, lastName: String) {
@PrimaryKey
var uid: Int = uid
@ColumnInfo(name = "first_name")
var firstName: String? = firstName
@ColumnInfo(name = "last_name")
var lastName: String? = lastName
}
Como podéis observar, Room convierte todas las clases anotadas con @Entity a tablas, sin necesidad de establecer sentencias de creación y borrado de tablas y sobre todo sin la necesidad de tener que recorrer Cursores para obtener los resultados de las consultas, ya que nos devolverá un objeto o listado de Customer directamente resuelto. ¿Cómodo verdad?.
La única entidad que puede ser un poco fuera de lo normal es Bill, ya que existe una relación entre cliente y factura. Como el nuestro es un ejemplo sencillo, solo mostraremos a que cliente pertenece la factura que estemos viendo. Para ello, nuestra entidad Bill definirá de el siguiente modo la «foreign key».
@Entity(foreignKeys =
arrayOf(
ForeignKey(
entity = Customer::class,
parentColumns = arrayOf("uid"),
childColumns = arrayOf("customer_id"),
onDelete = ForeignKey.CASCADE
)
), indices = arrayOf(Index(value = "customer_id"))
)
class Bill constructor(amount: Int, customerId: Int) {
@PrimaryKey(autoGenerate = true)
var uid: Int = 0
@ColumnInfo(name = "amount")
var amount: Int = amount
@ColumnInfo(name = "customer_id")
var customerId: Int = customerId
}
En este caso, le hemos dicho que el campo «customer_id» es una columna que hace referencia a la columna «uid» de la tabla Customer. A parte, hemos indexado el campo «customer_id» para evitar realizar un «full scan» cuando realicemos búsquedas.
Bien, creo que lo voy cogiendo
Para poder trabajar con nuestras tablas, necesitamos hacer uso de 2 componentes, Database y DAO (Data Access Object). En nuestra clase anotada con @Database, definiremos nuestras entidades, versión y parámetros de configuración, así como los métodos para obtener los DAOs. Es una practica recomendable que esta clase sea un singleton, ya que las instancias de RoomDatabase son bastante «pesadas».
@Database(entities = [(Customer::class), (Provider::class), (Bill::class)], version = 5, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun customerDao(): CustomerDao
abstract fun providerDao(): ProviderDao
abstract fun billDao(): BillDao
companion object {
/**
* The only instance
*/
private var sInstance: AppDatabase? = null
/**
* Gets the singleton instance of SampleDatabase.
*
* @param context The context.
* @return The singleton instance of SampleDatabase.
*/
@Synchronized
fun getInstance(context: Context): AppDatabase {
if (sInstance == null) {
sInstance = Room
.databaseBuilder(context.applicationContext, AppDatabase::class.java, "example")
.fallbackToDestructiveMigration()
.build()
}
return sInstance!!
}
}
}
Como podéis ver, hemos anotado las clases que conforman nuestra base de datos y la versión actual, así como los parámetros de configuración que consideremos.
¿Y los DAOs?
Los DAOs son interfaces que contienen las consultas a que se realizarán a base de datos. ROOM nos facilita parte de las consultas habituales. como por ejemplo UPDATE, DELETE o INSERT. Estas consultas podemos utilizarlas anotando las funciones con sus correspondientes anotaciones, valga la redundancia. Dichas anotaciones son @Insert, @Update y @Delete. A parte de estas, podemos hacer uso de @Query. Esta anotación, nos permite escribir la consulta que nos haga falta como valor. Veamos un ejemplo:
@Dao
interface BillDao {
@get:Query("SELECT * FROM bill")
val all: List<Bill>
@Query("SELECT * FROM bill WHERE uid IN (:billIds)")
fun loadAllByIds(billIds: Array<Int>): List<Bill>
@Query("SELECT * FROM bill WHERE customer_id = :uid")
fun findByCustomerId(uid: Int): List<Bill>
@Insert
fun insertAll(bills: List<Bill>)
@Insert
fun insert(bill: Bill)
@Delete
fun delete(bill: Bill)
}
En este caso, tenemos varias funciones:
- La primera nos devolverá todos los registros entrando a través de la variable «all«.
- loadAllByIds contiene una query preestablecida que nos devolverá todos los registros cuyos id´s estén dentro del «IN».
- findByCustomerId nos devolverá todas las facturas de un cliente en concreto.
- insertAll e insert, escribirán nuevos registros, bien como un batch o de uno en uno.
- delete como su nombre indica, eliminará el registro que le facilitemos.
Seguramente os habéis dado cuenta ya, pero los parámetros que tenemos dentro de @Query se relacionan con los parámetros que le llegan a la función para no tener que escribir código extra.
¿Y cómo orquesto todo esto?
AppDatabse es tu fiel compañera. Será quien se encargue de todo.
doAsync {
val database = AppDatabase.getInstance(context = this@BillsActivity)
val bills = database.billDao().all
uiThread {
mAdapter!!.addAll(bills)
}
}
val customers: MutableList<Customer> = mutableListOf()
for (index: Int in 0..20) {
val client = Customer(index, "Name" + index, "Surname" + index)
customers.add(index, client)
}
database.customerDao().insertAll(customers = customers)
Fácil, ¿verdad?
Conclusiones
Si has trabajado alguna vez con ORM´s como Realm, pronto te darás cuenta de que se parecen mucho. Siendo ROOM quien conserva pinceladas de lo que una vez fue la gestión de bases de datos en Android. Si hablamos de crear bases de datos desde 0, seguramente hacerlo con Realm sea bastante mas rápido.
Por otro lado si lo que nos interesa es el rendimiento, ROOM no es el competidor más rápido del sector. No obstante, tiene otras cualidades que lo hacen una buena apuesta para un proyecto, como por ejemplo sus no mas de 300 métodos y 50kb de peso en la apk final. En algunos casos, estos argumentos son mas que suficientes para decantarse por un ORM u otro.
Extra ball
Si quieres echar un ojo al repositorio, contribuir y mejorar los ejemplos, aquí os dejo el enlace https://github.com/irontec/android-room-example, sed bienvenid@s.
Queremos tu opinión :)