# Room Database w Androidzie 2026: Migracje, Relacje i Współbieżność z Coroutines > Kompleksowy przewodnik po Room Database w Androidzie 2026 - od migracji schematu, przez relacje między encjami, po integrację z Kotlin Coroutines i najlepsze praktyki. - Published: 2026-07-10 - Updated: 2026-07-10 - Author: SharpSkill - Tags: android, room, kotlin, coroutines, database - Reading time: 12 min --- Room Database pozostaje fundamentalnym narzędziem do lokalnego przechowywania danych w aplikacjach Android. W 2026 roku biblioteka ta oferuje jeszcze więcej możliwości, włączając w to ulepszoną obsługę migracji, zaawansowane relacje między encjami oraz pełną integrację z Kotlin Coroutines. Ten artykuł przedstawia kompleksowe podejście do wykorzystania Room w nowoczesnych projektach Android. > Room Database jest częścią Android Jetpack i stanowi warstwę abstrakcji nad SQLite, eliminując potrzebę pisania boilerplate code oraz zapewniając weryfikację zapytań SQL w czasie kompilacji. ## Konfiguracja Room w projekcie Android 2026 Rozpoczęcie pracy z Room wymaga dodania odpowiednich zależności do projektu. W 2026 roku zaleca się korzystanie z najnowszych wersji biblioteki wraz z KSP (Kotlin Symbol Processing) dla lepszej wydajności kompilacji. ```kotlin // build.gradle.kts (Module) 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") } ``` Po skonfigurowaniu zależności można przystąpić do definiowania encji, które reprezentują tabele w bazie danych. ## Definiowanie encji i podstawowych operacji Encje w Room to klasy danych oznaczone adnotacją `@Entity`. Każda encja odpowiada tabeli w bazie SQLite. ```kotlin @Entity(tableName = "users") data class User( @PrimaryKey(autoGenerate = true) val id: Long = 0, @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String, @ColumnInfo(name = "email") val email: String, @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis() ) ``` DAO (Data Access Object) definiuje metody dostępu do danych. Room generuje implementację tych interfejsów automatycznie. ```kotlin @Dao interface UserDao { @Query("SELECT * FROM users ORDER BY created_at DESC") suspend fun getAllUsers(): List @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserById(userId: Long): User? @Query("SELECT * FROM users WHERE email = :email LIMIT 1") suspend fun getUserByEmail(email: String): User? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUser(user: User): Long @Update suspend fun updateUser(user: User) @Delete suspend fun deleteUser(user: User) @Query("DELETE FROM users") suspend fun deleteAllUsers() } ``` ## Migracje schematu bazy danych Migracje są kluczowym elementem zarządzania ewolucją schematu bazy danych. Room wymaga jawnego zdefiniowania migracji przy każdej zmianie wersji bazy. ```kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT") } } val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(""" CREATE TABLE IF NOT EXISTS user_preferences ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER NOT NULL, theme TEXT NOT NULL DEFAULT 'system', notifications_enabled INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """.trimIndent()) db.execSQL("CREATE INDEX index_user_preferences_user_id ON user_preferences(user_id)") } } ``` W przypadku skomplikowanych migracji, takich jak zmiana typu kolumny lub reorganizacja danych, konieczne jest zastosowanie strategii z tabelą tymczasową. ```kotlin val MIGRATION_3_4 = object : Migration(3, 4) { override fun migrate(db: SupportSQLiteDatabase) { // Tworzenie nowej tabeli z poprawionym schematem db.execSQL(""" CREATE TABLE users_new ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL, email TEXT NOT NULL, phone_number TEXT, created_at INTEGER NOT NULL, is_verified INTEGER NOT NULL DEFAULT 0 ) """.trimIndent()) // Kopiowanie danych ze starej tabeli db.execSQL(""" INSERT INTO users_new (id, first_name, last_name, email, phone_number, created_at) SELECT id, first_name, last_name, email, phone_number, created_at FROM users """.trimIndent()) // Usunięcie starej tabeli db.execSQL("DROP TABLE users") // Zmiana nazwy nowej tabeli db.execSQL("ALTER TABLE users_new RENAME TO users") // Odtworzenie indeksów db.execSQL("CREATE UNIQUE INDEX index_users_email ON users(email)") } } ``` ## Auto-migracje w Room 2.7 Room 2.7 wprowadza uproszczone auto-migracje dla prostych zmian schematu. System automatycznie wykrywa dodanie nowych kolumn lub tabel. ```kotlin @Database( version = 5, entities = [User::class, UserPreferences::class, Article::class], autoMigrations = [ AutoMigration(from = 4, to = 5) ], exportSchema = true ) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun articleDao(): ArticleDao } ``` Dla bardziej złożonych auto-migracji można zdefiniować specyfikację migracji. ```kotlin @Database( version = 6, entities = [User::class, UserPreferences::class, Article::class], autoMigrations = [ AutoMigration(from = 5, to = 6, spec = Migration5To6::class) ] ) abstract class AppDatabase : RoomDatabase() { @RenameColumn(tableName = "users", fromColumnName = "phone_number", toColumnName = "mobile_phone") @DeleteColumn(tableName = "users", columnName = "legacy_field") class Migration5To6 : AutoMigrationSpec } ``` ## Relacje między encjami Room obsługuje różne typy relacji między encjami: jeden-do-jednego, jeden-do-wielu oraz wiele-do-wielu. ### Relacja jeden-do-wielu ```kotlin @Entity(tableName = "articles") data class Article( @PrimaryKey(autoGenerate = true) val id: Long = 0, val authorId: Long, val title: String, val content: String, val publishedAt: Long = System.currentTimeMillis() ) data class UserWithArticles( @Embedded val user: User, @Relation( parentColumn = "id", entityColumn = "authorId" ) val articles: List
) @Dao interface UserDao { @Transaction @Query("SELECT * FROM users WHERE id = :userId") suspend fun getUserWithArticles(userId: Long): UserWithArticles? @Transaction @Query("SELECT * FROM users") fun getAllUsersWithArticles(): Flow> } ``` ### Relacja wiele-do-wielu ```kotlin @Entity(tableName = "tags") data class Tag( @PrimaryKey(autoGenerate = true) val tagId: Long = 0, val name: String ) @Entity( tableName = "article_tag_cross_ref", primaryKeys = ["articleId", "tagId"], foreignKeys = [ ForeignKey( entity = Article::class, parentColumns = ["id"], childColumns = ["articleId"], onDelete = ForeignKey.CASCADE ), ForeignKey( entity = Tag::class, parentColumns = ["tagId"], childColumns = ["tagId"], onDelete = ForeignKey.CASCADE ) ] ) data class ArticleTagCrossRef( val articleId: Long, val tagId: Long ) data class ArticleWithTags( @Embedded val article: Article, @Relation( parentColumn = "id", entityColumn = "tagId", associateBy = Junction(ArticleTagCrossRef::class) ) val tags: List ) ``` ## Integracja z Kotlin Coroutines i Flow Room oferuje natywną integrację z Kotlin Coroutines, umożliwiając reaktywne obserwowanie zmian w bazie danych. ```kotlin @Dao interface ArticleDao { // Jednorazowe pobranie danych @Query("SELECT * FROM articles ORDER BY publishedAt DESC") suspend fun getAllArticles(): List
// Reaktywne Flow - automatycznie emituje przy zmianach @Query("SELECT * FROM articles ORDER BY publishedAt DESC") fun observeAllArticles(): Flow> // Flow z parametrem @Query("SELECT * FROM articles WHERE authorId = :authorId ORDER BY publishedAt DESC") fun observeArticlesByAuthor(authorId: Long): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertArticle(article: Article): Long @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertArticles(articles: List
) @Query("DELETE FROM articles WHERE id = :articleId") suspend fun deleteArticleById(articleId: Long) } ``` W ViewModel dane z Flow można przekształcić w StateFlow dla łatwiejszego zarządzania stanem UI. ```kotlin class ArticleViewModel( private val articleDao: ArticleDao ) : ViewModel() { val articles: StateFlow> = articleDao .observeAllArticles() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading.asStateFlow() fun addArticle(title: String, content: String, authorId: Long) { viewModelScope.launch { _isLoading.value = true try { val article = Article( title = title, content = content, authorId = authorId ) articleDao.insertArticle(article) } finally { _isLoading.value = false } } } } ``` ## Transakcje i operacje wsadowe Room wspiera transakcje dla operacji wymagających atomowości. Adnotacja `@Transaction` zapewnia wykonanie wszystkich operacji w jednej transakcji. ```kotlin @Dao interface ArticleDao { @Transaction suspend fun replaceAllArticles(articles: List
) { deleteAllArticles() insertArticles(articles) } @Query("DELETE FROM articles") suspend fun deleteAllArticles() @Insert suspend fun insertArticles(articles: List
) } ``` Dla bardziej złożonych scenariuszy można wykorzystać `withTransaction` z obiektu bazy danych. ```kotlin class ArticleRepository( private val database: AppDatabase, private val articleDao: ArticleDao, private val tagDao: TagDao ) { suspend fun createArticleWithTags( article: Article, tagNames: List ): Long = database.withTransaction { val articleId = articleDao.insertArticle(article) tagNames.forEach { tagName -> val existingTag = tagDao.getTagByName(tagName) val tagId = existingTag?.tagId ?: tagDao.insertTag(Tag(name = tagName)) tagDao.insertArticleTagCrossRef( ArticleTagCrossRef(articleId = articleId, tagId = tagId) ) } articleId } } ``` ## Testowanie Room Database Room oferuje wsparcie dla testowania z wykorzystaniem bazy danych w pamięci. ```kotlin @RunWith(AndroidJUnit4::class) class UserDaoTest { private lateinit var database: AppDatabase private lateinit var userDao: UserDao @Before fun setup() { val context = ApplicationProvider.getApplicationContext() database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) .allowMainThreadQueries() .build() userDao = database.userDao() } @After fun teardown() { database.close() } @Test fun insertAndRetrieveUser() = runTest { val user = User( firstName = "Jan", lastName = "Kowalski", email = "jan@example.com" ) val userId = userDao.insertUser(user) val retrievedUser = userDao.getUserById(userId) assertThat(retrievedUser).isNotNull() assertThat(retrievedUser?.firstName).isEqualTo("Jan") assertThat(retrievedUser?.email).isEqualTo("jan@example.com") } @Test fun testMigration() = runTest { val helper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java ) // Tworzenie bazy w wersji 1 helper.createDatabase(TEST_DB, 1).apply { execSQL("INSERT INTO users (first_name, last_name, email, created_at) VALUES ('Test', 'User', 'test@example.com', 0)") close() } // Migracja do wersji 2 helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2) } companion object { private const val TEST_DB = "test-database" } } ``` ## Najczęstsze pytania rekrutacyjne dotyczące Room Podczas rozmów kwalifikacyjnych na stanowiska Android Developer często pojawiają się pytania dotyczące Room Database: **Czym różni się Room od SQLite?** Room jest warstwą abstrakcji nad SQLite, która oferuje weryfikację zapytań SQL w czasie kompilacji, automatyczne generowanie kodu DAO oraz natywną integrację z Kotlin Coroutines i LiveData/Flow. **Jak Room obsługuje wątkowość?** Room domyślnie wymaga wykonywania operacji bazodanowych poza głównym wątkiem. Dzięki integracji z Coroutines, metody DAO oznaczone jako `suspend` automatycznie wykonują się w odpowiednim kontekście. **Kiedy używać auto-migracji a kiedy ręcznych migracji?** Auto-migracje sprawdzają się przy prostych zmianach jak dodanie kolumny lub tabeli. Ręczne migracje są niezbędne przy zmianie typu danych, reorganizacji tabel lub transformacji istniejących danych. ## Podsumowanie Room Database w 2026 roku oferuje dojrzałe i stabilne rozwiązanie do lokalnego przechowywania danych w aplikacjach Android. Kluczowe aspekty to prawidłowe zarządzanie migracjami schematu, efektywne modelowanie relacji między encjami oraz wykorzystanie Kotlin Coroutines do reaktywnego dostępu do danych. Znajomość tych zagadnień jest niezbędna dla każdego programisty Android pracującego nad aplikacjami wymagającymi lokalnej bazy danych. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/android/room-database-android-2026-migrations-relations-coroutines