Подтвердить что ты не робот

Передовая практика фонового контекста основных данных

У меня есть большая задача импорта, которую я должен делать с основными данными.
Скажем, моя основная модель данных выглядит следующим образом:

Car
----
identifier 
type

Получаю список информации о машине JSON с моего сервера, а затем я хочу синхронизировать ее с моим основным объектом Car, что означает:
Если новый автомобиль → создайте новый объект Core Data Car из новой информации.
Если автомобиль уже существует → обновите объект Core Data Car.

Итак, я хочу сделать этот импорт в фоновом режиме, не блокируя пользовательский интерфейс, а при использовании прокручивает табличный вид автомобилей, в котором представлены все автомобили.

В настоящее время я делаю что-то вроде этого:

// 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];
}];

Но я не уверен, что делаю здесь правильные вещи, например:

Хорошо ли, что я использую setParentContext?
Я видел несколько примеров, которые используют его так, но я видел другие примеры, которые не называют setParentContext, вместо этого они делают что-то вроде этого:

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

Еще одна вещь, которую я не уверен, - это когда вызывать сохранение в основном контексте. В моем примере я просто вызываю save в конце импорта, но я видел примеры, которые используют:

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

Как я упоминал ранее, я хочу, чтобы пользователь мог взаимодействовать с данными во время обновления, так что, если пользователь меняет тип автомобиля, а импорт меняет тот же автомобиль, как я его написал?

UPDATE:

Благодаря @TheBasicMind отличное объяснение, я пытаюсь реализовать вариант A, поэтому мой код выглядит примерно так:

Это конфигурация основных данных в 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;
}  

Вот как выглядит мой метод импорта:

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

И у меня также есть следующий наблюдатель:

[[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];
            }];
        }
    }
}];
4b9b3361

Ответ 1

Это чрезвычайно запутанная тема для людей, которые впервые подходят к Core Data. Я не говорю это легко, но с опытом я уверен в том, что документация Apple несколько вводит в заблуждение по этому вопросу (на самом деле это непротиворечиво, если вы читаете ее очень осторожно, но они недостаточно иллюстрируют, почему слияние данных остается во многих случаях это лучшее решение, чем полагаться на родительские/дочерние контексты и просто экономить от ребенка до родителя).

Документация дает сильное впечатление, что родительский/дочерний контексты являются новым предпочтительным способом обработки фоновых изображений. Однако Apple пренебрегает некоторыми сильными оговорками. Во-первых, имейте в виду, что все, что вы получаете в свой детский контекст, сначала вытягивается через него родительский. Поэтому лучше всего ограничить любого дочернего элемента основного контекста, работающего на основном потоке, на обработку (редактирование) данных, которые уже были представлены в пользовательском интерфейсе основного потока. Если вы используете его для общих задач синхронизации, скорее всего, вы захотите обработать данные, которые выходят далеко за рамки того, что вы сейчас показываете в пользовательском интерфейсе. Даже если вы используете NSPrivateQueueConcurrencyType, для контекста редактирования ребенка вы потенциально будете перетаскивать большой объем данных через главный контекст, что может привести к плохой производительности и блокировке. Теперь лучше не делать основной контекст дочерним элементом контекста, который вы используете для синхронизации, потому что он не будет уведомлен об обновлениях синхронизации, если вы не сделаете это вручную, плюс вы будете выполнять потенциально долго выполняемые задачи на контекст, вам может потребоваться реагировать на сохранение, инициированное как каскад из контекста редактирования, который является дочерним из вашего основного контекста, через основной контакт и вниз до хранилища данных. Вам придется либо вручную объединить данные, либо, возможно, отслеживать то, что должно быть признано недействительным в основном контексте и повторной синхронизации. Не самый простой шаблон.

В документации Apple не говорится о том, что вам, скорее всего, понадобится гибрид методов, описанных на страницах, описывающих способ "старинного" ограничения контентов в потоке, а также новый способ создания родительского ребенка вещи.

Вероятнее всего, лучший вариант (и я предлагаю общее решение, лучшее решение может зависеть от ваших подробных требований), чтобы иметь контекст сохранения NSPrivateQueueConcurrencyType как самый верхний родительский элемент, который сохраняется непосредственно в хранилище данных. [Edit: вы не будете очень сильно заниматься этим контекстом], а затем дайте этому контексту сохранения как минимум двух прямых детей. Один из основных контекстов вашего NSMainQueueConcurrencyType, который вы используете для пользовательского интерфейса [Edit: лучше всего быть дисциплинированным и избегать любого редактирования данных в этом контексте), а другой - NSPrivateQueueConcurrencyType, который вы используете для редактирования пользовательских прав данных, а также (в вариант A на прилагаемой схеме) ваши задачи синхронизации.

Затем вы делаете основной контекст целью уведомления NSManagedObjectContextDidSave, созданного контекстом синхронизации, и отправляете уведомления. Словарь .userInfo в основной контекст mergeChangesFromContextDidSaveNotification:.

Следующий вопрос, который следует рассмотреть, - это место, где вы помещаете контекст редактирования пользователя (контекст, в котором изменения, сделанные пользователем, возвращаются обратно в интерфейс). Если действия пользователя всегда ограничиваются редактированием на небольших объемах представленных данных, то повторение этого дочернего элемента основного контекста с использованием NSPrivateQueueConcurrencyType - лучший выбор и самый простой способ управления (сохранение затем сохраняет изменения непосредственно в основной контекст, и если у вас есть NSFetchedResultsController, соответствующий метод делегата будет вызываться автоматически, чтобы ваш пользовательский интерфейс мог обрабатывать контроллер обновлений: didChangeObject: atIndexPath: forChangeType: newIndexPath:) (опять же это вариант A).

Если, с другой стороны, действия пользователя могут привести к обработке большого количества обрабатываемых данных, вам может потребоваться сделать его другим партнером основного контекста и контекста синхронизации, так что контекст сохранения имеет три прямых дочерних элемента. main, sync (частный тип очереди) и редактировать (тип частной очереди). Я показал эту схему как вариант B на диаграмме.

Аналогично контексту синхронизации вам нужно будет [Изменить: настроить основной контекст для получения уведомлений] при сохранении данных (или если вам нужна более подробная информация, когда данные обновляются) и предпринять действия для объединения данных (обычно используя mergeChangesFromContextDidSaveNotification:). Обратите внимание, что при таком расположении нет необходимости в том, чтобы основной контекст когда-либо вызывал метод save:. enter image description here

Чтобы понять отношения между родителями и дочерними элементами, воспользуйтесь Вариантом A: Подход родительского ребенка просто означает, что если контекст редактирования извлекает NSManagedObjects, они будут "скопированы в" (зарегистрированы с) сначала контекстом сохранения, затем основным контекстом, а затем, наконец, редактировать контекст. Вы сможете внести в них изменения, а затем при вызове save: в контексте редактирования изменения будут сохранены только в основном контексте. Вам нужно будет вызвать save: в главном контексте, а затем вызвать save: в контекст сохранения, прежде чем они будут записаны на диск.

При сохранении с дочернего элемента, до родителя, запускаются различные уведомления об изменении и сохранении NSManagedObject. Например, если вы используете контролер результатов выборки для управления вашими данными для вашего пользовательского интерфейса, тогда будут делегированы методы делегирования, чтобы вы могли обновить интерфейс.

Некоторые последствия: если вы извлекаете объект и NSManagedObject A в контекст редактирования, измените его и сохраните, чтобы изменения были возвращены в основной контекст. Теперь у вас есть модифицированный объект, зарегистрированный как для основного, так и для контекста редактирования. Это был бы плохой стиль, но теперь вы можете изменить объект снова в главном контексте, и теперь он будет отличаться от объекта, поскольку он сохраняется в контексте редактирования. Если вы затем попытаетесь внести дополнительные изменения в объект, сохраненный в контексте редактирования, ваши изменения будут несовместимы с объектом в главном контексте, и любая попытка сохранить контекст редактирования вызовет ошибку.

По этой причине с помощью такой опции, как опция A, это хороший шаблон, который пытается получить объекты, изменить их, сохранить и reset контекст редактирования (например, [editContext reset] с любой отдельной итерацией run-loop (или внутри любого заданного блока, переданного в [editContext performBlock:]). Лучше также быть дисциплинированным и избегать любых изменений в основном контексте. Кроме того, для повторной итерации, поскольку вся обработка на главном является основным потоком, если вы выберете большое количество объектов в контексте редактирования, основной контекст будет выполнять извлечение обработки в основном потоке, поскольку эти объекты копируются итеративно из от родителей к ребенку. Если обрабатывается большое количество данных, это может привести к невосприимчивости к пользовательскому интерфейсу. Так что, например, у вас есть большой магазин управляемых объектов, и у вас есть опция пользовательского интерфейса, которая приведет к тому, что все они будут отредактированы. Было бы плохой идеей в этом случае настроить ваше приложение как вариант A. В таком случае вариант B - лучшая ставка.

Если вы не обрабатываете тысячи объектов, то вариант A может быть вполне достаточным.

Кстати, не беспокойтесь о том, какой вариант вы выберете. Возможно, неплохо начать с A, и если вам нужно перейти на B. Это легче, чем вы думаете, чтобы сделать такое изменение и обычно имеет меньше последствий, чем вы могли ожидать.

Ответ 2

Во-первых, родительский/дочерний контекст не для фоновой обработки. Они предназначены для атомных обновлений связанных данных, которые могут быть созданы в нескольких контроллерах представлений. Поэтому, если последний контроллер просмотра отменен, дочерний контекст может быть выброшен без каких-либо неблагоприятных воздействий на родителя. Это полностью объясняется Apple в нижней части этого ответа в [^ 1]. Теперь, когда вы не в курсе общей ошибки, вы можете сосредоточиться на том, как правильно выполнять базовые данные.

Создайте новый постоянный координатор хранилища (больше не нужен на iOS 10 см. обновление ниже) и контекст частной очереди. Слушайте уведомление о сохранении и объедините изменения в основной контекст (в iOS 10 контекст имеет свойство делать это автоматически)

Для примера, приведенного Apple, см. "Землетрясения: заполнение основного хранилища данных с использованием фоновой очереди" https://developer.apple.com/library/mac/samplecode/Earthquakes/Introduction/Intro.html Как вы можете видеть из истории изменений в 2014-08-19 годах, они добавили "Новый образец кода, который показывает, как использовать второй стек Core Data для извлечения данных в фоновом режиме".

Вот этот бит из 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;
}

И в AAPLQuakesViewController.m

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

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

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

Вот полное описание того, как образец был спроектирован:

Землетрясения: использование координатора хранилища "private" для получения данных в фоновом режиме

В большинстве приложений, использующих основные данные, используется один постоянный координатор хранилища для оповещения о доступе к данному постоянному хранилищу. Earthquakes показывает, как использовать дополнительный координатор хранилища "private" при создании управляемых объектов с использованием данных, полученных с удаленного сервера.

Архитектура приложений

В приложении используются два "стека" основных данных (как определено наличием постоянного координатора хранилища). Первый - это типичный стек "общего назначения"; второй создается контроллером представления специально для извлечения данных с удаленного сервера (с IOS 10 второй координатор больше не нужен, см. обновление в нижней части ответа).

Главный постоянный координатор хранилища обрабатывается одним элементом "контроллер стека" (экземпляр CoreDataStackManager). Клиенты должны создавать контекст управляемого объекта для работы с координатором [^ 1]. Контроллер стека также поддерживает свойства для модели управляемого объекта, используемой приложением, и расположение постоянного хранилища. Клиенты могут использовать эти последние свойства для создания дополнительных постоянных координаторов хранилищ для совместной работы с основным координатором.

Контроллер главного представления, экземпляр QuakesViewController, использует постоянный координатор хранилища стека для поиска землетрясений из постоянного хранилища для отображения в виде таблицы. Получение данных с сервера может быть долговременной операцией, которая требует значительного взаимодействия с постоянным хранилищем, чтобы определить, являются ли записи, извлеченные с сервера, новыми землетрясениями или потенциальными обновлениями для существующих землетрясений. Чтобы гарантировать, что приложение может оставаться отзывчивым во время этой операции, контроллер просмотра использует второго координатора для управления взаимодействием с постоянным хранилищем. Он настраивает координатора на использование той же модели управляемого объекта и постоянного хранилища, что и главный координатор, управляемый контроллером стека. Он создает объект управляемого объекта, привязанный к частной очереди для извлечения данных из хранилища и фиксации изменений в хранилище.

[^ 1]: Это поддерживает подход "передать дубинку", в частности, в приложениях iOS - контекст передается с одного контроллера представления на другой. Контроллер корневого представления отвечает за создание начального контекста и передачу его в контроллеры дочерних представлений при необходимости.

Причиной этого шаблона является обеспечение того, чтобы изменения в графе управляемого объекта были соответствующим образом ограничены. Core Data поддерживает "вложенные" контексты управляемых объектов, которые обеспечивают гибкую архитектуру, которая упрощает поддержку независимых, аннулируемых наборов изменений. С помощью детского контекста вы можете разрешить пользователю создавать набор изменений для управляемых объектов, которые затем могут быть переданы родителям (и в конечном итоге сохранены в хранилище) в виде отдельной транзакции или отброшены. Если все части приложения просто извлекают один и тот же контекст, например, из делегата приложения, это затрудняет или невозможно поддерживать такое поведение.

Update:В iOS 10 Apple переместила синхронизацию с уровня файла sqlite до постоянного координатора. Это означает, что теперь вы можете создать контекст частной очереди и повторно использовать существующий координатор, используемый основным контекстом, без тех же проблем с производительностью, которые вы бы делали раньше, cool!

Ответ 3

Кстати, этот document Apple очень четко объясняет эту проблему. Быстрая версия выше для всех, кто интересуется

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)")
    }
}

И даже проще, если вы используете NSPersistentContainer для iOS 10 и выше

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)")
    }
}