Room Database en Android 2026: Migraciones, Relaciones y Coroutines
Guía completa de Room Database en 2026: migraciones automáticas y manuales, relaciones entre entidades, integración con Kotlin Coroutines y Flow para aplicaciones Android modernas.

Room Database representa la solución de persistencia recomendada por Google para aplicaciones Android. En 2026, esta biblioteca ofrece integración nativa con Kotlin Coroutines y Flow, migraciones automáticas robustas y gestión avanzada de relaciones entre entidades. Este tutorial presenta las funcionalidades esenciales de Room para desarrollar aplicaciones Android de alto rendimiento y fácil mantenimiento.
Room forma parte del conjunto Android Jetpack y proporciona una capa de abstracción sobre SQLite. La versión 2.7+ incorpora mejoras significativas en migraciones automáticas y soporte nativo para coroutines.
Configuración de Room Database
La integración de Room en un proyecto Android moderno requiere agregar las dependencias apropiadas y configurar el plugin KSP (Kotlin Symbol Processing) para la generación de código.
plugins {
id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}
dependencies {
val roomVersion = "2.7.0"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
}La configuración del compilador Room permite activar funcionalidades avanzadas como la exportación del esquema para migraciones.
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.generateKotlin", "true")
}Definición de Entidades y Relaciones
Room utiliza anotaciones para mapear clases Kotlin hacia tablas SQLite. Las entidades representan las tablas de la base de datos.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "full_name")
val fullName: String,
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis()
)
@Entity(
tableName = "posts",
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["user_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index(value = ["user_id"])]
)
data class Post(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "user_id")
val userId: Long,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "content")
val content: String
)Para representar relaciones entre entidades, Room ofrece clases de datos con la anotación @Relation.
data class UserWithPosts(
@Embedded
val user: User,
@Relation(
parentColumn = "id",
entityColumn = "user_id"
)
val posts: List<Post>
)
data class PostWithUser(
@Embedded
val post: Post,
@Relation(
parentColumn = "user_id",
entityColumn = "id"
)
val user: User
)Las relaciones many-to-many requieren una tabla de unión.
@Entity(tableName = "tags")
data class Tag(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String
)
@Entity(
tableName = "post_tag_cross_ref",
primaryKeys = ["postId", "tagId"]
)
data class PostTagCrossRef(
val postId: Long,
val tagId: Long
)
data class PostWithTags(
@Embedded
val post: Post,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = PostTagCrossRef::class,
parentColumn = "postId",
entityColumn = "tagId"
)
)
val tags: List<Tag>
)Creación de DAOs con Coroutines y Flow
Los Data Access Objects (DAO) definen las operaciones de base de datos. Room 2.7 ofrece soporte nativo para coroutines Kotlin y Flow.
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY created_at DESC")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Long): Flow<User?>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserByIdOnce(userId: Long): User?
@Transaction
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserWithPosts(userId: Long): Flow<UserWithPosts?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(users: List<User>): List<Long>
@Update
suspend fun updateUser(user: User): Int
@Delete
suspend fun deleteUser(user: User): Int
@Query("DELETE FROM users WHERE id = :userId")
suspend fun deleteUserById(userId: Long): Int
}
@Dao
interface PostDao {
@Query("SELECT * FROM posts WHERE user_id = :userId ORDER BY id DESC")
fun getPostsByUser(userId: Long): Flow<List<Post>>
@Transaction
@Query("SELECT * FROM posts WHERE id = :postId")
fun getPostWithTags(postId: Long): Flow<PostWithTags?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPost(post: Post): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPostTagCrossRef(crossRef: PostTagCrossRef)
@Transaction
suspend fun insertPostWithTags(post: Post, tagIds: List<Long>) {
val postId = insertPost(post)
tagIds.forEach { tagId ->
insertPostTagCrossRef(PostTagCrossRef(postId, tagId))
}
}
}Configuración de la Base de Datos
La clase abstracta anotada con @Database representa el punto de entrada principal de Room.
@Database(
entities = [
User::class,
Post::class,
Tag::class,
PostTagCrossRef::class
],
version = 1,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}Migraciones de Base de Datos
Room ofrece dos enfoques para las migraciones: automáticas y manuales. Las migraciones automáticas gestionan modificaciones simples del esquema.
@Database(
entities = [User::class, Post::class, Tag::class, PostTagCrossRef::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
],
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
// ...
}Para modificaciones complejas que requieren especificación explícita, Room proporciona AutoMigrationSpec.
@Database(
entities = [User::class, Post::class, Tag::class, PostTagCrossRef::class],
version = 3,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3, spec = AppDatabase.Migration2To3::class)
],
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@RenameColumn(tableName = "users", fromColumnName = "full_name", toColumnName = "display_name")
@DeleteColumn(tableName = "users", columnName = "legacy_field")
class Migration2To3 : AutoMigrationSpec
// ...
}Las migraciones manuales ofrecen control total para transformaciones de datos complejas.
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
// Creación de una nueva tabla con el esquema actualizado
db.execSQL("""
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
display_name TEXT NOT NULL,
email TEXT NOT NULL,
avatar_url TEXT,
created_at INTEGER NOT NULL DEFAULT 0
)
""".trimIndent())
// Migración de datos existentes
db.execSQL("""
INSERT INTO users_new (id, display_name, email, created_at)
SELECT id, display_name, email, created_at FROM users
""".trimIndent())
// Eliminación de la tabla antigua y renombrado
db.execSQL("DROP TABLE users")
db.execSQL("ALTER TABLE users_new RENAME TO users")
// Creación de índices
db.execSQL("CREATE INDEX index_users_email ON users(email)")
}
}
// Aplicación de la migración
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_3_4)
.build()Integración con Repository Pattern
La arquitectura recomendada utiliza un Repository como intermediario entre los ViewModels y los DAOs.
class UserRepository(
private val userDao: UserDao,
private val postDao: PostDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
val allUsers: Flow<List<User>> = userDao.getAllUsers()
fun getUserWithPosts(userId: Long): Flow<UserWithPosts?> {
return userDao.getUserWithPosts(userId)
}
suspend fun createUser(fullName: String, email: String): Result<Long> {
return withContext(ioDispatcher) {
try {
val user = User(fullName = fullName, email = email)
val id = userDao.insertUser(user)
Result.success(id)
} catch (e: Exception) {
Result.failure(e)
}
}
}
suspend fun createPostWithTags(
userId: Long,
title: String,
content: String,
tagIds: List<Long>
): Result<Long> {
return withContext(ioDispatcher) {
try {
val post = Post(userId = userId, title = title, content = content)
postDao.insertPostWithTags(post, tagIds)
Result.success(post.id)
} catch (e: Exception) {
Result.failure(e)
}
}
}
}Uso con ViewModel y StateFlow
La integración de Room con ViewModels permite una gestión reactiva de la interfaz de usuario.
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _selectedUserId = MutableStateFlow<Long?>(null)
val users: StateFlow<List<User>> = repository.allUsers
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
val selectedUserWithPosts: StateFlow<UserWithPosts?> = _selectedUserId
.filterNotNull()
.flatMapLatest { userId ->
repository.getUserWithPosts(userId)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
fun selectUser(userId: Long) {
_selectedUserId.value = userId
}
fun createUser(fullName: String, email: String) {
viewModelScope.launch {
repository.createUser(fullName, email)
.onSuccess { id ->
_selectedUserId.value = id
}
.onFailure { error ->
// Manejo del error
}
}
}
}Pruebas Unitarias de Room Database
Room proporciona utilidades para probar migraciones y operaciones de base de datos.
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(
context,
AppDatabase::class.java
)
.allowMainThreadQueries()
.build()
userDao = database.userDao()
}
@After
fun teardown() {
database.close()
}
@Test
fun insertAndRetrieveUser() = runTest {
val user = User(fullName = "John Doe", email = "john@example.com")
val id = userDao.insertUser(user)
val retrieved = userDao.getUserByIdOnce(id)
assertThat(retrieved).isNotNull()
assertThat(retrieved?.fullName).isEqualTo("John Doe")
}
@Test
fun flowEmitsUpdates() = runTest {
val user = User(fullName = "Jane Doe", email = "jane@example.com")
userDao.getAllUsers().test {
assertThat(awaitItem()).isEmpty()
userDao.insertUser(user)
val users = awaitItem()
assertThat(users).hasSize(1)
assertThat(users[0].fullName).isEqualTo("Jane Doe")
cancelAndIgnoreRemainingEvents()
}
}
}
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun migrate3To4() {
// Creación de la base v3
helper.createDatabase("test_db", 3).apply {
execSQL("INSERT INTO users (id, display_name, email, created_at) VALUES (1, 'Test', 'test@test.com', 0)")
close()
}
// Migración a v4
val db = helper.runMigrationsAndValidate("test_db", 4, true, MIGRATION_3_4)
// Verificación
val cursor = db.query("SELECT * FROM users WHERE id = 1")
assertThat(cursor.moveToFirst()).isTrue()
assertThat(cursor.getString(cursor.getColumnIndex("display_name"))).isEqualTo("Test")
}
}¿Listo para aprobar tus entrevistas de Android?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Preguntas Frecuentes en Entrevistas
Durante las entrevistas técnicas de Android, las preguntas sobre Room Database aparecen con regularidad. Los candidatos deben dominar las diferencias entre Flow y suspend para consultas, la gestión de migraciones sin pérdida de datos, y la optimización de consultas con relaciones.
Los puntos clave incluyen la comprensión del funcionamiento interno de Room con SQLite, el uso apropiado de índices para el rendimiento, y la gestión de transacciones para mantener la integridad de los datos.
Room Database continúa siendo un componente fundamental del ecosistema Android Jetpack. El dominio de las migraciones, las relaciones entre entidades y la integración con Kotlin Coroutines permite construir aplicaciones robustas y de alto rendimiento. El enfoque reactivo con Flow simplifica la sincronización entre la base de datos y la interfaz de usuario, mientras que el patrón Repository asegura una arquitectura limpia y testeable.

Escrito por
Anthony Fillion-MailletDesarrollador fullstack, fundador de SharpSkill
Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.
Actualizado el 10 de julio de 2026
Compartir
Artículos relacionados

Kotlin Flow vs StateFlow vs SharedFlow: preguntas de entrevista de Android en 2026
Las preguntas sobre Kotlin Flow vs StateFlow vs SharedFlow que hacen los entrevistadores de Android en 2026, con respuestas claras, una tabla comparativa y código listo para producción.

Kotlin Multiplatform 2026: compartir código Android/iOS
Cómo Kotlin Multiplatform comparte la lógica de negocio entre Android e iOS en 2026: configuración de Gradle, expect/actual, red con Ktor, interfaz Compose Multiplatform y preguntas de entrevista de KMP comunes.

Android 16 en 2026: Nuevas APIs, Modo Escritorio y Preguntas de Entrevista
Análisis profundo de Android 16 API 36: edge-to-edge obligatorio, Desktop Mode, ProgressStyle, navegación predictiva y preguntas de entrevista técnica para desarrolladores Android en 2026.