如何异步同步 CoreData 和 REST Web 服务,同时正确地将任何 REST 错误传播到 UI 中

嘿,我正在为我们的应用程序做模型层。

有些要求是这样的:

  1. 它应该可以在 iPhone OS 3.0 + 上运行。
  2. 我们的数据源是一个 RESTful Rails 应用程序。
  3. 我们应该使用核心数据在本地缓存数据。
  4. 客户机代码(我们的 UI 控制器)应该尽可能少地了解任何网络内容,并应该使用 Core Data API 查询/更新模型。

我已经查看了关于构建服务器驱动的用户体验的 WWDC10第117节,花了一些时间查看了 客观资源核心资源RestfulCoreData框架。

Objective Resource 框架本身并不与 Core Data 对话,它只是一个 REST 客户机实现。Core Resource 和 RestfulCoreData 都假设您在代码中与 Core Data 对话,它们解决了模型层后台的所有具体细节。

到目前为止,一切看起来都还不错,最初我认为 Core Resource 或 RestfulCoreData 都可以满足上述所有需求,但是... ... 有几个问题它们似乎都没有正确解决:

  1. 在将本地更新保存到服务器时,不应阻塞主线程。
  2. 如果保存操作失败,则应将错误传播到 UI,并且不应将任何更改保存到本地 CoreData 存储。

当您在托管对象上下文上调用 - (BOOL)save:(NSError **)error时,核心资源恰好向服务器发出所有请求,因此能够以某种方式向服务器提供底层请求的正确 NSERerror 实例。但是它会阻塞调用线程,直到保存操作完成。失败。

RestfulCoreData 保持 -save:调用的完整性,并且不会为客户端线程引入任何额外的等待时间。它只是监视 NSManagedObjectContextDidSaveNotification,然后在通知处理程序中向服务器发出相应的请求。但是通过这种方式,-save:调用总是成功地完成(好吧,假设 Core Data 同意保存的更改) ,实际调用它的客户机代码无法知道保存可能没有传播到服务器,因为一些 404421或任何服务器端错误发生。而且,本地存储变得更新了数据,但是服务器永远不会知道这些更改。失败。

因此,我正在寻找解决所有这些问题的可能方案/通用实践:

  1. 当网络请求发生时,我不希望调用线程阻塞每个 -save:调用。
  2. 我想以某种方式在 UI 中获得某些同步操作出错的通知。
  3. 如果服务器请求失败,我希望实际的核心数据保存也失败。

有什么想法吗?

18270 次浏览

You need a callback function that's going to run on the other thread (the one where actual server interaction happens) and then put the result code/error info a semi-global data which will be periodically checked by UI thread. Make sure that the wirting of the number that serves as the flag is atomic or you are going to have a race condition - say if your error response is 32 bytes you need an int (whihc should have atomic acces) and then you keep that int in the off/false/not-ready state till your larger data block has been written and only then write "true" to flip the switch so to speak.

For the correlated saving on the client side you have to either just keep that data and not save it till you get OK from the server of make sure that you have a kinnf of rollback option - say a way to delete is server failed.

Beware that it's never going to be 100% safe unless you do full 2-phase commit procedure (client save or delete can fail after the signal from the server server) but that's going to cost you 2 trips to the server at the very least (might cost you 4 if your sole rollback option is delete).

Ideally, you'd do the whole blocking version of the operation on a separate thread but you'd need 4.0 for that.

This becomes a sync problem and not one easy to solve. Here's what I'd do: In your iPhone UI use one context and then using another context (and another thread) download the data from your web service. Once it's all there go through the sync/importing processes recommended below and then refresh your UI after everything has imported properly. If things go bad while accessing the network, just roll back the changes in the non UI context. It's a bunch of work, but I think it's the best way to approach it.

Core Data: Efficiently Importing Data

Core Data: Change Management

Core Data: Multi-Threading with Core Data

There are three basic components:

  1. The UI Action and persisting the change to CoreData
  2. Persisting that change up to the server
  3. Refreshing the UI with the response of the server

An NSOperation + NSOperationQueue will help keep the network requests orderly. A delegate protocol will help your UI classes understand what state the network requests are in, something like:

@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

The protocol format will of course depend on your specific use case but essentially what you're creating is a mechanism by which changes can be "pushed" up to your server.

Next there's the UI loop to consider, to keep your code clean it would be nice to call save: and have the changes automatically pushed up to the server. You can use NSManagedObjectContextDidSave notifications for this.

- (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
}

The computational overhead for inserting the network operations should be rather low, however if it creates a noticeable lag on the UI you could simply grab the entity ids out of the save notification and create the operations on a background thread.

If your REST API supports batching, you could even send the entire array across at once and then notify you UI that multiple entities were synchronized.

The only issue I foresee, and for which there is no "real" solution is that the user will not want to wait for their changes to be pushed to the server to be allowed to make more changes. The only good paradigm I have come across is that you allow the user to keep editing objects, and batch their edits together when appropriate, i.e. you do not push on every save notification.

You should really take a look at RestKit (http://restkit.org) for this use case. It is designed to solve the problems of modeling and syncing remote JSON resources to a local Core Data backed cache. It supports an offline mode for working entirely from the cache when there is no network available. All syncing occurs on a background thread (network access, payload parsing, and managed object context merging) and there is a rich set of delegate methods so you can tell what is going on.