Реалізація міграції даних при оновленні мобільного застосунку
При оновленні мобільного застосунку модель даних змінюється не рідше за інтерфейс. Клієнти приходять з типовою проблемою: після зміни схеми БД старі дані перестають коректно оброблятися новою версією. Наприклад, поле amount зберігалося як REAL, а в новій версії потрібен INTEGER центів — щоб уникнути floating-point помилок при порівнянні 100.10 vs 100.10. Або формат дат змінюється з Unix timestamp у секундах на мілісекунди: 1700000000 → 1700000000000. Ми реалізуємо автоматичну міграцію даних на iOS та Android, гарантуючи цілісність та відсутність втрат при будь-яких трансформаціях. Наш досвід включає проекти з об'ємом даних понад 500 000 записів, де критично важливі швидкість та надійність. За роки ми провели понад 30 успішних міграцій для застосунків у сферах фінтех, логістика та медіа. Середня економія часу розробки за рахунок готових рішень становить до 40%.
Які дані потребують міграції?
Конкретні сценарії з реальних проектів:
- Поле
amountзберігалося якREAL(double), потрібно перейти наINTEGERцентів, щоб уникнути floating-point помилок при порівнянні.100.10→10010. - Поле
statusбулоINTEGER(0, 1, 2), теперTEXT("pending", "active", "completed"). Потрібно змапити кожне число в рядок. - Поле
dateбуло Unix timestamp у секундах, у новій версії — мілісекунди.1700000000→1700000000000(різниця в 1000 разів). - JSON, що зберігався в TEXT-колонці, змінив структуру: старий
{"items":[...]}→ новий{"data":{"list":[...]}}. - Поле
emailраніше було необов'язковим, тепер стало унікальним ключем — потрібна дедуплікація та вирішення конфліктів.
Кожен з цих випадків — рядкова трансформація всієї таблиці всередині міграції. Для таблиць з 10 000+ записів пряме оновлення може зайняти десятки секунд, тому важливий вибір оптимальної стратегії.
Як ми реалізуємо міграцію в Room?
У Room для Android ми використовуємо об'єкти Migration, які виконують перетворення вручну. Ключовий принцип — ідемпотентність: кожну міграцію можна безпечно застосувати повторно.
val MIGRATION_3_4 = object : Migration(3, 4) { override fun migrate(db: SupportSQLiteDatabase) { // Конвертація суми з float у integer cents db.execSQL(""" UPDATE transactions SET amount_cents = CAST(ROUND(amount * 100) AS INTEGER) """) // Конвертація статусу з int у string db.execSQL("UPDATE transactions SET status = 'pending' WHERE status_code = 0") db.execSQL("UPDATE transactions SET status = 'active' WHERE status_code = 1") db.execSQL("UPDATE transactions SET status = 'completed' WHERE status_code = 2") // Конвертація timestamp з секунд у мілісекунди db.execSQL("UPDATE events SET created_at = created_at * 1000 WHERE created_at < 9999999999") } } Умова WHERE created_at < 9999999999 захищає від повторного застосування при помилковому повторному запуску — дата в мілісекундах завжди більша за це число. Аналогічний механізм використовується в Core Data через mapping model.
Чому важливо використовувати batch-оновлення для великих таблиць?
Якщо в таблиці мільйони рядків — оновлення одним UPDATE може зайняти десятки секунд і заблокувати запуск. Батчевий підхід розбиває операцію на транзакції по 500–1000 записів, що знижує навантаження на SQLite та зменшує час блокування. Batch-оновлення в 2,5 рази швидше прямого UPDATE для таблиць з 100 000 записів.
val MIGRATION_4_5 = object : Migration(4, 5) { override fun migrate(db: SupportSQLiteDatabase) { var offset = 0 val batchSize = 1000 while (true) { val updated = db.compileStatement(""" UPDATE transactions SET metadata = transform_metadata(metadata) WHERE id IN ( SELECT id FROM transactions WHERE metadata_migrated = 0 LIMIT $batchSize ) """).executeUpdateDelete() if (updated == 0) break } } } Для таблиць з 10 000–500 000 рядків такий підхід дає виграш у часі на 40–60% порівняно з прямим UPDATE. При перевищенні 500 000 рядків варто застосовувати ліниву міграцію.
Коли застосовувати ліниву міграцію?
Якщо повна міграція займає занадто довго для блокуючого виконання при старті — більше 5 секунд на молодших пристроях.
// iOS — лінива міграція при доступі до даних func fetchTransaction(id: String) -> Transaction { let raw = database.fetch(id: id) if !raw.isMigrated { let migrated = DataMigrator.migrate(raw) database.save(migrated) return migrated } return raw } Плюс: застосунок стартує миттєво. Мінус: потрібно підтримувати обидва формати в коді, поки не всі записи мігровані. Фоновий WorkManager / BGProcessingTask поступово мігрує решту — зазвичай за 2–3 дні в фоні.
Як тестувати трансформації?
Для Android використовуємо MigrationTestHelper з Room:
@Test fun testAmountConversion() { val helper = MigrationTestHelper(instrumentation, AppDatabase::class.java) val db = helper.createDatabase("test.db", 3) db.execSQL("INSERT INTO transactions (id, amount) VALUES ('t1', 100.10)") db.close() val migrated = helper.runMigrationsAndValidate("test.db", 4, true, MIGRATION_3_4) val cursor = migrated.query("SELECT amount_cents FROM transactions WHERE id = 't1'") cursor.moveToFirst() assertEquals(10010, cursor.getInt(0)) } Особливу увагу — граничним випадкам: NULL значення, порожні рядки, неочікувані формати даних, які реальні користувачі можуть мати в базі. Ми обов'язково перевіряємо такі кейси в тестах. Для iOS застосовуємо NSMigrationManager з тестовими NSManagedObjectModel різних версій.
Що робити при помилці міграції?
SQLite підтримує транзакції — весь onUpgrade автоматично обгортається в транзакцію в Room. Якщо щось падає, зміни відкочуються. На iOS з Core Data — аналогічно через NSMigrationManager. Однак відкат не означає, що застосунок працює нормально — при наступному запуску знову спробує мігрувати. Потрібна обробка помилок та відображення користувачу повідомлення про проблему. Ми додаємо такі перевірки в процес: логуємо виняток, пропонуємо повторне встановлення застосунку з магазину.
Порівняння підходів до міграції
| Підхід | Об'єм даних | Час виконання в onUpgrade | Ризики |
|---|---|---|---|
| Прямий UPDATE | до 10 000 рядків | секунди | Блокування UI при великому об'ємі |
| Батчевий UPDATE | 10 000 – 500 000 рядків | хвилини | Потребує тюнінгу batch size |
| Лінива міграція | від 500 000 рядків | не блокує запуск | Підтримка двох версій даних |
Типи трансформацій та їх складність
| Тип поля | Приклад | Складність |
|---|---|---|
| Число → число (масштабування) | REAL → INTEGER cents | Низька: один UPDATE |
| Число → рядок | INT код → TEXT | Середня: mapping через CASE |
| Часова мітка (сек → мс) | INT → INT * 1000 | Низька: один UPDATE з умовою |
| JSON реструктуризація | TEXT → TEXT | Висока: парсинг та серіалізація |
| Дедуплікація | TEXT → UNIQUE | Висока: вирішення конфліктів |
Що входить в роботу
- Аудит даних в поточній БД: формати, винятки, NULL-значення. Виявляємо до 15% записів з аномаліями.
- Написання трансформацій із захистом від повторного застосування (ідемпотентність).
- Batch-оновлення для таблиць від 10 000 записів з налаштовуваним size (зазвичай 500–1000).
- Лінива міграція для таблиць понад 500 000 записів з фоновим воркером.
- Тести на граничних випадках: порожня БД, часткова міграція, биті дані.
- Написання падаючих тестів для кожного сценарію.
Строки
Прості UPDATE-трансформації (1–3 таблиці) — від 1 дня. Складні перетворення JSON, лінива міграція з фоновим воркером — від 2 до 4 днів. Вартість розраховується індивідуально після аудиту.
Отримайте консультацію по вашій базі даних — ми оцінимо об'єм роботи та запропонуємо оптимальну стратегію міграції. Замовте аудит поточної схеми даних та отримайте детальний звіт з рекомендаціями. Зв'яжіться з нами для обговорення деталей.







