Онлайн/оффлайн управление данными

Мне нужно создать приложение, имеющее функциональность, похожую на приложение для контактов. Вы можете добавить контакт на клиентский iPhone и загрузить его на клиентский iPad. Если клиент обновляет контакт на своем iPad, он должен обновиться на своем iPhone.

Большая часть этого довольно прямолинейна. Я использую Parse.com как мой задний конец и локально сохраняя контакты с помощью Core Data. Единственная проблема, с которой я сталкиваюсь, заключается в управлении контактами, когда пользователь отключен.

Скажем, у меня есть iPhone и iPad. Обе они в настоящее время имеют одну и ту же версию онлайн-базы данных. Мой iPhone теперь отключен. Это 9AM.

В 10 утра я обновляю номер телефона для контакта на своем iPad. Это сохраняет изменения локально и онлайн. В 11:00 я обновляю адрес электронной почты для одного и того же контакта на своем iPhone, но я все еще не в сети.

В полдень мой iPhone подключается к Интернету и проверяет сервер на наличие изменений. Он видит, что его изменения более поздние, чем последнее обновление (проверка свойства updatedAt timestamp), поэтому вместо того, чтобы загружать новый номер телефона для контакта (который является "устаревшим" ), он отменяет номер телефона вместе с адресом электронной почты адрес (обновляет новый номер телефона до старой версии, потому что он был отключен во время обновления номера телефона в 10 утра, и его изменения предположительно более свежие).

Как я могу управлять проблемами в сети/офлайн, с которыми сталкиваются, например, выше? Решением, о котором я могу думать, было бы сохранить обновленные временные метки для каждого атрибута для контакта, а не только для общего свойства updatedAt для всего контакта, например. когда было обновлено имя, когда была обновлена ​​фамилия, а затем вручную проверьте, имеет ли автономное устройство последние изменения для каждого атрибута вместо того, чтобы перезаписывать весь объект, но это кажется неаккуратным.

Я также думал о наличии свойства timestamp updatedLocally и updatedOnline для каждого объекта Core Data. Таким образом, если эти два не совпадают, я могу выполнить проверку diff и использовать последний для конфликтов, но это все еще не похоже на самое чистое решение. Кто-нибудь еще сталкивался с чем-то похожим? Если да, то как вы его решили?

Псевдокод/​​Резюме для того, что я думаю? охватывает все тестовые примеры, но все еще не очень элегантно/полно:

2 Объекты на Parse.com: история контактов и контактов

Контакт имеет первый, последний, телефон, адрес электронной почты, онлайн-адрес

История контактов имеет первичный ключ для контакта для ссылок и те же атрибуты, но с историей. например first: [{value:"josue",onlineUpdate:"9AM"},{value:"j",onlineUpdate:"10AM"},{value:"JOSUEESP",onlineUpdate:"11AM"}]

1 Объект по основным данным, контакт:

Контакт имеет первый, последний телефон, адрес электронной почты, onlineUpdate и offlineUpdate (ВАЖНО: это только в Core Data, а не в Parse)

for every contact in parse database as onlineContact {
    if onlineContact does not exist in core data {
        create contact in core data
    }
    else {
        // found matching local object to online object, check for changes
        var localContact = core data contact with same UID as onlineContact
        if localContact.offlineUpdate more recent than onlineContact.onlineUpdate {
            for every attribute in localContact as attribute {
                var lastOnlineValueReceived = Parse database Contact History at the time localContact.onlineUpdate for attribute
                if lastOnlineValueReceived == localContact.attribute {
                    // this attribute did not change in the offline update. use latest available online value
                    localContact.attribute = onlineContact.attribute
                }
                else{
                    // this attribute changed during the more recent offline update, update it online
                    onlineContact.attribute = localContact.attribute
                }
            }
        }
        else if onlineContact.onlineUpdate more recent than localContact.offlineUpdate {
            // another device updated the contact. use the online contact.
            localContact = offlineContact
        }
        else{
            // when a device is connected to the internet, and it saves a contact
            // the offline/online update times are the same
            // therefore contacts should be equivalent in this else statement
            // do nothing
        }
}

TL; DR: Как вы планируете структурировать своего рода систему контроля версий для онлайновых/оффлайновых обновлений без случайной перезаписи? Я хотел бы ограничить использование полосы пропускания до минимума.

Ответ 1

Я бы предложил использовать обновления на основе ключей вместо обновлений на основе контактов.

Вы не должны отправлять весь контакт на сервер, в большинстве случаев пользователь просто изменит несколько атрибутов в любом случае (такие вещи, как "фамилия", обычно не меняются очень часто). Это также уменьшает использование полосы пропускания.
Наряду с внесенными изменениями вашего автономного контакта вы отправляете старый номер версии/последнее обновление временной метки вашего локального контакта с сервером. Теперь сервер может определить, обновлены ли ваши локальные данные, просто просмотрев свой старый номер версии.
Если ваш старый номер версии соответствует текущему номеру версии сервера, вам не нужно обновлять какую-либо другую информацию. Если это не так, сервер должен отправить вам новый контакт (после применения вашего запрошенного обновления).

Вы также можете сохранить эти коммиты, это приведет к истории контактов который не сохраняет весь контакт каждый раз, когда ключ был изменен, а только сами изменения.

Простая реализация в псевдокоде может выглядеть так:

for( each currentContact in offlineContacts ) do
{

if( localChanges.length > 0){      // updates to be made
    commitAllChanges();
    answer = getServerAnswer();

    if(answer.containsContact() == true){  
                                  // server sent us a contact as answer so 
                                  // we should overwrite the contact
    currentContact = answer.contact;
    } else {
      // the server does not want us to overwrite the contact, so we are up to date!
    }
    // ... 

}
} // end of iterating over contacts

Серверная сторона будет выглядеть так же просто:

for (currentContactToUpdate in contactsToUpdate) do 
{   
    sendBackContact = false;   // only send back the updated contact if the client missed updates
    for( each currentUpdate in incomingUpdates ) do {
        oldClientVersion = currentUpdate.oldversion;
        oldServerVersion = currentContact.getVersion();

       if( oldClientVersion != oldServerVersion ){
            sendBackContact = true;
            // the client missed some updates from other devices
            // because he tries to update an old version
       } 

       currentContactToUpdate.apply(currentUpdate);

    }

    if(sendBackContact == true){
       sendBack(currentUpdate);
    }
}



Чтобы лучше понять рабочий процесс, я приведу пример:


8 AM и клиенты, и сервер обновлены, каждое устройство находится в режиме онлайн

Каждое устройство имеет запись (в данном случае строку) для контакта "Foo Bar", которая имеет идентификатор первичного ключа. Версия для каждой записи одинакова, поэтому все они обновлены.

 _        Server    iPhone    iPad
 ID       42        42        42 
 Ver      1         1         1
 First    Foo       Foo       Foo
 Last     Bar       Bar       Bar
 Mail     [email protected]       [email protected]       [email protected]

(извините этот ужасный формат, SO, к сожалению, не поддерживает какие-либо таблицы...)



9 утра ваш iPhone отключен. Вы заметили, что электронная почта Foo Bar изменилась на "foo @b" . Вы изменяете контактную информацию на своем телефоне следующим образом:

UPDATE 42 FROM 1          TO 2             [email protected]
 //    ^ID     ^old version  ^new version  ^changed attribute(s)

теперь контакт на вашем телефоне будет выглядеть следующим образом:

 _        iPhone   
 ID       42       
 Ver      2       
 First    Foo      
 Last     Bar   
 Mail     [email protected]   



10 утра ваш iPad отключен. Вы заметили, что "Foo Bar" на самом деле написан как "Voo Bar" ! Вы немедленно применяете изменения на своем iPad.

UPDATE 42 FROM 1 TO 2 First=Voo

Обратите внимание, что iPad по-прежнему считает, что текущая версия контакта 42 равна 1. Ни сервер, ни iPad не заметили, как вы изменили адрес электронной почты и увеличили номер версии, поскольку в сеть не было подключено никаких устройств. Эти изменения только локально сохранены и видны на вашем iPad.


11 AM вы подключаете iPad к сети. iPad отправляет последнее обновление к серверу.

До:

 _        Server    iPad
 ID       42        42 
 Ver      1         2
 First    Foo       Voo
 Last     Bar       Bar
 Mail     [email protected]       [email protected]


iPad → Сервер:

UPDATE 42 FROM 1 TO 2 First=Voo

Теперь сервер может видеть, что вы обновляете версию 1 контакта 42. Поскольку версия 1 - это текущая версия, ваш клиент обновлен (никаких изменений не было сделано в то время, когда вы были в автономном режиме).

Сервер → iPad

UPDATED 42 FROM 1 TO 2 - OK


После:

 _        Server    iPad
 ID       42        42 
 Ver      2         2
 First    Voo       Voo
 Last     Bar       Bar
 Mail     [email protected]       [email protected]



12 утра вы отключили iPad от сети и подключили свой iPhone. IPhone пытается зафиксировать последние изменения.

До:

 _        Server    iPhone
 ID       42        42 
 Ver      2         2
 First    Voo       Voo
 Last     Bar       Bar
 Mail     [email protected]       [email protected]


iPhone → Сервер

UPDATE 42 FROM 1 TO 2 [email protected]

Сервер уведомляет, как вы пытаетесь обновить старую версию одного и того же контакта. Он будет применять ваше обновление, поскольку он является более поздним, чем обновление iPad, но отправит вам новые контактные данные, чтобы убедиться, что вы также получите обновленное имя.

После:

 _        Server    iPhone
 ID       42        42 
 Ver      2         2
 First    Voo       Voo
 Last     Bar       Bar
 Mail     [email protected]     [email protected]


Сервер → iPad

UPDATED 42 FROM 1 TO 3 - Ver=2;First=Voo;.... // send the whole contact
/* Note how the version number was changed to 3, and not to 2, as requested.
*  If the new version number was (still) 2 the iPad would miss the update
*/


В следующий раз, когда ваш iPad подключится к сети и не будет изменений для фиксации, он должен просто отправить текущую версию контакта и посмотреть, все ли он в актуальном состоянии.


Теперь вы совершили две автономные изменения, не перезаписывая друг друга.
Вы можете легко расширить этот подход и, следовательно, некоторые оптимизации.
Например:

  • Если клиент пытается обновить старую версию контакта, не отправляйте им весь контакт в качестве ответа. Скорее отправьте им те коммиты, которые они пропустили, и пусть они обновляют свои контакты сами. Это полезно, если вы храните много информации о своем клиенте и ожидаете, что между обновлениями будет сделано несколько изменений.
  • Если клиент обновил всю информацию о контакте, мы можем предположить, что ему не нужно знать о пропущенных обновлениях, однако мы бы сообщали ему обо всем, что он пропустил (но это было бы/не должно было повлиять на него)



Надеюсь, это поможет.

Ответ 2

Я ничего не знаю об iOs, основных данных и parse.com, поэтому я могу предложить только общее алгоритмическое решение. Я думаю, вы можете подойти так же, как и в системах контроля версий.

Самое простое - сохранить всю историю на сервере: сохранить все изменения в списке контактов. Теперь во время синхронизации телефон отправляет информацию о последней ревизии сервера, которую он видел, и эта ревизия будет "общим родителем" для текущей версии телефона и текущей версии сервера.

Теперь вы можете увидеть, что изменилось на сервере и на телефоне с этой версии, и применить обычное трехстороннее сравнение: если какое-то поле изменилось только на сервере, то отправьте новое поле на телефон; если какое-то поле изменилось только на телефоне, а затем измените его и на сервере, если какое-то поле было изменено как на телефоне, так и на сервере, а изменения разные, тогда у вас есть конфликт и вы должны спросить пользователя.

Вариантом такого подхода может быть работа с изменениями, а не с изменениями. Первичные данные как на сервере, так и на клиенте будут не списком контактов, а историей его изменений. (Текущий список контактов, а также набор "ключевых кадров" также могут быть сохранены в случае необходимости, он не будет использоваться для алгоритма разрешения конфликтов, но может использоваться, чтобы его можно было быстро отобразить и использовать.)

Затем, когда пользователь синхронизирует данные, вы загружаете/загружаете только изменения. Если есть какие-либо изменения в конфликте, у вас нет ничего, кроме как спросить пользователя, иначе вы просто слейте их. Как вы определяете изменение и какие изменения считаются противоречивыми, зависит от вас. Простой подход может определять изменение как пару (поле, новое значение), а два изменения противоречат друг другу, если они имеют одно и то же поле. Вы также можете использовать более совершенную логику разрешения конфликтов, например, если одно изменение изменяет только первую половину письма, а вторую вторую половину, то вы можете объединить их.

Ответ 3

Правильный способ сделать это - сохранить журнал транзакций. Всякий раз, когда вы сохраняете в Core Data, вы создаете запись в журнале транзакций. Когда вы будете в режиме онлайн, вы воспроизводите журнал транзакций с сервером.

Вот как работают iCloud и другие службы синхронизации.

Ответ 4

  • Вместо отдельного флага для каждого из основных объектов данных у вас может быть отдельная таблица, в которой будут храниться идентификаторы (первичный ключ из таблицы базы данных, в котором хранятся контактные данные) всех обновленных контактов.

  • Позже, когда пользователь приходит в Интернет, вы просто загружаете эти контакты из своей фактической таблицы контактов и загружаете их на свой сервер.