Реалізація міграції схеми бази даних (Room Migration) в Android-додатках
Користувач оновив додаток — і при першому запуску бачить білий екран або крах. У 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.
Як Room виявляє зміни схеми
Room зберігає хеш схеми бази даних. При кожному запуску порівнює хеш скомпільованого @Database з хешем, збереженим у room_master_table. Якщо вони не збігаються — Room викидає виключення, якщо не знайдена підходящої міграція.
version у @Database — це не довільний номер. Це контракт: якщо схема змінилася, version повинен бути збільшений, і повинна бути додана явна Migration(fromVersion, toVersion).
Типи змін та їхня міграція
Додавання колонки (простий випадок)
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) {
// 1. Створюємо нову таблицю з правильним ім'ям колонки
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
)
""")
// 2. Копіюємо дані (стара колонка 'note' → нова 'description')
db.execSQL("""
INSERT INTO transactions_new (id, amount, description, created_at)
SELECT id, amount, note, created_at FROM transactions
""")
// 3. Видаляємо старту
db.execSQL("DROP TABLE transactions")
// 4. Переименовуємо нову
db.execSQL("ALTER TABLE transactions_new RENAME TO transactions")
}
}
Пересоздання таблиці — єдиний надійний шлях для будь-яких структурних змін на всьому діапазоні Android API.
Додавання таблиці з зовнішнім ключем
db.execSQL("""
CREATE TABLE IF NOT EXISTS tags (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
color INTEGER NOT NULL DEFAULT 0
)
""")
db.execSQL("""
CREATE TABLE IF NOT EXISTS transaction_tags (
transaction_id TEXT NOT NULL,
tag_id TEXT NOT NULL,
PRIMARY KEY (transaction_id, tag_id),
FOREIGN KEY (transaction_id) REFERENCES transactions(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
)
""")
Ланцюги міграцій та пропущені версії
Room може будувати ланцюги: якщо користувач не оновлював додаток із v1 на v3, Room застосує Migration(1,2) + Migration(2,3). Але це працює лише якщо ви зареєстрували всі проміжні міграції.
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.build()
Якщо хочете підтримати прямий перехід 1→4 (швидше, одна SQL-операція) — додайте Migration(1, 4) явно.
Тестування міграцій
Room надає MigrationTestHelper для JUnit тестів:
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun migrate1To2() {
// Створюємо базу версії 1
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 description FROM transactions WHERE id = 'id1'")
assertTrue(cursor.moveToFirst())
assertEquals("", cursor.getString(0))
}
}
Тест на кожну міграцію — не опція, а обов'язковість. MigrationTestHelper.runMigrationsAndValidate валідує кінцеву схему проти очікуваної.
Експортовані JSON-схеми
Включіть експорт схеми у build.gradle:
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
}
Room зберігає schemas/1.json, schemas/2.json — знімки схеми кожної версії. Їх потрібно коммітити в репозиторій. MigrationTestHelper використовує їх для валідації. Без цих файлів — тестування міграцій неможливо.
Fallback на destructive migration
У крайньому випадку — тільки для dev-збірок або при явній згоді користувача:
.fallbackToDestructiveMigration() // стирає всі дані та пересоздає базу
У production це недопустимо без попередження користувача.
Обсяг роботи
- Аудит поточної схеми та історії версій
- Написання
Migrationоб'єктів для всіх змін схеми - Тести через
MigrationTestHelperдля кожної міграції - Налаштування експорту JSON-схем
- Обробка edge cases: пусті таблиці, зовнішні ключі, індекси, тригери
Строки
1–2 прості міграції (додавання колонок): 0,5–1 день. Складна реструктуризація (переименування, пересоздання таблиць, ланцюги міграцій) з повним покриттям тестами: 2–3 дні.







