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

Room Database залишається фундаментальним інструментом для локального зберігання даних у додатках Android. У 2026 році ця бібліотека пропонує ще більше можливостей, включаючи покращену підтримку міграцій, розширені зв'язки між сутностями та повну інтеграцію з Kotlin Coroutines. Ця стаття представляє комплексний підхід до використання Room у сучасних проектах Android.
Room Database є частиною Android Jetpack і становить рівень абстракції над SQLite, усуваючи потребу писати шаблонний код та забезпечуючи перевірку SQL-запитів під час компіляції.
Налаштування Room у проекті Android 2026
Для початку роботи з Room необхідно додати відповідні залежності до проекту. У 2026 році рекомендується використовувати найновіші версії бібліотеки разом з KSP (Kotlin Symbol Processing) для кращої продуктивності компіляції.
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.
@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 автоматично генерує реалізацію цих інтерфейсів.
@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 вимагає явного визначення міграцій при кожній зміні версії бази.
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)")
}
}У випадку складних міграцій, таких як зміна типу колонки або реорганізація даних, необхідно застосувати стратегію з тимчасовою таблицею.
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 впроваджує спрощені автоматичні міграції для простих змін схеми. Система автоматично виявляє додавання нових колонок або таблиць.
@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
}Для складніших автоматичних міграцій можна визначити специфікацію міграції.
@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 підтримує різні типи зв'язків між сутностями: один-до-одного, один-до-багатьох та багато-до-багатьох.
Зв'язок один-до-багатьох
@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>>
}Зв'язок багато-до-багатьох
@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, дозволяючи реактивно спостерігати за змінами в базі даних.
@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.
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 забезпечує виконання всіх операцій в одній транзакції.
@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 з об'єкта бази даних.
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 пропонує підтримку тестування з використанням бази даних у пам'яті.
@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, який працює над додатками, що вимагають локальної бази даних.
Теги
Поділитися
Пов'язані статті

Kotlin Flow проти StateFlow проти SharedFlow: питання на Android-співбесіді у 2026
Питання про Kotlin Flow проти StateFlow проти SharedFlow, які Android-інтерв'юери ставлять у 2026 році, з чіткими відповідями, таблицею порівняння та готовим до продакшену кодом.

Kotlin Coroutines: Повний посібник 2026 для Android
Опануйте Kotlin coroutines для Android-розробки: suspend-функції, скоупи, диспатчери та просунуті патерни.

Kotlin 2.3 для Android: іменована деструктуризація, KMP та співбесідні питання 2026
Питання на співбесіду з Kotlin 2.3, що охоплюють іменовану деструктуризацію, Kotlin Multiplatform, контекстні параметри, корутини та Flow. Підготовка до співбесід Android-розробника у 2026 з реальними прикладами коду.