Najlepsze praktyki w kontekście podstawowych danych

Mam duże zadanie importowania danych podstawowych.
Powiedzmy, że mój podstawowy model danych wygląda tak:

Car
----
identifier 
type

Pobieram listę car info JSON z mojego serwera, a następnie chcę zsynchronizować ją z moimi podstawowymi danymi Car object, czyli:
Jeśli jest to nowy samochód - > utwórz nowy obiekt Core Data Car z nowej informacji.
Jeżeli samochód już istnieje - > zaktualizuj obiekt Core Data Car.

Więc chcę zrobić ten import w tle bez blokowania interfejsu i podczas gdy użyj przewijania widok tabeli samochodów, które przedstawiają wszystkie samochody.

Obecnie robię coś takiego:

// create background context
NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[bgContext setParentContext:self.mainContext];

[bgContext performBlock:^{
    NSArray *newCarsInfo = [self fetchNewCarInfoFromServer]; 

    // import the new data to Core Data...
    // I'm trying to do an efficient import here,
    // with few fetches as I can, and in batches
    for (... num of batches ...) {

        // do batch import...

        // save bg context in the end of each batch
        [bgContext save:&error];
    }

    // when all import batches are over I call save on the main context

    // save
    NSError *error = nil;
    [self.mainContext save:&error];
}];

Ale nie jestem pewien, czy dobrze robię, na przykład:

Czy Mogę używać setParentContext ?
Widziałem kilka przykładów, które używają tego w ten sposób, ale widziałem inne przykłady, które nie wywołują setParentContext, zamiast tego robią coś takiego:

NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
bgContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;  
bgContext.undoManager = nil;

Kolejną rzeczą, której nie jestem pewien, jest to, kiedy wywołać save w głównym kontekście, w moim przykładzie I po prostu wywołaj save na końcu importu, ale widziałem przykłady, które używają:

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
    NSManagedObjectContext *moc = self.managedObjectContext;
    if (note.object != moc) {
        [moc performBlock:^(){
            [moc mergeChangesFromContextDidSaveNotification:note];
        }];
    }
}];  

Jak wspomniałem wcześniej, chcę, aby użytkownik mógł wchodzić w interakcje z danymi podczas aktualizacji, co z tego, że ja użytkownik zmieni typ samochodu, podczas gdy import zmienia ten sam samochód, czy sposób, w jaki go napisałem, jest bezpieczny?

UPDATE:

Dzięki @ TheBasicMind świetne Wyjaśnienie próbuję zaimplementować opcję A, więc mój kod wygląda mniej więcej tak:

Jest to podstawowa konfiguracja danych w AppDelegate:

AppDelegate.m  

#pragma mark - Core Data stack

- (void)saveContext {
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            DDLogError(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}  

// main
- (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    _managedObjectContext.parentContext = [self saveManagedObjectContext];

    return _managedObjectContext;
}

// save context, parent of main context
- (NSManagedObjectContext *)saveManagedObjectContext {
    if (_writerManagedObjectContext != nil) {
        return _writerManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _writerManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_writerManagedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _writerManagedObjectContext;
}  

I tak teraz wygląda moja metoda importu:

- (void)import {
    NSManagedObjectContext *saveObjectContext = [AppDelegate saveManagedObjectContext];

    // create background context
    NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    bgContext.parentContext = saveObjectContext;

    [bgContext performBlock:^{
        NSArray *newCarsInfo = [self fetchNewCarInfoFromServer];

        // import the new data to Core Data...
        // I'm trying to do an efficient import here,
        // with few fetches as I can, and in batches
        for (... num of batches ...) {

            // do batch import...

            // save bg context in the end of each batch
            [bgContext save:&error];
        }

        // no call here for main save...
        // instead use NSManagedObjectContextDidSaveNotification to merge changes
    }];
}  

I mam też następujący obserwator:

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {

    NSManagedObjectContext *mainContext = self.managedObjectContext;
    NSManagedObjectContext *otherMoc = note.object;

    if (otherMoc.persistentStoreCoordinator == mainContext.persistentStoreCoordinator) {
        if (otherMoc != mainContext) {
            [mainContext performBlock:^(){
                [mainContext mergeChangesFromContextDidSaveNotification:note];
            }];
        }
    }
}];
Author: Eyal, 2014-07-09

3 answers

Jest to niezwykle mylący temat dla osób, które po raz pierwszy podchodzą do podstawowych danych. Nie mówię tego lekko, ale z doświadczeniem jestem przekonany, że dokumentacja Apple jest nieco myląca w tej sprawie (w rzeczywistości jest spójna, jeśli przeczytasz ją bardzo uważnie, ale nie ilustrują one odpowiednio, dlaczego łączenie danych pozostaje w wielu przypadkach lepszym rozwiązaniem niż poleganie na kontekstach rodzic/dziecko i po prostu zapisywanie od dziecka do rodzica).

The dokumentacja daje silne wrażenie, że konteksty rodzica/dziecka są nowym preferowanym sposobem przetwarzania w tle. Jednak Apple zaniedbuje podkreślić pewne silne zastrzeżenia. Po pierwsze, należy pamiętać, że wszystko, co pobierasz w kontekście dziecka, jest najpierw wyciągane przez rodzica. Dlatego najlepiej ograniczyć każde dziecko głównego kontekstu działające w głównym wątku do przetwarzania (edycji) danych, które zostały już przedstawione w interfejsie użytkownika w głównym wątku. Jeśli używasz go do ogólnych zadania synchronizacji prawdopodobnie będziesz chciał przetwarzać dane, które wykraczają daleko poza granice tego, co aktualnie wyświetlasz w interfejsie użytkownika. Nawet jeśli używasz NSPrivateQueueConcurrencyType, dla kontekstu edycji podrzędnej potencjalnie przeciągniesz dużą ilość danych przez główny kontekst, co może prowadzić do złej wydajności i blokowania. Teraz najlepiej nie uczynić głównego kontekstu potomkiem kontekstu używanego do synchronizacji, ponieważ nie będzie powiadamiany o aktualizacje synchronizacji, chyba że masz zamiar zrobić to ręcznie, plus będziesz wykonywać potencjalnie długo uruchomionych zadań w kontekście może trzeba być reagować na zapisów zainicjowanych jako kaskada z kontekstu edycji, który jest potomkiem głównego kontekstu, poprzez główny kontakt i aż do magazynu danych. Będziesz musiał ręcznie scalać dane, a także śledzić, co należy unieważnić w głównym kontekście i ponownie zsynchronizować. Nie jest to najłatwiejszy wzór.

What the Dokumentacja Apple nie wyjaśnia, że najprawdopodobniej potrzebujesz hybrydy technik opisanych na stronach opisujących "stary" sposób robienia rzeczy, a nowy sposób robienia rzeczy w kontekście rodzic-dziecko.

Najlepszym rozwiązaniem jest prawdopodobnie (i podaję ogólne rozwiązanie tutaj, najlepsze rozwiązanie może być zależne od szczegółowych wymagań), aby mieć nsprivatequeueconcurrencytype Zapisz kontekst jako najwyższy rodzic, który zapisuje bezpośrednio do datastore. [Edit: nie będziesz robił zbyt wiele bezpośrednio w tym kontekście], to daj temu kontekstowi zapisz co najmniej dwa bezpośrednie dzieci. Jeden twój główny kontekst NSMainQueueConcurrencyType, którego używasz dla interfejsu użytkownika [Edit: najlepiej być zdyscyplinowanym i unikać kiedykolwiek edytowania danych w tym kontekście], drugi a NSPrivateQueueConcurrencyType, którego używasz do edycji danych przez użytkownika, a także (w opcji A na załączonym diagramie) Twoje zadania synchronizacji.

Następnie tworzymy główny kontekst cel powiadomienia NSManagedObjectContextDidSave generowanego przez kontekst synchronizacji i wysyłania powiadomień .userInfo dictionary to the main context ' s mergeChangesFromContextDidSaveNotification:.

Kolejnym pytaniem do rozważenia jest miejsce, w którym umieszczasz kontekst edycji użytkownika (kontekst, w którym zmiany dokonane przez użytkownika są odzwierciedlane z powrotem w interfejsie). Jeśli działania użytkownika są zawsze ograniczone do edycji na małych ilościach prezentowanych danych, to uczynienie tego potomkiem głównego kontekstu ponownie użycie NSPrivateQueueConcurrencyType jest najlepszym rozwiązaniem i najłatwiejszym w zarządzaniu (save zapisze zmiany bezpośrednio w głównym kontekście i jeśli masz kontroler nsfetchedresultscontroller, odpowiednia metoda delegata zostanie wywołana automatycznie, aby twój interfejs mógł przetwarzać kontroler aktualizacji:didChangeObject: atIndexPath: forChangeType: newIndexPath:) (ponownie jest to opcja A).

Jeśli z drugiej strony działania użytkownika mogą skutkować przetwarzaniem dużej ilości danych, możesz chcieć rozważ uczynienie go kolejnym partnerem kontekstu głównego i kontekstu synchronizacji, tak aby kontekst zapisu miał trzy bezpośrednie dzieci. główna, sync (typ kolejki prywatnej) i edit (typ kolejki prywatnej). Pokazałem ten układ jako opcję B na schemacie.

Podobnie do kontekstu synchronizacji będziesz musiał [Edit: configure the main context to receive notifications] kiedy dane są zapisywane( lub jeśli potrzebujesz większej szczegółowości, gdy dane są aktualizowane) i podjąć działania w celu Scal dane (zazwyczaj używając mergeChangesFromContextDidSaveNotification:). Zauważ, że w takim układzie nie ma potrzeby, aby główny kontekst kiedykolwiek wywoływał metodę save:. Tutaj wpisz opis obrazka

Aby zrozumieć relacje rodzic/dziecko, weź opcję A: podejście rodzic-dziecko oznacza po prostu, że jeśli kontekst edycji pobierze NSManagedObjects, zostaną one "skopiowane do" (zarejestrowane) najpierw kontekstu zapisu, potem głównego kontekstu, a na końcu kontekstu edycji. Będziesz mógł dokonać zmian w następnie, gdy wywołasz save: w kontekście edycji, zmiany zostaną zapisane Tylko do głównego kontekstu. Trzeba by wywołać save: w głównym kontekście, a następnie wywołać save: w kontekście save, zanim zostaną one zapisane na dysk.

Podczas zapisywania od dziecka, aż do rodzica, różne powiadomienia nsmanagedobject change I save są wywoływane. Na przykład, jeśli używasz kontrolera wyników pobierania do zarządzania danymi dla interfejsu użytkownika, to metody delegowania będą wywołane, dzięki czemu można zaktualizować UI odpowiednio.

Niektóre konsekwencje: jeśli pobierasz obiekt i NSManagedObject A w kontekście edycji, zmodyfikuj go i zapisz, aby zmiany były zwracane do głównego kontekstu. Zmodyfikowany obiekt jest teraz zarejestrowany zarówno w kontekście głównym, jak i edycji. Byłoby to złym stylem, ale możesz teraz ponownie zmodyfikować obiekt w głównym kontekście i będzie on teraz inny niż obiekt, ponieważ jest przechowywany w kontekście edycji. If you then spróbuj wprowadzić dalsze modyfikacje do obiektu zapisanego w kontekście edycji, Twoje modyfikacje nie będą zsynchronizowane z obiektem w kontekście głównym, a każda próba zapisania kontekstu edycji spowoduje błąd.

Z tego powodu, przy takim układzie jak opcja A, dobrym wzorcem jest próba pobrania obiektów, ich modyfikacji, zapisania i zresetowania kontekstu edycji (np. [editcontext reset] z dowolną pojedynczą iteracją run-loop (lub w dowolnym bloku przekazanym do [editContext wykonanie:]). Najlepiej jest również być zdyscyplinowanym i unikać kiedykolwiek żadnych zmian w głównym kontekście. Ponadto, aby powtórzyć iterację, ponieważ całe przetwarzanie w głównym wątku jest głównym wątkiem, jeśli pobierasz wiele obiektów do kontekstu edycji, główny kontekst będzie wykonywał przetwarzanie fetch w głównym wątku , ponieważ obiekty te są iteracyjnie kopiowane z kontekstu rodzica do kontekstu potomnego. Jeśli przetwarzanych jest dużo danych, może to spowodować brak odpowiedzi w interfejsie użytkownika. Więc jeśli, dla przykład masz duży magazyn zarządzanych obiektów i masz opcję interfejsu użytkownika, która spowodowałaby ich edycję. To byłoby złym pomysłem w tym przypadku, aby skonfigurować aplikację jak opcja A. w takim przypadku opcja B jest lepszym zakładem.

Jeśli nie przetwarzasz tysięcy obiektów, opcja A może być całkowicie wystarczająca.

BTW nie przejmuj się zbytnio, którą opcję wybierzesz. Może to być dobry pomysł, aby zacząć od A i jeśli trzeba zmienić na B. To łatwiejsze niż ty może pomyśleć, aby dokonać takiej zmiany i zwykle ma mniej konsekwencji niż można się spodziewać.

 157
Author: TheBasicMind,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-10 12:23:08

Po pierwsze, kontekst rodzica/dziecka nie służy do przetwarzania w tle. Służą one do atomowych aktualizacji powiązanych danych, które mogą być tworzone w wielu kontrolerach widoku. Więc jeśli ostatni kontroler widoku zostanie anulowany, kontekst potomny może zostać wyrzucony bez niekorzystnego wpływu na rodzica. Jest to w pełni wyjaśnione przez Apple na dole tej odpowiedzi na [^1]. Teraz, że jest z drogi i nie dał się nabrać na wspólny błąd, można skupić się na tym, jak prawidłowo zrobić background Core Data.

Utwórz nowy koordynator trwałego sklepu (nie jest już potrzebny w systemie iOS 10, patrz aktualizacja poniżej) i kontekst kolejki prywatnej. Posłuchaj powiadomienia o zapisaniu i połącz zmiany z głównym kontekstem (w systemie iOS 10 kontekst ma właściwość, która robi to automatycznie)

Dla przykładu Apple zobacz "Earthquakes: wypełnianie podstawowego magazynu danych za pomocą kolejki w tle" https://developer.apple.com/library/mac/samplecode/Earthquakes/Introduction/Intro.html Jak widać z historia zmian w dniu 2014-08-19 dodali "Nowy przykładowy kod, który pokazuje, jak używać drugiego rdzenia stosu danych do pobierania danych z kolejki w tle."

Oto bit z AAPLCoreDataStackManager.m:

// Creates a new Core Data stack and returns a managed object context associated with a private queue.
- (NSManagedObjectContext *)createPrivateQueueContext:(NSError * __autoreleasing *)error {

    // It uses the same store and model, but a new persistent store coordinator and context.
    NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[AAPLCoreDataStackManager sharedManager].managedObjectModel];

    if (![localCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil
                                                  URL:[AAPLCoreDataStackManager sharedManager].storeURL
                                              options:nil
                                                error:error]) {
        return nil;
    }

    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [context performBlockAndWait:^{
        [context setPersistentStoreCoordinator:localCoordinator];

        // Avoid using default merge policy in multi-threading environment:
        // when we delete (and save) a record in one context,
        // and try to save edits on the same record in the other context before merging the changes,
        // an exception will be thrown because Core Data by default uses NSErrorMergePolicy.
        // Setting a reasonable mergePolicy is a good practice to avoid that kind of exception.
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;

        // In OS X, a context provides an undo manager by default
        // Disable it for performance benefit
        context.undoManager = nil;
    }];
    return context;
}

Oraz w AAPLQuakesViewController.m

- (void)contextDidSaveNotificationHandler:(NSNotification *)notification {

    if (notification.object != self.managedObjectContext) {

        [self.managedObjectContext performBlock:^{
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

Oto pełny opis jak zaprojektowana jest próbka:

Earthquakes: Using a "private" persistent store coordinator to fetch data in background

Większość aplikacji wykorzystujących Podstawowe Dane zatrudniaj jednego koordynatora sklepu trwałego, aby pośredniczył w dostępie do danego sklepu trwałego. Earthquakes pokazuje, jak używać dodatkowego" prywatnego " stałego koordynatora sklepu podczas tworzenia zarządzanych obiektów przy użyciu danych pobranych ze zdalnego serwera.

Architektura Aplikacji

Aplikacja wykorzystuje dwa podstawowe "stosy" danych (zdefiniowane przez istnienie stałego koordynatora sklepu). Pierwszy jest typowym stosem "ogólnego przeznaczenia"; drugi jest tworzony przez Widok kontroler specjalnie do pobierania danych ze zdalnego serwera(od iOS 10 drugi koordynator nie jest już potrzebny, patrz aktualizacja na dole odpowiedzi).

Główny koordynator persistent store jest zwracany przez obiekt singleton "stack controller" (instancję CoreDataStackManager). Zadaniem jej klientów jest stworzenie zarządzanego kontekstu obiektu do współpracy z koordynatorem [^1]. Kontroler stosu wysyła również właściwości dla zarządzanego modelu obiektowego używanego przez aplikację oraz miejsce przechowywania. Klienci mogą użyć tych ostatnich właściwości, aby skonfigurować dodatkowych stałych koordynatorów sklepu, aby pracowali równolegle z głównym koordynatorem.

Główny kontroler widoku, instancja kontrolera QuakesViewController, używa koordynatora persistent store kontrolera stosu do pobierania quakes ze sklepu persistent do wyświetlenia w widoku tabeli. Pobieranie danych z serwera może być długotrwałą operacją wymagającą znaczącej interakcji z trwały sklep, aby określić, czy rekordy pobrane z serwera są nowymi wstrząsami lub potencjalnymi aktualizacjami istniejących wstrząsów. Aby zapewnić, że aplikacja będzie reagować podczas tej operacji, kontroler widoku zatrudnia drugiego koordynatora do zarządzania interakcją ze sklepem trwałym. Konfiguruje koordynatora tak, aby korzystał z tego samego managed object model i persistent store, co koordynator główny zarządzany przez kontroler stosu. Tworzy zarządzany kontekst obiektu związany z Kolejka prywatna do pobierania danych ze sklepu i zatwierdzania zmian w sklepie.

[^1]: Obsługuje to podejście "podaj pałeczkę", w którym-szczególnie w aplikacjach iOS-kontekst jest przekazywany z jednego kontrolera widoku do drugiego. Główny kontroler widoku jest odpowiedzialny za tworzenie początkowego kontekstu i przekazywanie go do kontrolerów widoku potomnego w razie potrzeby.

Powodem tego wzorca jest zapewnienie, że zmiany w zarządzanym wykresie obiektów są odpowiednio ograniczone. Rdzeń Dane obsługują" zagnieżdżone " zarządzane konteksty obiektów, które pozwalają na elastyczną architekturę ułatwiającą obsługę niezależnych, anulowalnych zestawów zmian. W kontekście podrzędnym można zezwolić użytkownikowi na wprowadzenie zestawu zmian w zarządzanych obiektach, które następnie mogą zostać przekazane hurtowo rodzicowi (i ostatecznie zapisane w sklepie) jako pojedyncza transakcja lub odrzucone. Jeśli wszystkie części aplikacji po prostu pobierają ten sam kontekst z, powiedzmy, delegata aplikacji, powoduje to zachowanie trudne lub niemożliwe do utrzymania.

Aktualizacja: w iOS 10 Apple przeniosło synchronizację z poziomu pliku SQLITE do stałego koordynatora. Oznacza to, że możesz teraz utworzyć prywatny kontekst kolejki i ponownie użyć istniejącego koordynatora używanego przez główny kontekst bez tych samych problemów z wydajnością, które miałbyś robiąc to w ten sposób wcześniej, cool!

 13
Author: malhal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-31 22:46:40

Przy okazji ten dokument firmy Apple wyjaśnia ten problem bardzo wyraźnie. Wersja Swift dla wszystkich zainteresowanych

let jsonArray = … //JSON data to be imported into Core Data
let moc = … //Our primary context on the main queue

let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc

privateMOC.performBlock {
    for jsonObject in jsonArray {
        let mo = … //Managed object that matches the incoming JSON structure
        //update MO with data from the dictionary
    }
    do {
        try privateMOC.save()
        moc.performBlockAndWait {
            do {
                try moc.save()
            } catch {
                fatalError("Failure to save context: \(error)")
            }
        }
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}

I jeszcze prostsze, jeśli używasz NSPersistentContainer dla iOS 10 i nowszych

let jsonArray = …
let container = self.persistentContainer
container.performBackgroundTask() { (context) in
    for jsonObject in jsonArray {
        let mo = CarMO(context: context)
        mo.populateFromJSON(jsonObject)
    }
    do {
        try context.save()
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}
 5
Author: hariszaman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-04 13:45:34