Користувач оновив застосунок — і при першому запуску бачить білий екран або крэш. У Logcat: IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. Або гірше: Migration didn't properly handle з втратою всіх локальних даних. Це класика неправильно реалізованої міграції Room. Наш досвід — 7+ років в Android-розробці, понад 30 проєктів з міграціями різної складності. Ми допоможемо уникнути таких ситуацій. У цій статті розберемо, як правильно виконати міграцію схеми бази даних Room, які типи змін бувають і як їх тестувати.
За статистикою, близько 30% проєктів стикаються з помилками міграції, що призводять до втрати даних. Своєчасне тестування знижує цей ризик на 80%. Правильна міграція економить до 50% часу на налагодження.
Міграція Room: як уникнути помилок?
Як Room визначає необхідність міграції?
Room зберігає хеш схеми бази даних. При кожному запуску порівнює хеш скомпільованого @Database з хешем, що зберігається в room_master_table. Якщо вони не збігаються — Room кидає виняток, якщо не знайдено відповідної міграції. version в @Database — це контракт: якщо схема змінилася, version має бути збільшений, і додана явна Migration(fromVersion, toVersion). Room керує версіонуванням автоматично, але тільки за умови коректно описаних переходів.
Типи змін та їх міграція
Додавання колонки (простий випадок)
val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE transactions ADD COLUMN category TEXT NOT NULL DEFAULT ''") } } NOT NULL DEFAULT '' — обов'язково. SQLite не дозволяє додати NOT NULL колонку без DEFAULT у вже існуючу таблицю з даними.
Перейменування колонки
SQLite не підтримує ALTER TABLE RENAME COLUMN до версії 3.25.0. На Android API < 29 це недоступно. Універсальний шлях — перестворення таблиці:
val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(""" CREATE TABLE transactions_new ( id TEXT NOT NULL PRIMARY KEY, amount REAL NOT NULL, description TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL ) """) db.execSQL(""" INSERT INTO transactions_new (id, amount, description, created_at) SELECT id, amount, note, created_at FROM transactions """) db.execSQL("DROP TABLE transactions") db.execSQL("ALTER TABLE transactions_new RENAME TO transactions") } } Додавання таблиці з зовнішнім ключем
Створіть таблиці через CREATE TABLE IF NOT EXISTS. Зовнішні ключі вмикаються після завершення міграції — Room керує foreign_keys автоматично.
Що таке ланцюжки міграцій та як їх використовувати?
Room може застосовувати міграції послідовно. Якщо користувач перескочив з версії 1 на 4, Room виконає MIGRATION_1_2, потім MIGRATION_2_3, потім MIGRATION_3_4 — за умови, що всі вони зареєстровані.
Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) .build() Для прискорення можна додати прямий Migration(1,4), що виконує всі зміни за один прохід.
Тестування міграцій через MigrationTestHelper
Кожну міграцію необхідно тестувати. Room надає MigrationTestHelper для JUnit. Інструмент дозволяє автоматизувати перевірку в 10 разів швидше, ніж ручне тестування. Приклад для міграції 1→2:
@RunWith(AndroidJUnit4::class) class MigrationTest { @get:Rule val helper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java ) @Test fun migrate1To2() { helper.createDatabase(TEST_DB, 1).apply { execSQL("INSERT INTO transactions VALUES ('id1', 100.0, 'test', 1700000000)") close() } val db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2) val cursor = db.query("SELECT category FROM transactions WHERE id = 'id1'") assertTrue(cursor.moveToFirst()) assertEquals("", cursor.getString(0)) } } Без тестів ви ризикуєте втратити користувацькі дані. Ми завжди включаємо тести в обсяг робіт.
Експорт JSON-схем для валідації
Додайте в build.gradle анотацію:
android { defaultConfig { javaCompileOptions { annotationProcessorOptions { arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] } } } } Room генерує schemas/1.json, schemas/2.json — знімки схеми кожної версії. Ці файли потрібно комітити в репозиторій. Без них MigrationTestHelper не зможе валідувати міграції.
| Тип зміни | Опис | Ризик втрати даних | Необхідність тесту |
|---|---|---|---|
| Додавання колонки | ALTER TABLE ADD COLUMN | Низький | Так |
| Перейменування колонки | Перестворення таблиці | Середній | Обов'язковий |
| Додавання таблиці | CREATE TABLE | Низький | Так |
| Видалення колонки | Перестворення таблиці | Високий | Обов'язковий |
| Destructive migration | Database.delete() | Повна | Ні (не для production) |
Порівняння стратегій міграції
| Стратегія | Швидкість | Надійність | Складність |
|---|---|---|---|
| ALTER TABLE ADD COLUMN | Миттєво | Висока | Низька |
| Перестворення таблиці | Середня | Висока | Середня |
| Destructive migration | Миттєво | Низька | Нульова |
Як уникнути втрати даних при міграції?
Завжди тестуйте кожну міграцію на реальних або синтетичних даних. Використовуйте повний набір кейсів: порожні таблиці, записи з NULL, дублікати. Обов'язково перевіряйте індекси та тригери після міграції. Додатково рекомендуємо робити резервне копіювання перед оновленням.
Fallback на destructive migration
У крайньому випадку — тільки для debug-складання або за явною згодою користувача: fallbackToDestructiveMigration() стирає всі дані. У production це неприпустимо.
Міграція схеми Room: покрокове налаштування
- Визначте зміни схеми та збільшіть version в
@Database. - Напишіть об'єкт Migration з SQL-командами.
- Зареєструйте міграцію в
addMigrations(). - Налаштуйте експорт JSON-схем у build.gradle.
- Створіть JUnit-тест з MigrationTestHelper.
- Запустіть тест і переконайтеся в коректності.
Що входить в роботу
- Аудит поточної схеми та історії версій
- Написання Migration об'єктів для всіх змін
- Тести через MigrationTestHelper для кожної міграції
- Налаштування експорту JSON-схем
- Обробка edge cases: порожні таблиці, зовнішні ключі, індекси, тригери
Строки
1–2 прості міграції (додавання колонок): 0,5–1 день. Складна реструктуризація з повним покриттям тестами: 2–3 дні. Вартість розраховується індивідуально — зв'яжіться з нами для оцінки вашого проекту. Отримайте консультацію з міграції Room вже сьогодні. Замовте детальний аудит бази даних.







