Room Database в Android 2026: Міграції, Зв'язки та Coroutines

Повний посібник з Room Database в Android 2026 - від міграцій схеми, через зв'язки між сутностями, до інтеграції з Kotlin Coroutines та найкращих практик.

Room Database в Android - міграції, зв'язки та coroutines

Room Database залишається фундаментальним інструментом для локального зберігання даних у додатках Android. У 2026 році ця бібліотека пропонує ще більше можливостей, включаючи покращену підтримку міграцій, розширені зв'язки між сутностями та повну інтеграцію з Kotlin Coroutines. Ця стаття представляє комплексний підхід до використання Room у сучасних проектах Android.

Room Database є частиною Android Jetpack і становить рівень абстракції над SQLite, усуваючи потребу писати шаблонний код та забезпечуючи перевірку SQL-запитів під час компіляції.

Налаштування Room у проекті Android 2026

Для початку роботи з Room необхідно додати відповідні залежності до проекту. У 2026 році рекомендується використовувати найновіші версії бібліотеки разом з KSP (Kotlin Symbol Processing) для кращої продуктивності компіляції.

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")
}

Після налаштування залежностей можна приступати до визначення сутностей, які представляють таблиці в базі даних.

Визначення сутностей та базових операцій

Сутності в Room — це класи даних, позначені анотацією @Entity. Кожна сутність відповідає таблиці в базі 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) визначає методи доступу до даних. Room автоматично генерує реалізацію цих інтерфейсів.

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()
}

Міграції схеми бази даних

Міграції є ключовим елементом управління еволюцією схеми бази даних. Room вимагає явного визначення міграцій при кожній зміні версії бази.

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)")
    }
}

У випадку складних міграцій, таких як зміна типу колонки або реорганізація даних, необхідно застосувати стратегію з тимчасовою таблицею.

kotlin
val MIGRATION_3_4 = object : Migration(3, 4) {
    override fun migrate(db: SupportSQLiteDatabase) {
        // Створення нової таблиці з виправленою схемою
        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())
        
        // Копіювання даних зі старої таблиці
        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())
        
        // Видалення старої таблиці
        db.execSQL("DROP TABLE users")
        
        // Перейменування нової таблиці
        db.execSQL("ALTER TABLE users_new RENAME TO users")
        
        // Відновлення індексів
        db.execSQL("CREATE UNIQUE INDEX index_users_email ON users(email)")
    }
}

Автоматичні міграції в Room 2.7

Room 2.7 впроваджує спрощені автоматичні міграції для простих змін схеми. Система автоматично виявляє додавання нових колонок або таблиць.

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
}

Для складніших автоматичних міграцій можна визначити специфікацію міграції.

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
}

Зв'язки між сутностями

Room підтримує різні типи зв'язків між сутностями: один-до-одного, один-до-багатьох та багато-до-багатьох.

Зв'язок один-до-багатьох

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>>
}

Зв'язок багато-до-багатьох

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

Інтеграція з Kotlin Coroutines та Flow

Room пропонує нативну інтеграцію з Kotlin Coroutines, дозволяючи реактивно спостерігати за змінами в базі даних.

kotlin
@Dao
interface ArticleDao {
    // Одноразове отримання даних
    @Query("SELECT * FROM articles ORDER BY publishedAt DESC")
    suspend fun getAllArticles(): List<Article>
    
    // Реактивний Flow - автоматично емітує при змінах
    @Query("SELECT * FROM articles ORDER BY publishedAt DESC")
    fun observeAllArticles(): Flow<List<Article>>
    
    // Flow з параметром
    @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)
}

У ViewModel дані з Flow можна перетворити в StateFlow для легшого управління станом 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
            }
        }
    }
}

Транзакції та пакетні операції

Room підтримує транзакції для операцій, що вимагають атомарності. Анотація @Transaction забезпечує виконання всіх операцій в одній транзакції.

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

Для складніших сценаріїв можна використовувати withTransaction з об'єкта бази даних.

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
    }
}

Тестування Room Database

Room пропонує підтримку тестування з використанням бази даних у пам'яті.

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 = "Олександр",
            lastName = "Петренко",
            email = "oleksandr@example.com"
        )
        
        val userId = userDao.insertUser(user)
        val retrievedUser = userDao.getUserById(userId)
        
        assertThat(retrievedUser).isNotNull()
        assertThat(retrievedUser?.firstName).isEqualTo("Олександр")
        assertThat(retrievedUser?.email).isEqualTo("oleksandr@example.com")
    }
    
    @Test
    fun testMigration() = runTest {
        val helper = MigrationTestHelper(
            InstrumentationRegistry.getInstrumentation(),
            AppDatabase::class.java
        )
        
        // Створення бази у версії 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()
        }
        
        // Міграція до версії 2
        helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)
    }
    
    companion object {
        private const val TEST_DB = "test-database"
    }
}

Готовий до співбесід з Android?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Найчастіші питання на співбесідах щодо Room

Під час співбесід на позиції Android Developer часто з'являються питання про Room Database:

Чим Room відрізняється від SQLite? Room є рівнем абстракції над SQLite, який пропонує перевірку SQL-запитів під час компіляції, автоматичну генерацію коду DAO та нативну інтеграцію з Kotlin Coroutines і LiveData/Flow.

Як Room обробляє багатопотоковість? Room за замовчуванням вимагає виконання операцій з базою даних поза головним потоком. Завдяки інтеграції з Coroutines методи DAO, позначені як suspend, автоматично виконуються у відповідному контексті.

Коли використовувати автоматичні міграції, а коли ручні? Автоматичні міграції підходять для простих змін, як-от додавання колонки або таблиці. Ручні міграції необхідні при зміні типу даних, реорганізації таблиць або трансформації існуючих даних.

Висновок

Room Database у 2026 році пропонує зрілі та стабільні рішення для локального зберігання даних у додатках Android. Ключові аспекти включають правильне управління міграціями схеми, ефективне моделювання зв'язків між сутностями та використання Kotlin Coroutines для реактивного доступу до даних. Знання цих питань є необхідним для кожного розробника Android, який працює над додатками, що вимагають локальної бази даних.

Теги

#android
#room
#kotlin
#coroutines
#database

Поділитися

Пов'язані статті