# Room-Datenbank in Android 2026: Migrationen, Relationen und Coroutines > Beherrsche Room-Datenbanken in Android mit Migrationen, Entity-Relationen, Kotlin Coroutines und Flow. Vollständiges Tutorial mit produktionsreifen Patterns für 2026. - Published: 2026-07-10 - Updated: 2026-07-10 - Author: SharpSkill - Tags: android, room, kotlin, sqlite, coroutines, database - Reading time: 12 min --- Die Room-Datenbank bietet eine robuste Abstraktionsschicht über SQLite in Android und übernimmt den Boilerplate-Code, der die direkte SQLite-Nutzung mühsam macht. Mit Room 2.6 und Kotlin Coroutines ist der Aufbau reaktiver, typsicherer Datenbankschichten deutlich einfacher geworden. > **Room 2.6 Hauptfunktionen** > > Room 2.6 (stabil seit 2026) führt verbesserte Migrationsunterstützung, schnellere KSP-Verarbeitung und bessere Flow-Integration ein. Dieses Tutorial verwendet diese neuesten APIs für produktionsreifen Code. ## Room-Datenbank mit Kotlin KSP einrichten Room erfordert drei Kernkomponenten: Entities (Tabellen), DAOs (Data Access Objects) und die Datenbankklasse. Der KSP-Annotationsprozessor generiert Implementierungscode zur Kompilierzeit. Die Dependencies werden zur modul-spezifischen `build.gradle.kts` hinzugefügt. KSP hat KAPT seit 2024 als empfohlener Annotationsprozessor abgelöst und bietet schnellere Build-Zeiten. ```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") } ``` Das `room-ktx`-Artefakt stellt Coroutine-Erweiterungen bereit, die Suspend-Funktionen in DAOs ermöglichen. ## Entities mit Primary Keys und Indizes definieren Entities werden direkt auf SQLite-Tabellen abgebildet. Jede Entity-Klasse benötigt mindestens einen Primärschlüssel. Das Hinzufügen von Indizes auf häufig abgefragten Spalten verbessert die Leseleistung auf Kosten etwas langsamerer Schreibvorgänge. ```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 ) ``` Die `@ColumnInfo`-Annotation passt den Spaltennamen an, wenn der Kotlin-Eigenschaftsname vom gewünschten Datenbank-Spaltennamen abweicht. ## DAOs mit Coroutines und Flow erstellen DAOs definieren die Datenbankoperationen. Room 2.6 unterstützt drei asynchrone Patterns: Suspend-Funktionen für einmalige Operationen, `Flow` für reaktive Streams und `ListenableFuture` für Java-Interoperabilität. ```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-Abfragen geben automatisch aktualisierte Ergebnisse aus, wenn sich die zugrunde liegenden Daten ändern. Dies eliminiert manuelle Aktualisierungslogik in ViewModels. ## Die Room-Datenbankklasse erstellen Die Datenbankklasse verbindet Entities und DAOs. Ein Singleton-Pattern verhindert mehrere Datenbankinstanzen, die Ressourcenlecks verursachen würden. ```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 } } } } ``` Die Einstellung `exportSchema = true` generiert JSON-Schema-Dateien im `schemas/`-Ordner, die für die Validierung von Migrationen in Tests nützlich sind. > **Produktions-Migrationsstrategie** > > Verwende niemals `fallbackToDestructiveMigration()` in der Produktion. Dies löscht alle Benutzerdaten bei Schema-Änderungen. Der nächste Abschnitt behandelt die korrekte Migrationshandhabung. ## Sichere Datenbankmigrationen schreiben Migrationen bewahren Benutzerdaten, wenn sich das Schema zwischen App-Versionen ändert. Room validiert Migrationen zur Laufzeit, indem es das erwartete Schema mit der migrierten Datenbank vergleicht. Eine Migration definiert die SQL-Anweisungen, die benötigt werden, um die Datenbank von Version N zu Version N+1 zu transformieren. ```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 unterstützt Migrationsketten. Wenn ein Benutzer von Version 1 auf Version 3 aktualisiert, führt Room MIGRATION_1_2 und dann MIGRATION_2_3 nacheinander aus. ## Auto-Migrationen in Room 2.6 Für einfache Schema-Änderungen (Hinzufügen von Spalten, Tabellen oder Indizes) kann Room Migrationen automatisch generieren. Dies reduziert Boilerplate-Code für unkomplizierte 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-Migrationen erfordern `exportSchema = true` zum Vergleich der Versionen. Für Spaltenumbenennungen, Tabellenumbenennungen oder Löschungen muss eine `AutoMigrationSpec` mit den entsprechenden Annotationen bereitgestellt werden. ## Entity-Relationen mit Embedded Objects modellieren Room unterstützt drei Relationsmuster: eingebettete Objekte, One-to-Many und Many-to-Many. Im Gegensatz zu ORMs gibt Room flache Daten zurück — Relationen erfordern explizite Abfragen. ### One-to-Many: Benutzer mit Beiträgen Ein Benutzer kann mehrere Beiträge haben. Die Relation wird mit der `@Relation`-Annotation in einer Datenklasse definiert, die beide Entities kombiniert. ```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> ``` Die `@Transaction`-Annotation verhindert inkonsistente Lesevorgänge, wenn die Relation intern mehrere Abfragen umfasst. ### Many-to-Many: Benutzer und Tags Many-to-Many-Relationen erfordern eine Junction-Tabelle (Kreuzverweis), die Paare von Fremdschlüsseln speichert. ```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 ) ``` Der `associateBy`-Parameter spezifiziert die Junction-Tabelle, die Benutzer mit Tags verknüpft. ## Room mit ViewModel und Repository integrieren Eine saubere Architektur trennt die Datenbankschicht von der UI. Das Repository-Pattern abstrahiert Datenquellen, während ViewModel den Zustand für Compose UI bereitstellt. ```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 } } } ``` Der `stateIn`-Operator konvertiert den kalten `Flow` von Room in einen heißen `StateFlow`, der für Compose Collection geeignet ist. Weitere Details zu Coroutine-Patterns finden sich im Leitfaden zu [Kotlin Coroutines](/blog/android/mastering-kotlin-coroutines). > **Room-Datenbanken testen** > > Room bietet einen In-Memory-Datenbank-Builder für Tests: `Room.inMemoryDatabaseBuilder()`. Tests laufen schneller und persistieren keine Daten zwischen den Durchläufen. Migrationen sollten immer mit `MigrationTestHelper` aus dem `room-testing`-Artefakt getestet werden. ## Type Converter für komplexe Typen Room speichert nativ nur primitive Typen. Type Converter transformieren komplexe Typen (Datumswerte, Enums, Listen) in speicherbare Formate. ```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() ``` Für die JSON-Serialisierung komplexer Objekte empfiehlt sich die Verwendung von [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) anstelle manueller String-Manipulation. ## Datenbankoperationen im Coroutines-Scope handhaben Room-DAO-Operationen sollten auf dem IO-Dispatcher ausgeführt werden. Obwohl Room das Threading intern für Suspend-Funktionen handhabt, hält Error-Handling auf Repository-Ebene den Code robust. ```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") ``` Das Wrappen von Ergebnissen in `Result` ermöglicht es ViewModels, Fehler elegant zu behandeln, ohne Try-Catch-Boilerplate. ## Fazit Der Aufbau einer produktionsreifen Room-Datenbankschicht umfasst mehrere Schlüsselpraktiken: - KSP anstelle von KAPT für schnellere Kompilierung mit Room 2.6 verwenden - Indizes auf Spalten hinzufügen, die in WHERE-Klauseln und JOIN-Bedingungen verwendet werden - Flow von DAOs zurückgeben für automatische UI-Updates bei Datenänderungen - Explizite Migrationen für Schema-Änderungen schreiben — niemals destruktive Migration in der Produktion verwenden - `@Transaction` für Relationsabfragen verwenden, um konsistente Lesevorgänge zu gewährleisten - Datenbankoperationen in Result wrappen für sauberes Error-Handling in ViewModels - Migrationen mit `MigrationTestHelper` vor der Veröffentlichung von Updates testen Diese Patterns können mit [Room-Datenbank-Interviewfragen](/technologies/android/interview-questions/android-room-database) vertieft werden. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/android/room-database-android-2026-migrations-relations-coroutines