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.

Room Database w Androidzie - migracje, relacje i coroutines

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.

build.gradle.kts (Module)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")
}

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<User>
    
    @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<Article>
)

@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<List<UserWithArticles>>
}

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<Tag>
)

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<Article>
    
    // Reaktywne Flow - automatycznie emituje przy zmianach
    @Query("SELECT * FROM articles ORDER BY publishedAt DESC")
    fun observeAllArticles(): Flow<List<Article>>
    
    // Flow z parametrem
    @Query("SELECT * FROM articles WHERE authorId = :authorId ORDER BY publishedAt DESC")
    fun observeArticlesByAuthor(authorId: Long): Flow<List<Article>>
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertArticle(article: Article): Long
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertArticles(articles: List<Article>)
    
    @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<List<Article>> = articleDao
        .observeAllArticles()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
    
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _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<Article>) {
        deleteAllArticles()
        insertArticles(articles)
    }
    
    @Query("DELETE FROM articles")
    suspend fun deleteAllArticles()
    
    @Insert
    suspend fun insertArticles(articles: List<Article>)
}

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<String>
    ): 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<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(
            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"
    }
}

Gotowy na rozmowy o Android?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

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.

Tagi

#android
#room
#kotlin
#coroutines
#database

Udostępnij

Powiązane artykuły