# Room Database in Android 2026: Migraties, Relaties en Coroutines > Beheers Room database in Android met migraties, entity-relaties, Kotlin coroutines en Flow. Complete tutorial met productie-klare patterns voor 2026. - Published: 2026-07-10 - Updated: 2026-07-10 - Author: SharpSkill - Tags: android, room, kotlin, sqlite, coroutines, database - Reading time: 12 min --- Room database biedt een robuuste abstractielaag boven SQLite in Android en handelt de boilerplate code af die directe SQLite-gebruik omslachtig maakt. Met Room 2.6 en Kotlin coroutines is het bouwen van reactieve, type-veilige databaselagen aanzienlijk eenvoudiger geworden. > **Room 2.6 Belangrijkste Features** > > Room 2.6 (stabiel in 2026) introduceert verbeterde migratie-ondersteuning, snellere KSP-verwerking en betere Flow-integratie. Deze tutorial gebruikt deze nieuwste APIs voor productie-klare code. ## Room Database opzetten met Kotlin KSP Room vereist drie kerncomponenten: entities (tabellen), DAOs (data access objects) en de databaseklasse. De KSP-annotationprocessor genereert implementatiecode tijdens het compileren. De dependencies worden toegevoegd aan het module-niveau `build.gradle.kts` bestand. KSP heeft KAPT vervangen als aanbevolen annotationprocessor sinds 2024, met snellere buildtijden. ```kotlin // build.gradle.kts (Module :app) plugins { id("com.google.devtools.ksp") version "2.1.0-1.0.29" } dependencies { val roomVersion = "2.6.1" implementation("androidx.room:room-runtime:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") // Coroutines support ksp("androidx.room:room-compiler:$roomVersion") } ``` Het `room-ktx` artifact biedt coroutine-extensies, waardoor suspend-functies in DAOs mogelijk zijn. ## Entities definiëren met Primary Keys en Indexen Entities worden direct gemapped naar SQLite-tabellen. Elke entity-klasse heeft minimaal één primary key nodig. Het toevoegen van indexen op veelgebruikte kolommen verbetert de leesprestaties ten koste van iets langzamere schrijfoperaties. ```kotlin // User.kt import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey @Entity( tableName = "users", indices = [ Index(value = ["email"], unique = true), // Unique constraint + index Index(value = ["created_at"]) // Index for sorting queries ] ) data class User( @PrimaryKey(autoGenerate = true) val id: Long = 0, val email: String, val displayName: String, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(), val isActive: Boolean = true ) ``` De `@ColumnInfo`-annotatie past de kolomnaam aan wanneer de Kotlin-propertynaam verschilt van de gewenste database-kolomnaam. ## DAOs bouwen met Coroutines en Flow DAOs definiëren de database-operaties. Room 2.6 ondersteunt drie asynchrone patterns: suspend-functies voor eenmalige operaties, `Flow` voor reactieve streams en `ListenableFuture` voor Java-interoperabiliteit. ```kotlin // UserDao.kt import androidx.room.* import kotlinx.coroutines.flow.Flow @Dao interface UserDao { // One-shot insert - returns the generated ID @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUser(user: User): Long // Batch insert - efficient for multiple records @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insertUsers(users: List) // Flow emits new data whenever the table changes @Query("SELECT * FROM users WHERE is_active = 1 ORDER BY created_at DESC") fun observeActiveUsers(): Flow> // Suspend function for one-shot read @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserById(userId: Long): User? // Update returns the number of affected rows @Update suspend fun updateUser(user: User): Int // Delete by primary key @Delete suspend fun deleteUser(user: User) // Custom delete query @Query("DELETE FROM users WHERE is_active = 0") suspend fun deleteInactiveUsers(): Int } ``` Flow-retournerende queries geven automatisch bijgewerkte resultaten uit wanneer de onderliggende data verandert. Dit elimineert handmatige refresh-logica in ViewModels. ## De Room Database-klasse maken De databaseklasse koppelt entities en DAOs aan elkaar. Een singleton-pattern voorkomt meerdere database-instanties, wat resource-lekken zou veroorzaken. ```kotlin // AppDatabase.kt import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @Database( entities = [User::class, Post::class], version = 1, exportSchema = true // Generates schema JSON for migration validation ) 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() // Only for development .build() INSTANCE = instance instance } } } } ``` Het instellen van `exportSchema = true` genereert JSON-schemabestanden in de `schemas/` map, nuttig voor het valideren van migraties in tests. > **Productie-migratiestrategie** > > Gebruik nooit `fallbackToDestructiveMigration()` in productie. Dit verwijdert alle gebruikersdata wanneer het schema wijzigt. De volgende sectie behandelt correcte migratie-handling. ## Veilige database-migraties schrijven Migraties behouden gebruikersdata wanneer het schema verandert tussen app-versies. Room valideert migraties tijdens runtime door het verwachte schema te vergelijken met de gemigreerde database. Een migratie definieert de SQL-statements die nodig zijn om de database te transformeren van versie N naar versie N+1. ```kotlin // Migrations.kt import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase // Migration from version 1 to 2: add phone_number column val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT") } } // Migration from version 2 to 3: add posts table with foreign key val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(""" CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """.trimIndent()) db.execSQL("CREATE INDEX index_posts_user_id ON posts(user_id)") } } // Register migrations in database builder fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ) .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build() .also { INSTANCE = it } } } ``` Room ondersteunt migratie-chaining. Als een gebruiker upgradet van versie 1 naar versie 3, voert Room MIGRATION_1_2 en daarna MIGRATION_2_3 sequentieel uit. ## Auto-migraties in Room 2.6 Voor eenvoudige schema-wijzigingen (kolommen, tabellen of indexen toevoegen) kan Room migraties automatisch genereren. Dit vermindert boilerplate-code voor straightforward updates. ```kotlin // AppDatabase.kt @Database( entities = [User::class, Post::class], version = 3, autoMigrations = [ AutoMigration(from = 1, to = 2), // Adding nullable column AutoMigration( from = 2, to = 3, spec = AutoMigration2To3::class // Complex changes need spec ) ], exportSchema = true ) abstract class AppDatabase : RoomDatabase() { // DAOs... } // Migration spec for changes that need hints @RenameColumn(tableName = "users", fromColumnName = "name", toColumnName = "display_name") class AutoMigration2To3 : AutoMigrationSpec ``` Auto-migraties vereisen `exportSchema = true` om versies te vergelijken. Voor kolom-hernoemingen, tabel-hernoemingen of verwijderingen moet een `AutoMigrationSpec` met de juiste annotaties worden opgegeven. ## Entity-relaties modelleren met Embedded Objects Room ondersteunt drie relatiepatronen: embedded objecten, one-to-many en many-to-many. In tegenstelling tot ORMs retourneert Room platte data — relaties vereisen expliciete queries. ### One-to-Many: Gebruiker met Posts Een gebruiker kan meerdere posts hebben. De relatie wordt gedefinieerd met de `@Relation`-annotatie in een data class die beide entities combineert. ```kotlin // Post.kt @Entity( tableName = "posts", foreignKeys = [ ForeignKey( entity = User::class, parentColumns = ["id"], childColumns = ["user_id"], onDelete = ForeignKey.CASCADE // Delete posts when user deleted ) ], indices = [Index("user_id")] // Required for foreign key performance ) data class Post( @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo(name = "user_id") val userId: Long, val title: String, val content: String, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis() ) // UserWithPosts.kt - Relation container data class UserWithPosts( @Embedded val user: User, @Relation( parentColumn = "id", entityColumn = "user_id" ) val posts: List ) // In UserDao.kt @Transaction // Ensures atomic read of user + posts @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserWithPosts(userId: Long): UserWithPosts? @Transaction @Query("SELECT * FROM users WHERE is_active = 1") fun observeActiveUsersWithPosts(): Flow> ``` De `@Transaction`-annotatie voorkomt inconsistente reads wanneer de relatie intern meerdere queries omvat. ### Many-to-Many: Gebruikers en Tags Many-to-many relaties vereisen een junction-tabel (cross-reference) die paren van foreign keys opslaat. ```kotlin // Tag.kt @Entity(tableName = "tags") data class Tag( @PrimaryKey(autoGenerate = true) val tagId: Long = 0, val name: String ) // UserTagCrossRef.kt - Junction table @Entity( tableName = "user_tag_cross_ref", primaryKeys = ["userId", "tagId"], foreignKeys = [ ForeignKey(entity = User::class, parentColumns = ["id"], childColumns = ["userId"], onDelete = ForeignKey.CASCADE), ForeignKey(entity = Tag::class, parentColumns = ["tagId"], childColumns = ["tagId"], onDelete = ForeignKey.CASCADE) ] ) data class UserTagCrossRef( val userId: Long, val tagId: Long ) // UserWithTags.kt data class UserWithTags( @Embedded val user: User, @Relation( parentColumn = "id", entityColumn = "tagId", associateBy = Junction(UserTagCrossRef::class) ) val tags: List ) ``` De `associateBy`-parameter specificeert de junction-tabel die gebruikers aan tags koppelt. ## Room integreren met ViewModel en Repository Een schone architectuur scheidt de databaselaag van de UI. Het repository-pattern abstraheert databronnen, terwijl ViewModel de state blootstelt aan Compose UI. ```kotlin // UserRepository.kt class UserRepository(private val userDao: UserDao) { val activeUsers: Flow> = userDao.observeActiveUsers() suspend fun createUser(email: String, displayName: String): Long { val user = User(email = email, displayName = displayName) return userDao.insertUser(user) } suspend fun deactivateUser(userId: Long) { userDao.getUserById(userId)?.let { user -> userDao.updateUser(user.copy(isActive = false)) } } } // UserViewModel.kt class UserViewModel( private val repository: UserRepository ) : ViewModel() { val users: StateFlow> = repository.activeUsers .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun createUser(email: String, name: String) { viewModelScope.launch { repository.createUser(email, name) // No manual refresh needed - Flow emits automatically } } } ``` De `stateIn`-operator converteert de koude `Flow` van Room naar een warme `StateFlow` geschikt voor Compose-collectie. Voor meer details over coroutine-patterns, zie de gids over [Kotlin Coroutines](/blog/android/mastering-kotlin-coroutines). > **Room Databases testen** > > Room biedt een in-memory database builder voor testen: `Room.inMemoryDatabaseBuilder()`. Tests draaien sneller en persisteren geen data tussen runs. Test migraties altijd met `MigrationTestHelper` uit het `room-testing` artifact. ## Type Converters voor complexe types Room slaat alleen primitieve types native op. Type converters transformeren complexe types (datums, enums, lijsten) naar opslagbare formaten. ```kotlin // Converters.kt import androidx.room.TypeConverter import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset class Converters { // Instant <-> Long (milliseconds) @TypeConverter fun fromInstant(instant: Instant?): Long? = instant?.toEpochMilli() @TypeConverter fun toInstant(millis: Long?): Instant? = millis?.let { Instant.ofEpochMilli(it) } // LocalDate <-> String (ISO format) @TypeConverter fun fromLocalDate(date: LocalDate?): String? = date?.toString() @TypeConverter fun toLocalDate(dateStr: String?): LocalDate? = dateStr?.let { LocalDate.parse(it) } // List <-> JSON string @TypeConverter fun fromStringList(list: List?): String? = list?.joinToString(separator = ",") @TypeConverter fun toStringList(data: String?): List? = data?.split(",")?.filter { it.isNotBlank() } } // Register in database class @Database(...) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() ``` Voor JSON-serialisatie van complexe objecten wordt het gebruik van [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) aanbevolen in plaats van handmatige string-manipulatie. ## Database-operaties afhandelen in Coroutines Scope Room DAO-operaties moeten worden uitgevoerd op de IO-dispatcher. Hoewel Room threading intern afhandelt voor suspend-functies, houdt error-handling op repository-niveau de code robuust. ```kotlin // UserRepository.kt with error handling import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class UserRepository(private val userDao: UserDao) { suspend fun createUser(email: String, displayName: String): Result { return withContext(Dispatchers.IO) { try { val id = userDao.insertUser( User(email = email, displayName = displayName) ) Result.success(id) } catch (e: SQLiteConstraintException) { // Unique constraint violation (duplicate email) Result.failure(DuplicateEmailException(email)) } } } suspend fun deleteAllInactive(): Int = withContext(Dispatchers.IO) { userDao.deleteInactiveUsers() } } class DuplicateEmailException(email: String) : Exception("Email already exists: $email") ``` Resultaten wrappen in `Result` stelt ViewModels in staat om fouten elegant af te handelen zonder try-catch boilerplate. ## Conclusie Het bouwen van een productie-klare Room-databaselaag omvat verschillende belangrijke praktijken: - Gebruik KSP in plaats van KAPT voor snellere compilatie met Room 2.6 - Voeg indexen toe op kolommen die worden gebruikt in WHERE-clausules en JOIN-condities - Retourneer Flow vanuit DAOs voor automatische UI-updates wanneer data verandert - Schrijf expliciete migraties voor schema-wijzigingen — gebruik nooit destructieve migratie in productie - Gebruik `@Transaction` voor relatie-queries om consistente reads te garanderen - Wrap database-operaties in Result voor schone error-handling in ViewModels - Test migraties met `MigrationTestHelper` voordat updates worden uitgebracht Deze patterns kunnen worden verdiept met [Room database sollicitatievragen](/technologies/android/interview-questions/android-room-database). --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/android/room-database-android-2026-migrations-relations-coroutines