Почему мое приложение рушится, когда я изменяю отношения Core Data в подклассе NSOperation?

Фон

У меня есть следующее дерево объектов:

Name                       Project       
Users                      nil           
  John                     nil            
    Documents              nil           
      Acme Project         Acme Project    <--- User selects a project
        Proposal.doc       Acme Project  
          12:32-12:33      Acme Project  
          13:11-13:33      Acme Project  
            ...thousands more entries here...
  • Пользователь может назначить группу проекту. Все потомки получают этот проект.

  • Это блокирует основной поток, поэтому я использую NSOperations.

  • Я использую одобренный Apple способ сделать это, наблюдая за NSManagedObjectContextDidSaveNotification и сливаясь в основной контекст.

Проблема

Мои сэйвы были сбой при следующей ошибке:

Failed to process pending changes before save. The context is still dirty after 100 attempts. Typically this recursive dirtying is caused by a bad validation method, -willSave, or notification handler.

Что я пробовал

Я удалил все сложности своего приложения и сделал самый простой проект, о котором я мог думать. И ошибка все еще происходит. Я пробовал:

  • Установка максимального количества операций в очереди на 1 или 10.

  • Вызов refreshObject:mergeChanges: в нескольких точках подкласса NSOperation.

  • Настройка политик объединения в контексте управляемого объекта.

  • Построение и анализ. Он пуст.

Мой вопрос

Как установить отношения в NSOperation без моего приложения сбой? Неужели это не может быть ограничение Core Data? Может ли это?

Код

Загрузите мой проект: http://synapticmishap.co.uk/CDMTTest1.zip

Главный контроллер

@implementation JGMainController

-(IBAction)startTest:(id)sender {
    NSManagedObjectContext *imoc = [[NSApp delegate] managedObjectContext];

    JGProject *newProject = [JGProject insertInManagedObjectContext:imoc];
    [newProject setProjectName:@"Project"];
    [imoc save];

        // Make an Operation Queue
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:1]; // Also crashes with a higher number here (unsurprisingly)

    NSSet *allTrainingGroupsSet = [imoc fetchAllObjectsForEntityName:@"TrainingGroup"];

    for(JGTrainingGroup *thisTrainingGroup in allTrainingGroupsSet) {
        JGMakeRelationship *makeRelationshipOperation = [[JGMakeRelationship alloc] trainGroup:[thisTrainingGroup objectID] withProject:[newProject objectID]];
        [queue addOperation:makeRelationshipOperation];
        makeRelationshipOperation = nil;
    }
}

    // Called on app launch.
-(void)setupLotsOfTestData {
         // Sets up 10000 groups and one project
}

@end

Сделать работу с отношением

@implementation JGMakeRelationshipOperation

-(id)trainGroup:(NSManagedObjectID *)groupObjectID_ withProject:(NSManagedObjectID *)projectObjectID_ {
    appDelegate = [NSApp delegate];
    imoc = [[NSManagedObjectContext alloc] init];
    [imoc setPersistentStoreCoordinator:[appDelegate persistentStoreCoordinator]];
    [imoc setUndoManager:nil];
    [imoc setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(mergeChanges:) 
                                                 name:NSManagedObjectContextDidSaveNotification 
                                               object:imoc];
    groupObjectID = groupObjectID_;
    projectObjectID = projectObjectID_;
    return self;
}

-(void)main {
    JGProject       *project        = (JGProject *)[imoc objectWithID:projectObjectID];
    JGTrainingGroup *trainingGroup = (JGTrainingGroup *)[imoc objectWithID:groupObjectID];
    [project addGroupsAssignedObject:trainingGroup];
    [imoc save];

    trainingGroupObjectIDs = nil;
    projectObjectID = nil;
    project = nil;
    trainingGroup = nil;
}

-(void)mergeChanges:(NSNotification *)notification {
    NSManagedObjectContext *mainContext = [appDelegate managedObjectContext];
    [mainContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
                                  withObject:notification
                               waitUntilDone:YES];  
}

-(void)finalize {
    appDelegate = nil;
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    imoc = nil;
    [super finalize];
}
@end


@implementation NSManagedObjectContext (JGUtilities)

-(BOOL)save {
         // If there an save error, I throw an exception
}

@end

Модель данных

Data Model

Обновление 1

Я уже экспериментировал, и даже без слияния исключение все еще бросается. Просто сохранить контекст управляемого объекта в другом потоке после изменения отношения достаточно.

У меня есть общий постоянный координатор хранилища с делегатом приложения. Я попытался создать отдельный NSPsistentStoreCoordinator для потока с тем же URL-адресом, что и хранилище данных, но Core Data жалуется.

Мне бы хотелось предложить, как я могу создать координатор для потока. Основные данные docs ссылаются на то, что есть способ сделать это, но я не вижу, как это сделать.

Ответ 1

Вы пересекаете потоки (потоки в этом случае), что очень плохо в CoreData. Посмотрите на это следующим образом:

  • startTest вызывается с помощью кнопки (это IBAction, при нажатии кнопки) в главном потоке
  • Ваш цикл for создает объект JGMakeRelationship с использованием инициализатора trainGroup: withProject: (это должно быть вызвано init и, вероятно, вызовом super, но это не вызывает этой проблемы).
  • Вы создаете новый контекст управляемого объекта в операции, в основном потоке.
  • Теперь очередь операций вызывает операции "основной" из рабочего потока (помещаем здесь точку останова, и вы увидите ее не в основном потоке).
  • Ваше приложение запускается, потому что вы получили доступ к Контексту управляемого объекта из другого потока, чем тот, который вы создали.

Решение:

Инициализировать контекст управляемого объекта в основном методе операции.