合并更改的 CoreData 和 NSFetchedResultsController

Posted

技术标签:

【中文标题】合并更改的 CoreData 和 NSFetchedResultsController【英文标题】:CoreData and NSFetchedResultsController with merging changes 【发布时间】:2014-01-24 19:03:08 【问题描述】:

我正在与我的第一个 CoreData 应用程序作斗争,但遇到了几个问题 - 我觉得我做错了什么。

在主线程上,我有一个带有 NSFetchedResultsController 的 UITableView,使用以下代码创建:

- (NSFetchedResultsController *)newFetchedResultsControllerWithSearch:(NSString *)searchString

NSSortDescriptor *sort = [[[NSSortDescriptor alloc] initWithKey:@"order" ascending:YES] autorelease];
NSArray* sortDescriptors = @[sort];

NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"contactId != 1"];

/*
 Set up the fetched results controller.
 */
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
// Edit the entity name as appropriate.
NSEntityDescription *callEntity = [NSEntityDescription entityForName:@"ContactDB" inManagedObjectContext:[[AppManager sharedAppManager] managedObjectContext]];
[fetchRequest setEntity:callEntity];

NSMutableArray *predicateArray = [NSMutableArray array];
if(searchString.length)

    // your search predicate(s) are added to this array
    [predicateArray addObject:[NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", searchString]];
    // finally add the filter predicate for this view
    if(filterPredicate)
    
        filterPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:filterPredicate, [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray], nil]];
    
    else
    
        filterPredicate = [NSCompoundPredicate orPredicateWithSubpredicates:predicateArray];
    


[fetchRequest setPredicate:filterPredicate];

// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];

[fetchRequest setSortDescriptors:sortDescriptors];

// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                                                                            managedObjectContext:[[AppManager sharedAppManager] managedObjectContext]
                                                                                              sectionNameKeyPath:nil
                                                                                                       cacheName:nil];
aFetchedResultsController.delegate = self;

NSError *error = nil;
if (![aFetchedResultsController performFetch:&error])

    /*
     Replace this implementation with code to handle the error appropriately.

     abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
     */
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();


return aFetchedResultsController;

我正在做一些后台下载来更新显示在 UITableView 中的数据。根据我在网上找到的信息,我正在创建新的 MOC 来进行这些更改,稍后我会将其与我的主线程 MOC 合并。

- (void)managedObjectContextDidSave:(NSNotification *)notification 
dispatch_async(dispatch_get_main_queue(), ^
    if(notification.object != [[AppManager sharedAppManager] managedObjectContext]) 
        [[[AppManager sharedAppManager] managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
    
);

对于合并委托,我使用的是标准 Apple 函数。

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller

UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView beginUpdates];


- (void)controller:(NSFetchedResultsController *)controller
 didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
       atIndex:(NSUInteger)sectionIndex
 forChangeType:(NSFetchedResultsChangeType) 

UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
switch(type)

    case NSFetchedResultsChangeInsert:
        [tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeDelete:
        [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
        break;



- (void)controller:(NSFetchedResultsController *)controller
  didChangeObject:(id)anObject
   atIndexPath:(NSIndexPath *)theIndexPath
 forChangeType:(NSFetchedResultsChangeType)type
  newIndexPath:(NSIndexPath *)newIndexPath

UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
switch(type)

    case NSFetchedResultsChangeInsert:
        [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeDelete:
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeUpdate:
        [self fetchedResultsController:controller configureCell:[tableView cellForRowAtIndexPath:theIndexPath] atIndexPath:theIndexPath];
        break;

    case NSFetchedResultsChangeMove:
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:theIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
        break;



- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller

UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView endUpdates];

问题 1:通知通过并且合并在 UITableView 中可见,但问题是过滤不适用于合并。正如您在我的过滤器中看到的那样,我跳过了contactId = 1,但是当合并完成时,这个联系人出现在UITableView 中。

我仍然不确定是否有更好的方法来对另一个线程上对数据库所做的更改做出反应 - 例如重新加载表视图和重新获取 UITableView 数据。

问题 2:当我在重新加载时快速滚动时,它会因消息而崩溃:

Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x19762a40 

只有当我在后台线程中删除并且它出现在负责创建 UITableViewCell 的代码中时才会发生这种情况 - 它在访问其中一个属性时崩溃。

编辑

作为我所做的解决方法:

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller

UITableView *tableView = controller == self.fetchedResultsController ? self.tableView : self.searchDisplayController.searchResultsTableView;
[tableView endUpdates];

[controller performFetch:nil];
[tableView reloadData];

所以我手动执行Fetch 并重新加载数据以再次过滤条目。

这是我构造 CoreData 相关对象的方法:

- (NSManagedObjectModel *)managedObjectModel

if(_managedObjectModel != nil) 
    return _managedObjectModel;

NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
self.managedObjectModel = [[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL] autorelease];
return _managedObjectModel;


- (NSManagedObjectContext *)managedObjectContext

if (_managedObjectContext != nil) 
    return _managedObjectContext;


self.managedObjectContext = [self createManagedObjectContextWithConcurrencyType:NSMainQueueConcurrencyType];
return _managedObjectContext;


- (NSManagedObjectContext *) createManagedObjectContextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrrencyType

NSManagedObjectContext* managedObjectContext = [[[NSManagedObjectContext alloc] initWithConcurrencyType:concurrrencyType] autorelease];

if(concurrrencyType == NSMainQueueConcurrencyType)

    [managedObjectContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];

else

    [managedObjectContext setParentContext:self.managedObjectContext];


return managedObjectContext;

这里是更新联系人的代码:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
             NSManagedObjectContext* context = [[AppManager sharedAppManager] createManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType];

             //clear database
             NSArray* allContacts = [context fetchObjectsForEntityName:@"ContactDB"];

             if([allContacts count] > 0)
             
                 for (ContactDB* contact in allContacts)
                 
                     [context deleteObject:contact];
                 
             

             NSArray* responseContacts = responseObject[@"contacts"];
             [responseContacts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) 
                 NSMutableDictionary* contactDict = [NSMutableDictionary dictionaryWithDictionary:obj];
                 ContactDB* contact = [[[ContactDB alloc] initWithDictionary:contactDict andContext:context] autorelease];
                 contact.order = [NSNumber numberWithInt:[order indexOfObject:contact.contactId]];
             ];

             [context save:nil];
             [[[AppManager sharedAppManager] managedObjectContext] performBlock:^
                 [[[AppManager sharedAppManager] managedObjectContext] save:nil];
             ];

             dispatch_async(dispatch_get_main_queue(), ^
                 [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_LOADING_STOPPED object:self userInfo:nil];
             );
         );

【问题讨论】:

真的需要检查这些保存。传入一个错误并检查它。如果您不这样做,您可能会出现无法检测到或无法正确响应的隐藏错误。任何时候 Objective-C 要求一个错误指针,设置它并检查它。传入nil 会否定任何其他调试。 是的,我知道这只是为了加快测试速度 - 我重构了 10 次编码以找出发生了什么 - 但我知道这没什么错误 :) 即使把错误代码放在那里。使用宏完成应用程序使其更容易。通过将错误代码放回原处,我解决了代码审查中的更多问题,我无法计算。即使在演示幻灯片中,我也将其放入。 【参考方案1】:

经过 2 天的环顾,我从头开始准备了一个更简单的问题示例,最终我发现了一个问题。我仍然不明白为什么它在加载的数据库中工作而不是更新但我改变了:

NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"contactId != 1"];

收件人:

NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"contactId != %@", @"1"];

我知道第一个是不正确的,因为它不是 NSString 而我的contactId 是 NSString,但为什么它在我加载应用程序时工作 - 当核心数据从磁盘加载时。对于使用 != 1 的合并和更新 NSPredicate 不起作用。

也许它会对某人有所帮助 - 确保你尊重类型的谓词。

【讨论】:

当我将NSPersistentContainerperformBackgroundTask() 中所做的更改合并到NSPersistentContainerviewContext 中时,同样的问题导致我的NSFetchedResultsController 只注意到删除而不是插入。 【参考方案2】:

如果您要创建多个 MOC,是否有不使用父/子设计的原因?如果您使用的是父/子,那么您确实不需要需要合并更改。这是父母/孩子的好处之一。

从错误中,我怀疑您违反了线程边界规则。问题是我关于使用父母/孩子的问题。

当您遇到此错误时,断点在哪里触发?哪一行代码负责触发该错误?那行代码是在哪个线程上触发的?

更新

谓词最后会是什么样子?您是否将最终谓词打印到控制台以查看它并确保它正在按照您认为的方式构建?我在那里看到很多和/或组合。

您的崩溃是否随着切换到父子节点而消失?我问是因为我查看了您的合并代码,它可能值得怀疑。最好写成:

- (void)managedObjectContextDidSave:(NSNotification *)notification 

    NSManagedObjectContext *moc = [[AppManaged sharedAppManager] managedObjectContext];
    [moc performBlock:^
        [moc mergeChangesFromContextDidSaveNotification:notification];
    ];

这样你就可以保证在正确的队列中进行合并。

【讨论】:

您好,感谢您的建议。我已经尝试过使用父/子,但是当我将子与父合并时,它会长时间阻塞主线程,并且通常需要更长的时间来更新(我正在处理大约 5000 个条目更新)。我对父母/孩子有一些问题 - 我将改变这种方法并发布代码。 此错误出现在创建单元格的代码上 - 当我尝试访问 CoreData ManagedObject 属性之一时。 错误出现在哪个线程上?如果您不在 UI 代码中的线程零上,那么线程就是您的问题。 如果您在 P/C 和线程方面遇到性能问题,那么您应该专注于解决该问题而不是避免它,因为您只是将性能损失传递给其他地方。将更新分成小块通常是最简单的答案。 那么 FetchResultController 在该合并中忽略 NSPredicates 的问题呢?它也会在父母/孩子身上得到解决?【参考方案3】:

我想注意的是,您可以将发布在另一个线程上的 NSNotification 对象传递给 mergeChangesFromContextDidSaveNotification 而无需调度。

此外,在更新联系人而不是调度的代码中,您应该使用NSManagedObjectContextperformBlock: 方法,因为它是使用NSPrivateQueueConcurrencyType 并发类型创建的。

所以想法是创建NSManagedObjectContext 并发类型NSPrivateQueueConcurrencyType,并将它的父级设置为您的主NSManagedObjectContext。然后将主 MOC 用于“NSFetchedResultsController”,将私有 MOC 用于更新数据。

[privateMOC performBlock:^
    // update your data
    [privateMOC save:NULL]; // you should use NSError though
    [privateMOC.parentContext performBlock:^
        [privateMOC.parentContext save:NULL]; // there your NSFetchedResultsController delegate methods should be fired automatically
    ];
];

如您所见,不需要调度。 而且,你真的应该使用 ARC。我敢肯定编译器比我们更有效率。

更新

NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"contactId != 1"];

/*
 Set up the fetched results controller.
 */
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] initWithEntityName:@"ContactDB"] autorelease];

if(searchString.length)

    // your search predicate(s)
    NSPredicate *searchTermPredicate = [NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", searchString];
    // finally add the filter predicate for this view
    filterPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:filterPredicate, searchTermPredicate, nil]];

【讨论】:

对不起,我没有得到第二个提示。关于在主队列上调度 - 我发现建议必须在主线程上完成合并 - 以正确获取 UI 更新。 我也建议使用父/子上下文。我现在用它的例子来更新我的答案。 我没有使用 ARC - 几个月前我在另一个项目中遇到了很多 ARC 性能问题 - 所以我更喜欢手动处理;) @GrzegorzKrukowski 更新了我的答案。确保设置父 MOC。 感谢代码。就问题而言,这与我的工作方式完全相同:) 仍然完全忽略了带有 contactId != 1 的 NSPRedicate ,并且当 NSFetchedResultsController 被触发时,不符合 NSPredicates 的对象被添加到 UITable :(

以上是关于合并更改的 CoreData 和 NSFetchedResultsController的主要内容,如果未能解决你的问题,请参考以下文章

更改 coredata 版本时合并数据

CoreData 合并冲突显示托管对象版本更改而不是数据

CoreData prepareForDeletion 调用无限次

合并 NSManagedObjectContexts 之间的更改(多线程)

当用户更改全局对象时重新初始化合并发布者

NSManagedObjectController 不合并从添加管理对象上下文的更改