Миграция базы данных 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 (simple) |
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 недель в зависимости от сложности схемы и требований к конфликт-резолюции. Стоимость рассчитывается индивидуально после аудита вашего проекта. Закажите разработку под ключ — получите консультацию по выбору оптимального стека и миграциям.