Как асинхронно синхронизировать CoreData и веб-службу REST и в то же время правильно распространять любые ошибки REST в пользовательском интерфейсе
Эй, я работаю над слоем модели для нашего приложения здесь.
Вот некоторые из требований:
- Должно работать на iPhone OS 3.0+.
- Источником наших данных является приложение RESTful Rails.
- Мы должны кэшировать данные локально, используя Core Data.
- Клиентский код (наши контроллеры пользовательского интерфейса) должен иметь как можно меньше знаний о любых сетевых компонентах и должен запрашивать / обновлять модель с помощью Core Data API.
Я проверил сессию 117 WWDC10 по созданию пользовательского интерфейса на основе сервера, потратил некоторое время на проверку сред Objective Resource, Core Resource и RestfulCoreData.
Платформа Objective Resource сама по себе не взаимодействует с Core Data и представляет собой просто реализацию клиента REST. Core Resource и RestfulCoreData предполагают, что вы общаетесь с Core Data в своем коде, и они решают все основные вопросы на уровне модели.
Пока все выглядит хорошо, и изначально я думаю, что либо Core Resource, либо RestfulCoreData покроют все вышеперечисленные требования, но... Есть пара вещей, которые, похоже, не решаются правильно:
- Основной поток не должен быть заблокирован при сохранении локальных обновлений на сервере.
- Если операция сохранения не удалась, ошибка должна быть передана в пользовательский интерфейс, и никакие изменения не должны быть сохранены в локальном хранилище базовых данных.
Основной ресурс случается, чтобы выдать все свои запросы к серверу, когда вы звоните - (BOOL)save:(NSError **)error
в вашем контексте управляемых объектов и, следовательно, может предоставить правильный экземпляр NSError базовых запросов к серверу каким-либо образом. Но он блокирует вызывающий поток до завершения операции сохранения. ПОТЕРПЕТЬ ПОРАЖЕНИЕ.
RestfulCoreData хранит ваши -save:
Вызывает нетронутым и не вводит дополнительное время ожидания для клиентского потока. Это просто следит за NSManagedObjectContextDidSaveNotification
и затем выдает соответствующие запросы на сервер в обработчике уведомлений. Но так -save:
вызов всегда завершается успешно (хорошо, учитывая, что базовые данные в порядке с сохраненными изменениями), и клиентский код, который фактически вызвал его, не может знать, что сохранение могло не распространиться на сервер из-за некоторых 404
или же 421
или какая-либо ошибка на стороне сервера произошла. Более того, локальное хранилище обновляет данные, но сервер никогда не узнает об этих изменениях. ПОТЕРПЕТЬ ПОРАЖЕНИЕ.
Итак, я ищу возможное решение / общие методы решения всех этих проблем:
- Я не хочу, чтобы вызывающий поток блокировался на каждом
-save:
звоните пока сетевые запросы происходят. - Я хочу как-то получать уведомления в пользовательском интерфейсе о том, что какая-то операция синхронизации пошла не так.
- Я также хочу, чтобы сбой при сохранении базовых данных происходил и при сбое запросов к серверу.
Есть идеи?
4 ответа
Вы действительно должны взглянуть на RestKit ( http://restkit.org/) для этого варианта использования. Он предназначен для решения проблем моделирования и синхронизации удаленных ресурсов JSON с локальным кэшем с базовыми данными. Он поддерживает автономный режим для работы полностью из кэша, когда нет доступной сети. Вся синхронизация происходит в фоновом потоке (доступ к сети, анализ полезной нагрузки и объединение контекста управляемого объекта), и существует широкий набор методов делегатов, чтобы вы могли рассказать, что происходит.
Есть три основных компонента:
- Действие пользовательского интерфейса и сохранение изменений в CoreData
- Персистирующие что меняются до сервера
- Обновление пользовательского интерфейса с ответом сервера
NSOperation + NSOperationQueue поможет поддерживать сетевые запросы в порядке. Протокол делегирования поможет вашим классам пользовательского интерфейса понять, в каком состоянии находятся сетевые запросы, например:
@protocol NetworkOperationDelegate
- (void)operation:(NSOperation *)op willSendRequest:(NSURLRequest *)request forChangedEntityWithId:(NSManagedObjectID *)entity;
- (void)operation:(NSOperation *)op didSuccessfullySendRequest:(NSURLRequest *)request forChangedEntityWithId:(NSManagedObjectID *)entity;
- (void)operation:(NSOperation *)op encounteredAnError:(NSError *)error afterSendingRequest:(NSURLRequest *)request forChangedEntityWithId:(NSManagedObjectID *)entity;
@end
Формат протокола, конечно, будет зависеть от вашего конкретного случая использования, но по сути то, что вы создаете, - это механизм, с помощью которого изменения могут быть "перенесены" на ваш сервер.
Далее следует рассмотреть цикл пользовательского интерфейса, чтобы сохранить ваш код в чистоте, было бы неплохо вызвать save: и автоматически передать изменения на сервер. Вы можете использовать для этого уведомления NSManagedObjectContextDidSave.
- (void)managedObjectContextDidSave:(NSNotification *)saveNotification {
NSArray *inserted = [[saveNotification userInfo] valueForKey:NSInsertedObjects];
for (NSManagedObject *obj in inserted) {
//create a new NSOperation for this entity which will invoke the appropraite rest api
//add to operation queue
}
//do the same thing for deleted and updated objects
}
Затраты вычислительных ресурсов на вставку сетевых операций должны быть довольно низкими, однако, если это создает заметную задержку в пользовательском интерфейсе, вы можете просто извлечь идентификаторы сущностей из уведомления о сохранении и создать операции в фоновом потоке.
Если ваш REST API поддерживает пакетную обработку, вы можете даже отправить весь массив сразу, а затем уведомить вас о том, что несколько объектов были синхронизированы.
Единственная проблема, которую я предвижу и для которой не существует "реального" решения, заключается в том, что пользователь не захочет ждать, пока его изменения будут отправлены на сервер, чтобы ему было разрешено вносить дополнительные изменения. Единственная хорошая парадигма, с которой я столкнулся, заключается в том, что вы позволяете пользователю продолжать редактировать объекты и объединять их правки, когда это необходимо, т.е. вы не нажимаете на каждое уведомление о сохранении.
Это становится проблемой синхронизации, и ее нелегко решить. Вот что я бы сделал: в вашем пользовательском интерфейсе iPhone используйте один контекст, а затем с помощью другого контекста (и другого потока) загрузите данные из вашего веб-сервиса. После того, как все это пройдено, выполните процессы синхронизации / импорта, рекомендованные ниже, а затем обновите свой интерфейс после того, как все будет импортировано правильно. Если во время доступа к сети дела идут плохо, просто откатите изменения в не-пользовательском контексте. Это куча работы, но я думаю, что это лучший способ подойти к ней.
Основные данные: эффективный импорт данных
Вам нужна функция обратного вызова, которая будет запускаться в другом потоке (в котором происходит фактическое взаимодействие с сервером), а затем поместить код результата / информацию об ошибке в полуглобальные данные, которые будут периодически проверяться потоком пользовательского интерфейса. Удостоверьтесь, что запись числа, служащего флагом, является атомарной, или у вас будет условие гонки - скажем, если ваш ответ об ошибке составляет 32 байта, вам нужен int (который должен иметь атомный доступ), а затем сохраните этот int в выключенном / ложном / не готовом состоянии до тех пор, пока не будет записан ваш больший блок данных, и только после этого введите "true", чтобы, так сказать, щелкнуть переключателем.
Для коррелированного сохранения на стороне клиента вы должны либо просто сохранить эти данные, но не сохранять их до тех пор, пока не получите подтверждение от сервера, чтобы убедиться, что у вас есть опция отката kinnf - скажем, один из способов удаления - сбой сервера.
Помните, что он никогда не будет на 100% безопасным, если вы не выполните полную двухфазную процедуру фиксации (сохранение или удаление клиента может завершиться ошибкой после сигнала с сервера), но это будет стоить вам как минимум 2 поездки на сервер (может стоить вам 4, если ваш единственный вариант отката - удалить).
В идеале, вы должны выполнить всю блокирующую версию операции в отдельном потоке, но для этого вам понадобится 4.0.