Comment synchroniser CoreData et un service Web REST de manière asynchrone et en même temps propager correctement les erreurs REST dans l'interface utilisateur

85

Hé, je travaille sur la couche de modèle pour notre application ici.

Certaines des exigences sont les suivantes:

  1. Cela devrait fonctionner sur iPhone OS 3.0+.
  2. La source de nos données est une application RESTful Rails.
  3. Nous devons mettre en cache les données localement à l'aide de Core Data.
  4. Le code client (nos contrôleurs d'interface utilisateur) doit avoir le moins de connaissances possible sur les éléments du réseau et doit interroger / mettre à jour le modèle avec l'API Core Data.

J'ai vérifié la session WWDC10 117 sur la construction d' une expérience utilisateur conduite par le serveur, passé un certain temps à vérifier les ressources Objectif , des ressources essentielles et RestfulCoreData frameworks .

Le framework Objective Resource ne parle pas seul aux données de base et est simplement une implémentation de client REST. La ressource principale et RestfulCoreData supposent tous que vous parlez à Core Data dans votre code et ils résolvent tous les écrous et boulons en arrière-plan sur la couche de modèle.

Tout semble bien jusqu'à présent et au départ, je pensais que Core Resource ou RestfulCoreData couvrirait toutes les exigences ci-dessus, mais ... Il y a quelques choses qu'aucune d'entre elles ne semble résoudre correctement:

  1. Le thread principal ne doit pas être bloqué lors de l'enregistrement des mises à jour locales sur le serveur.
  2. Si l'opération d'enregistrement échoue, l'erreur doit être propagée à l'interface utilisateur et aucune modification ne doit être enregistrée dans le stockage Core Data local.

Core Resource envoie toutes ses demandes au serveur lorsque vous appelez - (BOOL)save:(NSError **)error votre contexte d'objet géré et peut donc fournir une instance NSError correcte des demandes sous-jacentes au serveur échouent d'une manière ou d'une autre. Mais il bloque le thread appelant jusqu'à ce que l'opération d'enregistrement se termine. ÉCHOUER.

RestfulCoreData garde vos -save:appels intacts et n'introduit aucun temps d'attente supplémentaire pour le thread client. Il surveille simplement le NSManagedObjectContextDidSaveNotification, puis émet les requêtes correspondantes au serveur dans le gestionnaire de notifications. Mais de cette façon, l' -save:appel se termine toujours avec succès (enfin, étant donné que Core Data est d'accord avec les modifications enregistrées) et le code client qui l'a réellement appelé n'a aucun moyen de savoir que la sauvegarde n'a pas pu se propager au serveur à cause de certains 404ou421 ou quoi que une erreur côté serveur s'est produite. Et plus encore, le stockage local devient d'avoir les données mises à jour, mais le serveur ne connaît jamais les changements. ÉCHOUER.

Donc, je recherche une solution possible / des pratiques courantes pour traiter tous ces problèmes:

  1. Je ne veux pas que le thread appelant se bloque à chaque -save:appel pendant que les requêtes réseau se produisent.
  2. Je souhaite en quelque sorte recevoir des notifications dans l'interface utilisateur indiquant qu'une opération de synchronisation a mal tourné.
  3. Je veux que la sauvegarde des données de base échoue également si les demandes du serveur échouent.

Des idées?

eploko
la source
1
Wow, vous ne savez pas à quel point vous m'avez sauvé en posant cette question. J'ai actuellement implémenté mon application pour que l'utilisateur attende les données chaque fois que je passe un appel (bien que vers un service Web .NET). J'ai réfléchi à un moyen de le rendre asynchrone mais je ne savais pas comment le faire. Merci pour toutes les ressources que vous avez fournies!
Tejaswi Yerukalapudi
Excellente question, merci.
Justin
Le lien vers Core Resource est rompu, est-ce que quelqu'un sait où il est hébergé maintenant?
Core Resource est toujours hébergé sur GitHub ici: github.com/mikelaurence/CoreResource
eploko
Et le site original peut également être trouvé sur gitHub: github.com/mikelaurence/coreresource.org
eploko

Réponses:

26

Vous devriez vraiment jeter un oeil à RestKit ( http://restkit.org ) pour ce cas d'utilisation. Il est conçu pour résoudre les problèmes de modélisation et de synchronisation des ressources JSON distantes avec un cache local basé sur Core Data. Il prend en charge un mode hors ligne pour travailler entièrement à partir du cache lorsqu'il n'y a pas de réseau disponible. Toutes les synchronisations se produisent sur un thread d'arrière-plan (accès réseau, analyse de la charge utile et fusion du contexte des objets gérés) et il existe un riche ensemble de méthodes de délégué afin que vous puissiez savoir ce qui se passe.

Blake Watters
la source
18

Il y a trois composants de base:

  1. L'action de l'interface utilisateur et la persistance du changement vers CoreData
  2. La persistance de ce changement jusqu'au serveur
  3. Actualisation de l'interface utilisateur avec la réponse du serveur

Une NSOperation + NSOperationQueue aidera à garder les requêtes réseau en ordre. Un protocole délégué aidera vos classes d'interface utilisateur à comprendre dans quel état se trouvent les demandes réseau, quelque chose comme:

@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

Le format du protocole dépendra bien sûr de votre cas d'utilisation spécifique, mais essentiellement ce que vous créez est un mécanisme par lequel les changements peuvent être «poussés» vers votre serveur.

Ensuite, il y a la boucle de l'interface utilisateur à considérer, pour garder votre code propre, il serait bien d'appeler save: et que les modifications soient automatiquement transmises au serveur. Vous pouvez utiliser les notifications NSManagedObjectContextDidSave pour cela.

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

La surcharge de calcul pour l'insertion des opérations réseau devrait être plutôt faible, mais si cela crée un retard notable sur l'interface utilisateur, vous pouvez simplement extraire les identifiants d'entité de la notification d'enregistrement et créer les opérations sur un thread d'arrière-plan.

Si votre API REST prend en charge le traitement par lots, vous pouvez même envoyer l'ensemble du tableau à la fois, puis vous informer de l'interface utilisateur que plusieurs entités ont été synchronisées.

Le seul problème que je prévois, et pour lequel il n'y a pas de «vraie» solution, c'est que l'utilisateur ne voudra pas attendre que ses modifications soient transmises au serveur pour être autorisé à apporter d'autres modifications. Le seul bon paradigme que j'ai rencontré est que vous permettez à l'utilisateur de continuer à éditer des objets et de regrouper leurs modifications le cas échéant, c'est-à-dire que vous n'appuyez pas sur chaque notification de sauvegarde.

ImHuntingWabbits
la source
2

Cela devient un problème de synchronisation et non facile à résoudre. Voici ce que je ferais: dans l'interface utilisateur de votre iPhone, utilisez un contexte, puis en utilisant un autre contexte (et un autre fil), téléchargez les données à partir de votre service Web. Une fois que tout est là, suivez les processus de synchronisation / importation recommandés ci-dessous, puis actualisez votre interface utilisateur une fois que tout a été importé correctement. Si les choses tournent mal lors de l'accès au réseau, annulez simplement les modifications dans le contexte hors interface utilisateur. C'est beaucoup de travail, mais je pense que c'est la meilleure façon de l'aborder.

Données de base: importer efficacement des données

Données de base: gestion du changement

Données de base: multithreading avec données de base

David Weiss
la source
0

Vous avez besoin d'une fonction de rappel qui va s'exécuter sur l'autre thread (celui où l'interaction réelle avec le serveur se produit), puis mettez le code de résultat / les informations d'erreur dans des données semi-globales qui seront périodiquement vérifiées par le thread d'interface utilisateur. Assurez-vous que le numéro qui sert de drapeau est atomique ou que vous allez avoir une condition de concurrence - disons que si votre réponse d'erreur est de 32 octets, vous avez besoin d'un int (qui devrait avoir un accès atomique), puis vous gardez cet int dans l'état off / false / not-ready jusqu'à ce que votre plus gros bloc de données ait été écrit et alors seulement écrivez "true" pour inverser le commutateur pour ainsi dire.

Pour l'enregistrement corrélé côté client, vous devez simplement conserver ces données et ne pas les enregistrer jusqu'à ce que vous obteniez OK du serveur ou assurez-vous que vous avez une option de restauration - par exemple, un moyen de supprimer est l'échec du serveur.

Sachez que cela ne sera jamais sûr à 100% à moins que vous n'effectuiez une procédure de validation complète en 2 phases (la sauvegarde ou la suppression du client peut échouer après le signal du serveur serveur) mais cela vous coûtera au moins 2 voyages vers le serveur ( peut vous coûter 4 si votre seule option de restauration est la suppression).

Idéalement, vous feriez toute la version de blocage de l'opération sur un thread séparé, mais vous auriez besoin de 4.0 pour cela.

ZXX
la source