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 en Android 2026: Migraciones, Relaciones y Coroutines

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.

build.gradle.kts (Module: app)kotlin
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.

build.gradle.ktskotlin
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.

kotlin
@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.

kotlin
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.

kotlin
@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.

kotlin
@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.

kotlin
@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.

kotlin
@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.

kotlin
@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.

kotlin
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.

kotlin
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.

kotlin
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.

kotlin
@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.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Desarrollador 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