Как обеспечить передачу и хранение данных с возможностью версионного контроля и разрешения конфликтов в нативных приложениях?
Широкомасштабный получился заголовок вопроса, но я приведу конкретный пример, чтобы на вопрос можно было дать более-менее однозначный ответ. Вопрос получился длинный потому, что я решил подробно описать задачу, чтобы не возникало вопросов типа "какую конкретно задачу вы решаете?", а также изложить ход мыслей, что Вы не тратили время объясняя то, что я уже знаю.
Предыстория
С начала IT-карьеры в 2016 я занимался в основном веб-разработкой. Хотя в теории в веб-разработке можно реализовать и версионирование, и возможность работать в оффлайне (хотя и с ограничениями), в реальности это редкость, а потому де-факто большинство веб-приложений работают по следующим принципам:
- Чёткий порядок при обмене данными:
- Блокировка управления со стороны пользователя
- Формирование HTTP-запроса
- Отправка запроса
- Обработка запроса и формирование ответа
- Отправка ответа
- Передача управления пользователю Для сравнения: в нативных приложениях гораздо чаще обмен данных осуществляется в фоновом режиме, что удобно для пользователя, но может возникнуть рассогласование межу локальными данными и данными на сервере.
- Гарантия свежести данных на момент открытия приложения: Если интернет-соединение в норме, то на момент загрузки страницы в браузере будут отображаться актуальные (не устаревшие) данные, в противном случае никакие веб-прлоижения и веб-сайты не будут доступны. Для сравнения: если в нативном приложении организовано локальное хранение данных, а интернет-соединение отсутствует, то отображающиеся данные из локальной базы данных могут быть устаревшими.
- Отсутствие версионирования: Допустим, два пользователя внесли изменения в одни и те же данные (например, статью блога) и отправили изменения на сервер почти одновременно. Тогда те изменения, которые пришли на сервер раньше, будут потеряны (перезаписаны).
Задача
Разработать/заимствовать/усовершенствовать методологию работы с данными с поддержкой режима оффлайн и многопользовательского режима для нативных приложений. На данный момент предполагается, что такая работа будет осуществляться на следующим принципам:
- Отправка данных на сервер осуществляется в фоновом режиме без блокировки управления пользователем.
- Все данные либо выборка данных хранятся в локальной базе данных, что позволяет работать при потере интернет-соединения.
- При восстановлении интернет соединения осуществляется синхронизация данных и при необходимости доступен режим разрешения конфликтов
- В многопользовательском режиме также обязателен режим разрешения конфликтов
Хочу сказать, что для меня, привыкшему к работе с данными в веб-приложениях по принципам, описанных выше в "Предыстории", это будет изменение мышления революционных масштабов, но я буду стараться.
Зачем это надо?
У некоторых может возникнуть вопрос, зачем мне это надо, когда "на дворе 21 век и интернет доступен как воздух" или "сейчас всё делается через браузер" и так далее.
Во-первых, интернет как воздух не доступен, ибо это (так, на минуточку) - платная услуга, а потери интернет-соединения были, есть и будут ввиду сложности и уязвимости оборудования, а также других факторов, например перебоев с электроснабжением.
А теперь представьте, что работа бухгалтерии, менеджмента и прочего администрирования в корпорации обеспечивается за счёт веб-приложений. Тогда, потеря интернет соединения означает паралич компании (за исключением случаев, когда приложение работает только локальной сети, но и там могут возникнуть проблемы с соединением, например ввиду неполадки в маршрутизаторе). Для крупных компаний это вовсе может повлечь за собой многомиллионный ущерб.
Поэтому несмотря на сложность задачи, я полон решимости ей заняться.
Начальная инфраструктура и механизм сохранения данных
Имеется локальная база данных (обычно SQLite) и удалённая (например, PosgreSQL, но это не так важно). Когда пользователь вносит изменения в данные, то прежде всего они сохраняются в локальную базу данных. Обычно это происходит почти мгновенно и ошибки возникают редко.
В идеале, следует тут же сохранить их и в удалённую базу данных, но здесь и время сохранения обычно дольше (особенно если у нас SaaS с миллионами пользователей наподобие Microsoft TODO List), да и вероятность возникновения ошибки выше. Поэтому тут нарушение синхронизации данных здесь неизбежно.
Конкретный пример
В качестве примера возьму кроссплатформенное корпоративное приложение для бухгалтерии. Также, примера ради возьму сущность "сделка". На C# определение соответствующего класса может быть таким:
public class Deal
{
public string ID { get; init; }
public DateTime Date { get; set; }
public double TotalAmount { get; set; }
public string Description { get; set; }
}
Хотя и Deal, с Transaction имеют смысл "сделка", насколько мне известно, в русском языке слово "сделка" не используется в отношении обмена данными с сервером или базой данных, поэтому когда я буду говорить об обмене данными, то буду использовать слово "транзакция".
Как обычно происходит обмен данными в веб-приложениях
Про CRUD-операции, думаю, знают многие, даже новички. Сейчас важно то, при каждом обмене данными мы какие-то данные сущности (в нашем примере - основанные на Deal
) отправляем, а какие-то получаем (их называют "DTO", "данными запроса" и так далее). Например, при добавлении новой сделки в качестве данных запроса необходимо отправить все обязательные поля, кроме ID.
Пример реализации CRUD-операций в стандартном исполнении:
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
public class DealService
{
private readonly HttpClient httpClient;
private const string DealsEndpoint = "api/deals";
public DealService(HttpClient httpClient)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<List<Deal>> GetDealsAsync()
{
return await httpClient.GetFromJsonAsync<List<Deal>>(DealsEndpoint);
}
public async Task<Deal> GetDealAsync(string id)
{
return await httpClient.GetFromJsonAsync<Deal>($"{DealsEndpoint}/{id}");
}
public class CreateDealDto
{
public DateTime Date { get; set; }
public double TotalAmount { get; set; }
public string Description { get; set; }
}
public async Task<Deal> CreateDealAsync(CreateDealDto createDealDto)
{
var response = await httpClient.PostAsJsonAsync(DealsEndpoint, createDealDto);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Deal>();
}
public async Task UpdateDealAsync(string id, Deal deal)
{
var response = await httpClient.PutAsJsonAsync($"{DealsEndpoint}/{id}", deal);
response.EnsureSuccessStatusCode();
}
public async Task DeleteDealAsync(string id)
{
var response = await httpClient.DeleteAsync($"{DealsEndpoint}/{id}");
response.EnsureSuccessStatusCode();
}
}
А хочу я сказать в этом разделе то, что скорее всего для решения поставленных задач придётся в корне изменить методологию обмена данными и сделать ещё более близкой к системам контроля версий. Вероятно, сохранять предстоит не состояние, а информацию о транзакции - такой подход называется event sourcing. Одно из его отличий от CRUD в том, что там есть только операции чтения и создания.
Разбор частных случаев
Случай 1 Нарушение синхронизации из-за медленного интернет-соединения либо большого объёма данных, 1 устройство
Описание сценария
Бухгалтер работает со сделками: добавляет новые, редактирует имеющиеся, иногда удаляет. Однако, ввиду большого объёма данных на сервере и/или низкой скорости интернет-соединения данные не успевают отправляться на сервер. В результате, в локальной базе данных происходит накопление изменений (несинхронизированных данных).
Размышления о решении
В данном случае нам нужно, чтобы в итоге все изменения были полностью синхронизированы с сервером в правильном порядке(!).
Если только в рамках этого случая, то самым тупым, но работающим решением будет полная синхронизация баз данных путём копирования образа. Неважно, какие именно манипуляции и сколько таких манипуляций было проведено локально - всё просто будет тупо скопировано.
Однако даже тут возникают проблемы. Если причиной является обрыв интернет-соединение и/или большой объём данных, то полная синхронизация образа может не просто занять сколь угодно много времени, но и быть прерванной ввиду очередного обрыва соединения.
Поэтому уже при таком раскладе понадобится event sourcing.
Случай 2 Нарушение синхронизации из-за потери интернет соединения на одном из устройств
Описание сценария
- Бухгалтер внёс изменения в данные с одного устройства (скажем, с компьютера). Синхронизацию прошла почти мгновенно.
- Бухгалтер отправился на выезд и сел в метро. По пути от решил со смартфона внести некоторые изменения в данные, однако из-за отсутствия интернет-соединения он совершил манипуляции над устаревшими данными.
- Когда он вышел из метро и интернет-соединение восстановилось, то данные были отправлены на сервер, но возник конфликт.
Размышления о решении
Если при восстановлении интернет-соединения тупо сохранить новые данные поверх тех, что были на сервере, то произойдёт утрата изменений, внесённых к компьютера. Здесь возникает конфликт, который решить в автоматическом режиме невозможно, потому что машина не может знать, какие изменения для нас ещё важны, а какие - уже нет. Опять же, без версионирования никак.
Первое, что тут нужно обеспечить - это хранение даты изменения. Отправляя на сервер изменения, необходимо проверить, над какими данных производились манипуляции.
Более сложные случаи
Изначально я планировал рассмотреть случаи и несколькими пользователями, однако при рассмотрении в первом приближении оказалось, что ситуация, в принципе, схожа со случаем 2, потому что сколько бы пользователей не было, важно лишь то, над какими данными осуществляются манипуляции - актуальными или устаревшими.
Конечный вопрос
Итак, задаю конкретный вопрос, как и обещал.
Какие изменения надо внести в:
- Сущность
Deal
DealService
- Базу данных
чтобы решить поставленную выше задачу, а именно обеспечить совместную работу для нескольких пользователей над данными с синхронизацией между несколькими устройствами и разрешением конфликтов? Вопрос создания GUI для разрешения конфликтов можно не затрагивать - с этим моментов более-менее ясно.
Ни в коем случаем ни ожидаю, что мне всё подробно распишут с примерами, потому что это займёт много времени. Мне достаточно подсказки, а дальше я уже сам найду дальнейшую информацию и/или сам что-то разработаю.