Si tu app controla grandes cantidades de datos estructurados, puedes beneficiarte con la posibilidad de conservar esos datos localmente. El caso de uso más común consiste en almacenar en caché datos relevantes para que el dispositivo no pueda acceder a la red, de modo que el usuario pueda explorar ese contenido mientras está sin conexión.
La biblioteca de persistencias Room brinda una capa de abstracción para SQLite que te permite acceder a la base de datos sin problemas y, al mismo tiempo, aprovechar toda la tecnología de SQLite.
Cómo configurar Room 2.x
Para usar Room 2.x en tu app, agrega las siguientes dependencias al archivo build.gradle de la app:
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
// ksp("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$room_version")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}
Componentes principales
Room tiene tres componentes principales:
- Clase de base de datos que contiene la base de datos y sirve como punto de acceso principal para la conexión subyacente a los datos persistentes de la app
- Entidades de datos que representan tablas de la base de datos de tu app
- Objetos de acceso a datos (DAO) que proporcionan métodos que tu app puede usar para consultar, actualizar, insertar y borrar datos en la base de datos
En la figura 1, se muestran las relaciones entre los diferentes componentes de Room.
Ejemplo de implementación
// Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Usage
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
Cómo definir datos mediante entidades
Cada entidad de Room representa una tabla en la base de datos. Define cada entidad como una
clase con @Entity como anotación.
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap? = null
)
- Nombres de tabla y columna personalizados: De forma predeterminada, Room usa el nombre de la clase como
el nombre de la tabla y los nombres de propiedad como nombres de columna. Para personalizarlos, usa
la
tableNamepropiedad en@Entityy la@ColumnInfo(name = "...")anotación. - Clave primaria: Para definir una clave primaria, usa
@PrimaryKey. Para las claves compuestas, usa la propiedadprimaryKeysde@Entity:@Entity(primaryKeys = ["firstName", "lastName"]). - Ignorar campos: Para evitar que los campos persistan, usa
@Ignore.
Convertidores de tipos
A veces, necesitas almacenar tipos personalizados, como Date, en una sola columna.
Proporciona @TypeConverter métodos para convertir tipos personalizados en tipos que
Room puede conservar y viceversa.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}
// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
Cómo acceder a datos con DAO
Los DAO definen métodos para la interacción con la base de datos. Anota la interfaz o la clase abstracta
con @Dao.
Métodos de conveniencia
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- Insertar: El método de inserción puede mostrar un
Longque representa el ID de la fila insertada o unList<Long>que contiene los IDs de todas las filas insertadas. - Actualizar o borrar: El método de actualización o eliminación puede mostrar un
Intque representa la cantidad de filas afectadas.
Métodos de búsqueda
Anota métodos con @Query para escribir instrucciones de SQL. Room valida las consultas en el tiempo de compilación.
@Dao
interface UserDao {
// Simple query
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
// Return a subset of columns using a POJO or tuple
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
// Pass parameters
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
// Collection of parameters
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
// Join tables
@Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
fun findBooksBorrowedByName(userName: String): List<Book>
}
Tipos de datos que se muestran de multimapa
En Room 2.4 y versiones posteriores, los métodos de búsqueda pueden mostrar un multimapa directamente con el tipo Map:
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
Consultas DAO asíncronas
Para evitar que la IU se bloquee, las consultas de la base de datos no pueden ejecutarse en el subproceso principal. Haz que tus consultas sean asíncronas con una de las siguientes integraciones:
Corrutinas y Flow de Kotlin
Requiere la dependencia room-ktx.
@Dao
interface UserDao {
// One-shot async query
@Insert
suspend fun insertUsers(vararg users: User)
// Observable query using Flow
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flow<User>
}
Java con RxJava
Requiere room-rxjava2 o room-rxjava3.
@Dao
interface UserDao {
@Insert
fun insertUsers(users: List<User>): Completable
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flowable<User>
}
Java con LiveData y Guava
Requiere room-guava para ListenableFuture.
@Dao
interface UserDao {
// LiveData for observable queries
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): LiveData<User>
// Guava ListenableFuture for one-shot queries
@Insert
fun insertUsers(users: List<User>): ListenableFuture<Integer>
}
Cómo definir relaciones en Room 2.x
Para evitar la carga diferida en el subproceso de IU, no puedes usar referencias directas de objetos entre entidades. En su lugar, define relaciones con clases de datos intermedias
con @Relation.
Uno a uno
Cada usuario tiene una sola biblioteca.
@Entity
data class User(@PrimaryKey val userId: Long, val name: String)
@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)
// Intermediate class
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
Uno a varios
Cada usuario puede tener muchas playlists.
@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
Varios a varios
Las playlists pueden tener muchas canciones, y las canciones pueden estar en muchas playlists. Requiere una tabla de unión.
@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
Relaciones anidadas
Consulta los usuarios, sus playlists y todas las canciones de esas playlists.
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
Administración de bases de datos
En esta sección, se abarcan varios aspectos de la administración de tu base de datos de Room, incluidas las vistas de la base de datos, la propagación previa de datos y las migraciones de bases de datos.
Vistas de la base de datos
Encapsula una consulta compleja en una clase con @DatabaseView como anotación.
@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)
// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }
Propaga la base de datos
Propaga la base de datos en la inicialización desde un archivo de recursos o el sistema de archivos.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
Migraciones
Cuando cambies el esquema, aumenta la versión de la base de datos y define un
Migration objeto.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2)
.build()
- Migraciones automáticas: Si usas Room 2.4.0 o versiones posteriores, puedes usar
@AutoMigrationpara migrar automáticamente los cambios básicos del esquema. Para ello, debes establecerexportSchemaentrueen la configuración de la base de datos:@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). - Respaldo destructivo: Si resulta aceptable perder datos cuando faltan rutas de migración, llama a
.fallbackToDestructiveMigrationcuando compiles la base de datos.
Cómo probar migraciones
Para verificar las migraciones, usa MigrationTestHelper del artefacto room-testing. Para admitir esto, asegúrate de exportar esquemas en la configuración de build.gradle.
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrate1To2() {
var db = helper.createDatabase("test-db", 1).apply {
execSQL("INSERT INTO User VALUES (1, 'John')")
close()
}
db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
// Verify data was migrated correctly
}
}
Cómo migrar de SQLite a Room
Para migrar tu app de SQLite a Room, completa los siguientes pasos:
- Actualiza las dependencias para incluir Room.
- Anota las clases de modelo con
@Entity,@PrimaryKeyy@ColumnInfo. - Crea DAO para reemplazar tus métodos de búsqueda auxiliares.
- Crea una clase RoomDatabase que haga referencia a tus entidades y DAO. Aumenta el número de versión.
- Define una ruta de migración vacía porque el esquema no cambia, solo
el framework:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - Actualiza la instancia para usar
Room.databaseBuildercon la ruta de migración.