Реализация истории изменений (Version History) в мобильном приложении
Представьте: юрист правит договор на планшете, случайно удаляет абзац и сохраняет. Через час — подписание, а данных нет. Без истории изменений — катастрофа. Мы реализовали систему, которая хранит каждую правку и позволяет откатить любую версию за секунды. В проектах с юридически значимыми документами, редакторской работой или сложными таблицами каждая правка должна быть отслежена и доступна для отката. Как спроектировать хранилище истории, чтобы оно не раздувалось и работало на мобильном устройстве с ограниченными ресурсами? Разбираем ключевые подходы, их сильные и слабые стороны, а также даём готовые схемы и код. Наш опыт — более 7 лет в мобильной разработке, мы реализовали версионирование в десятках проектов.
Два подхода: snapshot vs event sourcing
Snapshot — при каждом сохранении создаём полную копию объекта. Простота реализации и восстановления оборачивается взрывным ростом базы. Если документ весит 50 КБ, а пользователь сохраняет его 50 раз в день, за год накопится 50 × 50 × 365 ≈ 912 МБ только для одного документа. Для приложения с тысячами пользователей это неприемлемо.
Delta (event sourcing) — храним только разницу между версиями. Это компактно, но восстановление требует воспроизведения всех дельт от начального состояния. Для текста можно использовать diff-match-patch (бесплатная библиотека, доступна на iOS и Android). Однако сложность возрастает при параллельном редактировании.
Гибридный подход — комбинация: снэпшоты через каждые 10 версий или каждые 7 дней, между ними дельты. Восстановление: берём ближайший снэпшот и применяем дельты. Такой метод даёт до 90% экономии места по сравнению с чистыми снэпшотами и ускоряет восстановление в 5–10 раз относительно чистых дельт.
Ниже — сравнение трёх подходов.
| Характеристика | Snapshot | Delta | Гибрид |
|---|---|---|---|
| Объём хранилища | Огромный | Минимальный | Средний |
| Скорость восстановления | Мгновенно | Медленно | Быстро |
| Сложность реализации | Низкая | Высокая | Средняя |
| Устойчивость к конфликтам | Высокая | Низкая | Средняя |
Схема данных
Для SQLite-базы на Android или CoreData на iOS мы используем такую таблицу:
@Entity(tableName = "document_versions") data class DocumentVersion( @PrimaryKey(autoGenerate = true) val id: Long = 0, val documentId: String, val versionNumber: Int, val deltaJson: String?, // null если snapshot val snapshotJson: String?, // null если delta val authorId: String, val deviceId: String, val createdAt: Long = System.currentTimeMillis(), val comment: String? = null // "Автосохранение" / "Сохранено вручную" ) Индекс по (documentId, versionNumber) обязателен — иначе выборка истории станет тормозом. На Android используем @Index в Room, на iOS — составной индекс в CoreData.
Как выбрать стратегию хранения версий?
Выбор зависит от типа данных и частоты изменений. Для документов с редкими правками (1–2 раза в день) чистый snapshot — просто и надёжно. Для текстовых редакторов, где правки происходят каждые секунды, лучше гибрид. Всегда оценивайте нагрузку: если база растёт быстрее, чем пользователи создают контент, меняйте подход.
Ограничение глубины истории
Хранить все версии вечно — нецелесообразно. Предлагаем три стратегии:
| Стратегия | Описание | Пример лимита |
|---|---|---|
| Фиксированное количество | Хранить последние N версий | 50 версий |
| Временное окно | Версии не старше M дней | 30 дней |
| Умное прореживание | Смешанный подход | Полные за сутки, одна в день за неделю |
Пример удаления старых версий на Android с WorkManager:
@Transaction suspend fun pruneVersions(documentId: String, keepCount: Int) { val versions = getVersionsByDocument(documentId) if (versions.size > keepCount) { val toDelete = versions.drop(keepCount) deleteVersions(toDelete.map { it.id }) } } Почему стоит ограничивать глубину истории?
Без ограничений база данных может раздуться до гигабайтов на устройстве. Пользователь редко возвращается к версиям старше месяца. Наш опыт показывает, что разумный лимит — 50 версий или 30 дней — покрывает 99% потребностей, а хранилище остаётся под контролем. Экономия на хранении — до 90% объёма БД, что напрямую снижает затраты на инфраструктуру.
UI истории версий
Список версий с датой, автором и типом (автосохранение/ручное). Тап — предпросмотр, две кнопки: «Восстановить» и «Сравнить с текущей». Diff-view с подсветкой: зелёный для добавленного, красный для удалённого. На мобильном используем inline diff через SpannableString (Android) или NSAttributedString (iOS) — компактно и интуитивно.
Автосохранение и дебаунс
Автосохранение не должно создавать версию на каждый символ. Дебаунс 2–3 секунды после последнего изменения:
private var saveTask: Task<Void, Never>? func textDidChange(_ text: String) { saveTask?.cancel() saveTask = Task { try? await Task.sleep(nanoseconds: 2_000_000_000) guard !Task.isCancelled else { return } await saveVersion(text, type: .auto) } } Что входит в работу
- Анализ данных и выбор стратегии (snapshot / delta / гибрид) — оцениваем нагрузку и гарантируем производительность.
- Проектирование схемы данных с индексами и внешними ключами.
- Реализация автосохранения с дебаунсом.
- Разработка UI списка версий с метаданными.
- Создание diff-view для сравнения версий.
- Настройка фоновой очистки устаревших версий по расписанию.
Пример полной реализации очистки с WorkManager (Android)
class VersionPruneWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val dao = AppDatabase.getInstance(applicationContext).documentVersionDao() dao.pruneAllVersions(keepCount = 50) return Result.success() } } Сроки
Базовая реализация (snapshot + простой UI) — 1,5–2 дня. Полноценный гибрид с дельтами, diff-view и умным прореживанием — 4–5 дней. Стоимость рассчитывается индивидуально исходя из сложности данных и требований к глубине истории. Реализуем под ключ: от проектирования до публикации в сторах. Получите консультацию — напишите нам для оценки вашего проекта. Закажите внедрение истории изменений — мы предложим оптимальное решение.







