Міграція схеми Realm у мобільному застосунку: як не втратити дані
Realm кидає Migration is required due to the following errors при невідповідності схеми. Якщо в застосунку з 10 000 користувачів не задати правильну міграцію, оновлення викличе краш у кожного десятого — і негативні відгуки в App Store гарантовані. На відміну від Room або Core Data, Realm вимагає явного зазначення поточної версії схеми в конфігурації — і якщо ви про це забули, застосунок впаде при першому зверненні до бази даних. Наш досвід показує, що правильно налаштована міграція з першого разу позбавляє від даунтаймів і скорочує час тестування на 30%.
Як Realm визначає необхідність міграції?
Версія схеми зберігається всередині .realm файлу. Realm порівнює версію у файлі з версією, вказаною в RealmConfiguration.schemaVersion. Якщо версії не збігаються і не задано migrationBlock — виняток. Розробники часто забувають збільшити schemaVersion — це призводить до падіння у 90% користувачів при оновленні. Вартість такого бага — втрата репутації та час на екстрений реліз.
// iOS — базова конфігурація з міграцією
let config = Realm.Configuration(
schemaVersion: 3,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 2 {
// Міграція 1 → 2: доданий атрибут category
migration.enumerateObjects(ofType: Transaction.className()) { old, new in
new?["category"] = ""
}
}
if oldSchemaVersion < 3 {
// Міграція 2 → 3: перейменування поля
migration.renameProperty(onType: Transaction.className(), from: "note", to: "description")
}
}
)
Realm.Configuration.defaultConfiguration = config
Згідно з офіційною документацією Realm, всі міграції від будь-якої старої версії до поточної виконуються в одному migrationBlock — Realm сам визначає, з якої версії починати. Якщо у вас 5 версій, а у користувача файл на версії 1, Realm застосує всі проміжні кроки послідовно.
Чому важливо правильно налаштувати schemaVersion?
Кожного разу при зміні моделі даних (додавання, видалення, перейменування полів) необхідно збільшувати schemaVersion хоча б на 1. Якщо цього не зробити, Realm викине виняток при спробі відкрити базу на пристрої користувача. Уявіть: ви випустили оновлення з новою версією моделі, але забули оновити версію схеми — всі користувачі (а їх може бути мільйон) не зможуть запустити застосунок. Вартість такого бага вимірюється не лише часом на виправлення, а й репутаційними втратами. Ми гарантуємо, що у вашому проєкті цього не станеться. Економія на підтримці після грамотної міграції окупається за 2–4 тижні.
Які типи операцій підтримуються в migrationBlock?
Додавання поля
Realm додає нові поля автоматично з default-значеннями — але тільки якщо поле optional або має дефолт у моделі. Для заповнення нестандартними значеннями — enumerateObjects. На Android (Realm Kotlin SDK) міграція налаштовується через AutomaticSchemaMigration:
// Android (Realm Kotlin SDK)
migration.iterate("Transaction") { oldObject, newObject ->
val oldCategory = oldObject.getNullableValue<String>("category")
newObject.set("category", oldCategory ?: "uncategorized")
}
Видалення поля
Realm просто ігнорує поля, яких немає в новій моделі. Явної міграції не потрібно — але schemaVersion збільшити необхідно. Це часто викликає плутанину: розробники думають, що видалення безпечне, але якщо не оновити версію, Realm викине виняток.
Перейменування поля
migration.renameProperty(onType: "User", from: "fullName", to: "displayName")
Це зберігає дані. Якщо видалити старе поле і додати нове — дані втратяться. Ми рекомендуємо завжди використовувати renameProperty для збереження зворотної сумісності.
Зміна типу поля
Пряма конвертація типів не підтримується — це обмеження Realm. Шлях: читаємо старе значення, записуємо в нове поле нового типу, старе поле прибираємо з моделі (Realm видалить автоматично). Наприклад, якщо price був рядком, а став числом з плаваючою точкою:
migration.enumerateObjects(ofType: "Product") { old, new in
let priceString = old?["priceString"] as? String ?? "0"
new?["price"] = Double(priceString) ?? 0.0
}
Як відлагоджувати міграцію з Realm Studio?
Realm Studio дозволяє відкрити .realm файл і переглянути дані до та після міграції. Корисно для перевірки коректності. Файл на Android: /data/data/<package>/files/default.realm (доступний через Device Explorer в Android Studio). Realm Studio обробляє файли на 50% швидше, ніж прямий перегляд у браузері, і підтримує фільтрацію за класами.
| Інструмент |
Можливості |
Швидкість роботи |
| Realm Studio |
Перегляд, фільтрація, експорт даних |
Висока |
| Браузер (JSON) |
Тільки читання, без фільтрації |
Низька |
Як ми гарантуємо коректну міграцію: процес і тестування
Міграція схеми — критична операція. Помилка призводить до втрати даних користувачів, а відновлення з бекапу не завжди можливе. Наші інженери мають 5+ років досвіду роботи з Realm і гарантують коректне виконання всіх переходів.
- Аналіз поточної схеми та історії змін.
- Написання migrationBlock для всіх трансформацій.
- Тестування на Realm Studio та на реальному бекапі.
- Інтеграція в проєкт і деплой через App Store/Google Play.
Що входить в роботу
Ми надаємо:
- Документацію змін схеми з описом кожного кроку.
- Код міграцій з unit-тестами та інтеграційними тестами.
- Інструкцію з відкату на попередню версію.
- Підтримку при деплої протягом 2 тижнів.
Вартість міграції розраховується індивідуально, але часто економія на підтримці окупається через місяць. Замовте міграцію Realm прямо зараз і отримайте гарантію збереження даних. Зв'яжіться з нами — ми оцінимо ваш проєкт за один робочий день.
Типові помилки міграції Realm (і як їх уникнути)
- Забути збільшити schemaVersion — застосунок впаде у всіх користувачів. Завжди перевіряйте, що версія збільшена хоча б на 1.
- Пропустити обробку nullable полів — дані перетворяться на nil/null. Використовуйте enumerateObjects для явного заповнення.
- Перейменовувати поле через видалення+додавання — втрата даних. Завжди використовуйте renameProperty.
- Використовувати різні SDK в одному проєкті (Java + Kotlin) — конфлікт бібліотек. Виберіть один SDK і дотримуйтеся його.
| Етап |
Деталі |
Терміни |
| Аналіз |
Вивчення поточної схеми, списку версій, даних користувачів |
0,5 дня |
| Розробка |
Створення міграцій: додавання, перейменування, зміна типів |
1 день |
| Тестування |
Перевірка на Realm Studio, unit-тести, інтеграційне тестування |
1 день |
| Документація |
Опис змін та інструкція для команди |
0,5 дня |
Терміни: від 1 дня для простих міграцій до 3 днів для складних багатокрокових переходів. Оцінимо ваш проєкт безкоштовно. Отримайте консультацію з міграції Realm вже сьогодні — зв'яжіться з нами, і ми запропонуємо оптимальне рішення.
Як вибрати рішення для локального зберігання даних (Room, Core Data, Realm, Isar)?
Ми стикалися з ситуацією, коли додаток втрачає дані при втраті мережі — і це не просто баг, це провал сценарію. Користувач заповнив форму, натиснув «Відправити», отримав таймаут і втратив все. Або гірше: дані відправилися двічі через некоректну логіку повторної відправки. Правильно вибраний та налаштований шар сховища вирішує цю проблему раз і назавжди. Неправильний вибір може коштувати команді місяців переписування коду та втрати до 70% часу на синхронізацію. Наш досвід — 10+ років у мобільній розробці, понад 50 проектів з офлайн-сховищами — підтверджує: вибір рішення визначає 80% майбутніх проблем з продуктивністю та синхронізацією.
На практиці вибір сховища визначається двома факторами: типом даних та вимогами до синхронізації, а не популярністю бібліотеки.
Room (Android) — обгортка над SQLite з compile-time верифікацією SQL-запитів. Якщо запит невалідний, збірка падає — це краще, ніж SQLiteException в рантаймі. Room добре інтегрується з Kotlin Flow та LiveData, що робить реактивні UI-оновлення прямолінійними. Основна складність — міграції схеми. @Database(version = N, exportSchema = true) з файлами міграцій в assets/databases/ — обов'язкова практика, інакше при оновленні додатка fallbackToDestructiveMigration() просто зітре дані користувача.
Core Data (iOS) — не база даних, а фреймворк управління графом об'єктів поверх SQLite (або XML, або in-memory). NSPersistentContainer з viewContext для читання на main thread та newBackgroundContext() для запису — базова схема. Проблема починається, коли розробник робить save() в viewContext з фонового потоку: EXC_BAD_ACCESS в рандомний момент, відтворюється раз на тиждень, в крешлозі майже нічого корисного. Потрібно використовувати performAndWait або perform для кожного контексту строго в своєму потоці. Apple Core Data Programming Guide рекомендує саме такий підхід.
Realm виграє там, де потрібна швидкість роботи з великими наборами об'єктів та вбудована реактивність через Results + observe(). Realm зберігає об'єкти напряму, без маппінгу ORM, тому читання не потребує десеріалізації. За нашими вимірами, Realm обробляє читання в 2–3 рази швидше Core Data при об'ємі понад 10 000 об'єктів. На Flutter Realm SDK (ex-MongoDB Realm) підтримує Device Sync — але це вже managed-сервіс з окремою інфраструктурою.
Hive та Isar — Flutter-специфічні рішення. Hive — key-value сховище, швидко, просто, підходить для налаштувань та кешів. Isar — повноцінна документо-орієнтована БД з індексами, написана на Rust, компілюється в нативний код. Для Flutter-додатків з офлайн-функціональністю Isar зараз переважніше: вбудований query builder з типобезпечними фільтрами, транзакції, watchObject/watchQuery для реактивності.
| Платформа |
Рішення |
Реактивність |
Синхронізація |
| Android |
Room + Flow |
LiveData/Flow |
WorkManager |
| iOS |
Core Data |
NSFetchedResultsController |
CloudKit |
| Flutter |
Isar |
Streams |
Custom / Realm Sync |
| Cross-platform |
Realm |
RealmResults.observe |
Device Sync |
| Flutter (простий) |
Hive |
ValueListenable |
Немає |
Зв'яжіться з нами, щоб отримати безкоштовний аудит вашого поточного сховища та рекомендації з оптимізації — це зекономить вам сотні годин розробки та до 60% трафіку на серверні запити.
Чому офлайн-синхронізація — найскладніша частина?
Локальне сховище саме по собі нескладне. Складність — у синхронізації з сервером при наявності конфліктів.
Найчастіший патерн — optimistic updates з rollback. Користувач редагує запис, UI відображає зміну миттєво, фоновий запит йде на сервер. Якщо сервер повертає помилку — відкочуємо локальний стейт. Виглядає просто. На практиці: якщо користувач встиг піти з екрану і повернутися, а відкат відбувся через 3 секунди — UX зламаний. Потрібна явна черга операцій зі станом (PENDING, SYNCED, FAILED) в окремій таблиці.
На Android для фонової синхронізації використовуємо WorkManager з Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED). Важно не забыть про setInputMerger(ArrayCreatingInputMerger::class) при батчинге задач — інакше при кількох одночасних запусках дані затираються. Типова реалізація черги операцій:
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val pendingOps = syncDao.getPendingOperations()
for (op in pendingOps) {
try {
apiClient.send(op.payload)
syncDao.markSynced(op.id)
} catch (e: Exception) {
syncDao.markFailed(op.id, e.message)
return Result.retry()
}
}
return Result.success()
}
}
На iOS аналог — BGTaskScheduler з BGProcessingTaskRequest. Обмеження iOS на фоновий час виконання (~30 секунд для refresh tasks) означають, що синхронізація повинна бути інкрементальною: не «синхронізувати все», а «синхронізувати наступні N записів, зберегти курсор».
Конфлікти при мультипристроєвій роботі вирішуються одним із трьох підходів:
- Last-write-wins по
updated_at (найпростіший, втрачає дані при одночасному редагуванні)
- Server-wins (клієнт завжди приймає серверну версію)
- Three-way merge (складно, потрібен спільний предок — підходить для документів)
У більшості B2C-додатків достатньо last-write-wins з вектором часу на рівні користувача, але при спільному редагуванні потрібен CRDTs-підхід — тоді дивимося на Automerge або Yjs з мобільними біндингами.
Як ми будуємо шар сховища
Репозиторний патерн — не опціональний, а обов'язковий. UserRepository не знає, звідки дані: з Room, Realm чи мережі. ViewModel викликає repository.getUser(id), отримує Flow/Stream, відображає дані. Логіка кешування — всередині репозиторія.
Для Flutter типова архітектура: Isar для персистентності, Riverpod для управління стейтом, ConnectivityPlus для визначення стану мережі, кастомний SyncService з чергою операцій. Riverpod AsyncNotifier зручно покриває логіку «показати кеш, оновити з мережі, показати нові дані». Приклад репозиторія з кешуванням:
class UserRepository {
final Isar isar;
final ApiClient api;
Future<User> getUser(String id) async {
// 1. спробувати з локального сховища
final cached = await isar.user.where().idEqualTo(id).findFirst();
if (cached != null) return cached;
// 2. інакше з мережі
final remote = await api.fetchUser(id);
// 3. зберегти локально
await isar.writeTxn(() => isar.user.put(remote));
return remote;
}
}
Окрема тема — шифрування. Якщо додаток зберігає медичні дані, платіжні картки або корпоративні документи, SQLCipher (Android) та NSFileProtection (iOS) — не опція. Realm підтримує шифрування нативно через ключ у 64 байти, який потрібно зберігати в Keychain/Keystore, а не в SharedPreferences. Економія на безпеці може обійтися у витік даних з гучними наслідками.
Що входить в роботу
Ми гарантуємо прозорий процес і фіксуємо кожен етап:
| Етап |
Результат |
| Аудит вимог |
Документ з аналізом типів даних, обсягів, сценаріїв синхронізації |
| Проектування схеми |
ER-діаграма, файли міграцій, план конфлікт-резолюції |
| Розробка репозиторного шару |
Код з юніт-тестами (in-memory БД + моки мережі) |
| Інтеграція синхронізації |
Черга операцій, обробка помилок, fallback-логіка |
| Профілювання та оптимізація |
Звіт Android Profiler / Core Data SQLDebug, рекомендації |
| Деплой та документування |
Інструкція з розгортання, API-опис, доступ до репозиторію |
Бажаєте уникнути типових помилок при проектуванні сховища? Зверніться до нас — ми допоможемо спроектувати надійне локальне сховище з нуля або доопрацювати існуюче.
Які етапи роботи?
Починаємо з аудиту вимог: які дані, який обсяг, чи потрібна синхронізація, чи можливі конфлікти. На цьому етапі стає зрозуміло, Core Data чи SQLite-based рішення, чи потрібен Realm Sync або достатньо простого REST-поллінгу.
Далі — проектування схеми з урахуванням міграцій. Схему змінюють у будь-якому проекті — питання не «чи будуть міграції», а «наскільки болісно вони пройдуть». Експортуємо схему в JSON, зберігаємо в репозиторії, пишемо тести на міграцію кожної версії.
Розробка йде з покриттям репозиторного шару юніт-тестами: моки мережевого шару, реальна in-memory база для тестування запитів. Перед релізом — профілювання запитів через Android Profiler (вкладка Database Inspector) або Core Data debug флаги (-com.apple.CoreData.SQLDebug 1).
Термін реалізації шару сховища з базовою офлайн-синхронізацією — від 2 до 6 тижнів залежно від складності схеми та вимог до конфлікт-резолюції. Вартість розраховується індивідуально після аудиту вашого проекту. Замовте розробку під ключ — отримайте консультацію з вибору оптимального стеку та міграціям.