web-dev-qa-db-fra.com

Comment synchroniser les données de base et un service Web REST de manière asynchrone et en même temps propager correctement toutes les erreurs REST dans l'interface utilisateur)

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

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 les données en cache 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é le WWDC10 Session 117 sur la construction d'une expérience utilisateur pilotée par le serveur, j'ai passé un peu de temps à vérifier le Objective Resource =, Core Resource , et RestfulCoreData frameworks.

Le framework Objective Resource ne parle pas à Core Data seul et est simplement une implémentation client REST. La ressource Core 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 le calque du modèle.

Tout semble correct jusqu'à présent et au début, 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 de données central local.

Core Resource arrive à envoyer toutes ses demandes au serveur lorsque vous appelez - (BOOL)save:(NSError **)error sur votre contexte d'objet géré et est donc en mesure de fournir une instance NSError correcte des demandes sous-jacentes au serveur échoue d'une manière ou d'une autre. Mais il bloque le thread appelant jusqu'à la fin de l'opération de sauvegarde. ÉCHOUER.

RestfulCoreData conserve votre -save: appelle intact et n'introduit pas de temps d'attente supplémentaire pour le thread client. Il surveille simplement le NSManagedObjectContextDidSaveNotification puis envoie les requêtes correspondantes au serveur dans le gestionnaire de notification. Mais de cette façon, le -save: l'appel se termine toujours correctement (enfin, étant donné que Core Data est d'accord avec les modifications enregistrées) et le code client qui l'a effectivement appelé n'a aucun moyen de savoir que la sauvegarde n'a pas pu se propager au serveur à cause de 404 ou 421 ou toute autre erreur côté serveur s'est produite. Et encore plus, le stockage local devient d'avoir les données mises à jour, mais le serveur ne connaît jamais les changements. ÉCHOUER.

Je recherche donc une solution possible/des pratiques courantes pour faire face à tous ces problèmes:

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

Des idées?

84
eploko

Vous devriez vraiment jeter un œil à 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 vers un cache local soutenu par Core Data. Il prend en charge un mode hors ligne pour fonctionner entièrement à partir du cache lorsqu'il n'y a pas de réseau disponible. Toute la synchronisation se produit sur un thread d'arrière-plan (accès réseau, analyse de la charge utile et fusion du contexte d'objet géré) et il existe un riche ensemble de méthodes déléguées pour que vous puissiez savoir ce qui se passe.

26
Blake Watters

Il y a trois composants de base:

  1. L'action d'interface utilisateur et la persistance du changement vers CoreData
  2. Persistance de cette modification sur le serveur
  3. Actualisation de l'interface utilisateur avec la réponse du serveur

Une NSOperation + NSOperationQueue aidera à garder les requêtes réseau ordonnées. Un protocole délégué aidera vos classes d'interface utilisateur à comprendre dans quel état se trouvent les requêtes 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 modifications peuvent être "poussées" vers votre serveur.

Ensuite, il y a la boucle d'interface utilisateur à prendre en compte, pour garder votre code propre, il serait bien d'appeler save: et d'avoir les modifications 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 doit être plutôt faible, mais si cela crée un décalage notable sur l'interface utilisateur, vous pouvez simplement extraire les identifiants d'entité de la notification de sauvegarde 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 en une seule 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é à effectuer 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 ne poussez pas sur chaque notification d'enregistrement.

18
ImHuntingWabbits

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

Core Data: Importer efficacement les données

Core Data: Change Management

Core Data: Multi-Threading avec Core Data

2
David Weiss

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 placez 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 tourbillon du nombre qui sert d'indicateur est atomique ou que vous allez avoir une condition de concurrence - dites 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 bloc de données plus volumineux ait été écrit et alors seulement écrivez "true" pour basculer le commutateur pour ainsi dire.

Pour les économies corrélées côté client, vous devez simplement conserver ces données et ne pas les enregistrer jusqu'à ce que vous obteniez OK du serveur, assurez-vous que vous disposez d'une option kinnf de restauration - par exemple, un moyen de supprimer le serveur a échoué.

Attention, cela ne sera jamais sûr à 100% sauf si vous effectuez 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 du serveur), mais cela vous coûtera au moins 2 déplacements 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.

0
ZXX