Jak asynchronicznie synchronizować CoreData i usługę internetową REST, a jednocześnie poprawnie propagować wszelkie błędy REST do interfejsu użytkownika

85

Hej, pracuję tutaj nad warstwą modelu dla naszej aplikacji.

Oto niektóre wymagania:

  1. Powinien działać na iPhone OS 3.0+.
  2. Źródłem naszych danych jest aplikacja RESTful Rails.
  3. Powinniśmy buforować dane lokalnie przy użyciu danych podstawowych.
  4. Kod klienta (nasze kontrolery UI) powinien mieć jak najmniejszą wiedzę na temat jakichkolwiek rzeczy w sieci i powinien odpytywać / aktualizować model za pomocą interfejsu API podstawowych danych.

Sprawdziłem sesję WWDC10 117 dotyczącą tworzenia środowiska użytkownika opartego na serwerze, spędziłem trochę czasu na sprawdzaniu frameworków Objective Resource , Core Resource i RestfulCoreData .

Struktura zasobów celu nie komunikuje się samodzielnie z danymi podstawowymi i jest jedynie implementacją klienta REST. Zasoby podstawowe i RestfulCoreData zakładają, że rozmawiasz z danymi podstawowymi w swoim kodzie i rozwiązują one wszystkie nakrętki i śruby w tle na warstwie modelu.

Jak na razie wszystko wygląda dobrze i początkowo myślałem, że albo Core Resource, albo RestfulCoreData pokryją wszystkie powyższe wymagania, ale ... Jest kilka rzeczy, których żadna z nich nie wydaje się rozwiązać poprawnie:

  1. Główny wątek nie powinien być blokowany podczas zapisywania lokalnych aktualizacji na serwerze.
  2. Jeśli operacja zapisywania nie powiedzie się, błąd powinien zostać propagowany do interfejsu użytkownika i żadne zmiany nie powinny być zapisywane w lokalnym magazynie danych podstawowych.

Zdarza się, że zasób podstawowy wysyła wszystkie swoje żądania do serwera, gdy wywołujesz - (BOOL)save:(NSError **)errorkontekst obiektu zarządzanego, a zatem jest w stanie w jakiś sposób dostarczyć poprawną instancję NSError podstawowych żądań do serwera. Ale blokuje wątek wywołujący do zakończenia operacji zapisywania. ZAWIEŚĆ.

RestfulCoreData zachowuje -save:połączenia bez zmian i nie wprowadza dodatkowego czasu oczekiwania na wątek klienta. Po prostu obserwuje, NSManagedObjectContextDidSaveNotificationa następnie wysyła odpowiednie żądania do serwera w module obsługi powiadomień. Ale ten sposób -save:rozmowa zawsze zakończy się pomyślnie (dobrze, biorąc pod uwagę dane Core jest w porządku z zapisanymi zmianami) oraz kod klienta, że faktycznie nazywa to nie ma sposobu, by poznać oszczędzania może nie udało się rozchodzą się z serwerem z powodu niektórych 404lub 421czy cokolwiek Wystąpił błąd po stronie serwera. Co więcej, lokalna pamięć masowa zaczyna aktualizować dane, ale serwer nigdy nie wie o zmianach. ZAWIEŚĆ.

Więc szukam możliwego rozwiązania / wspólnych praktyk w radzeniu sobie z tymi wszystkimi problemami:

  1. Nie chcę, aby wątek wywołujący blokował się przy każdym -save:połączeniu podczas wykonywania żądań sieciowych.
  2. Chcę w jakiś sposób otrzymywać powiadomienia w interfejsie użytkownika, że ​​jakaś operacja synchronizacji poszła nie tak.
  3. Chcę, aby rzeczywiste zapisywanie danych podstawowych również się nie powiodło, jeśli żądania serwera nie powiodą się.

Jakieś pomysły?

eploko
źródło
1
Wow, nie masz pojęcia, ile kłopotów oszczędziłeś mi zadając to pytanie. Obecnie zaimplementowałem moją aplikację, aby użytkownik czekał na dane za każdym razem, gdy wykonuję połączenie (choć z usługą internetową .NET). Zastanawiałem się, jak uczynić to asynchronicznym, ale nie mogłem wymyślić, jak to zrobić. Dziękujemy za wszystkie udostępnione zasoby!
Tejaswi Yerukalapudi
Świetne pytanie, dziękuję.
Justin
Link do podstawowego zasobu jest uszkodzony, czy ktoś wie, gdzie jest teraz hostowany?
Zasoby podstawowe są nadal hostowane na GitHub tutaj: github.com/mikelaurence/CoreResource
eploko
Oryginalną stronę można również znaleźć na gitHub: github.com/mikelaurence/coreresource.org
eploko

Odpowiedzi:

26

Naprawdę powinieneś rzucić okiem na RestKit ( http://restkit.org ) dla tego przypadku użycia. Jest przeznaczony do rozwiązywania problemów związanych z modelowaniem i synchronizacją zdalnych zasobów JSON z lokalną pamięcią podręczną wspieraną przez dane podstawowe. Obsługuje tryb offline do pracy całkowicie z pamięci podręcznej, gdy nie ma dostępnej sieci. Cała synchronizacja odbywa się w wątku w tle (dostęp do sieci, analizowanie ładunku i scalanie kontekstu obiektu zarządzanego), a istnieje bogaty zestaw metod delegatów, dzięki czemu można stwierdzić, co się dzieje.

Blake Watters
źródło
18

Istnieją trzy podstawowe elementy:

  1. Akcja interfejsu użytkownika i utrwalanie zmiany w CoreData
  2. Utrzymywanie tej zmiany aż do serwera
  3. Odświeżanie interfejsu użytkownika z odpowiedzią serwera

NSOperation + NSOperationQueue pomoże utrzymać porządek w żądaniach sieciowych. Protokół delegata pomoże twoim klasom UI zrozumieć, w jakim stanie są żądania sieciowe, na przykład:

@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

Format protokołu będzie oczywiście zależał od konkretnego przypadku użycia, ale zasadniczo tworzysz mechanizm, za pomocą którego zmiany mogą być „wypychane” na serwer.

Następnie należy wziąć pod uwagę pętlę interfejsu użytkownika, aby zachować porządek w kodzie, dobrze byłoby wywołać save: i automatycznie przesłać zmiany do serwera. W tym celu można użyć powiadomień 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
}

Obciążenie obliczeniowe związane z wstawianiem operacji sieciowych powinno być raczej niskie, jednak jeśli powoduje to zauważalne opóźnienie w interfejsie użytkownika, można po prostu pobrać identyfikatory jednostek z powiadomienia o zapisie i utworzyć operacje w wątku w tle.

Jeśli Twój interfejs API REST obsługuje przetwarzanie wsadowe, możesz nawet wysłać całą tablicę na raz, a następnie powiadomić interfejs użytkownika, że ​​wiele jednostek zostało zsynchronizowanych.

Jedynym problemem, który przewiduję i dla którego nie ma „prawdziwego” rozwiązania, jest to, że użytkownik nie będzie chciał czekać, aż jego zmiany zostaną przesłane na serwer, aby móc wprowadzić więcej zmian. Jedynym dobrym paradygmatem, z jakim się spotkałem, jest to, że pozwalasz użytkownikowi na dalsze edytowanie obiektów i grupowanie ich edycji razem w razie potrzeby, tj. Nie naciskasz na każde powiadomienie o zapisaniu.

ImHuntingWabbits
źródło
2

Staje się to problemem z synchronizacją i niełatwym do rozwiązania. Oto, co bym zrobił: w interfejsie użytkownika iPhone'a użyj jednego kontekstu, a następnie za pomocą innego kontekstu (i innego wątku) pobierz dane z usługi internetowej. Gdy wszystko będzie gotowe, przejdź przez procesy synchronizacji / importowania zalecane poniżej, a następnie odśwież interfejs użytkownika po prawidłowym zaimportowaniu wszystkiego. Jeśli coś pójdzie źle podczas uzyskiwania dostępu do sieci, po prostu cofnij zmiany w kontekście innym niż interfejs użytkownika. To sporo pracy, ale myślę, że to najlepszy sposób, aby do tego podejść.

Podstawowe dane: efektywne importowanie danych

Podstawowe dane: zarządzanie zmianami

Dane podstawowe: wielowątkowość z danymi podstawowymi

David Weiss
źródło
0

Potrzebujesz funkcji zwrotnej, która będzie działać w innym wątku (tym, w którym ma miejsce rzeczywista interakcja z serwerem), a następnie umieść kod wyniku / informacje o błędzie jako dane półglobalne, które będą okresowo sprawdzane przez wątek interfejsu użytkownika. Upewnij się, że zapis liczby, która służy jako flaga, jest atomowy lub będziesz mieć warunek wyścigu - powiedz, że jeśli odpowiedź na błąd ma 32 bajty, potrzebujesz int (który powinien mieć atomowy dostęp), a następnie zachowujesz ten int w stanie off / false / not-ready, dopóki większy blok danych nie zostanie zapisany, a dopiero potem napisz „true”, aby przełączyć przełącznik, że tak powiem.

Aby uzyskać skorelowane zapisywanie po stronie klienta, musisz albo po prostu zachować te dane i nie zapisywać ich, dopóki nie otrzymasz OK z serwera, lub upewnij się, że masz opcję kinnf wycofania - powiedzmy, że sposób usunięcia jest uszkodzony.

Uważaj, to nigdy nie będzie w 100% bezpieczne, chyba że wykonasz pełną procedurę zatwierdzania 2-fazowego (zapisywanie lub usuwanie klienta może się nie powieść po sygnale z serwera serwera), ale będzie to kosztować co najmniej 2 podróże na serwer ( może kosztować 4, jeśli jedyną opcją wycofania jest usunięcie).

Idealnie byłoby, gdybyś wykonał całą blokującą wersję operacji w osobnym wątku, ale do tego potrzebowałbyś 4.0.

ZXX
źródło