ios - CoreData - 在后台上下文中删除对象时的不一致行为(附加测试项目)

标签 ios objective-c core-data

我们在我们的一个应用程序中遇到了与删除具有多个上下文的对象有关的奇怪行为。

在背景上下文中删除对象后,它仍然存在于其父对象的关系中。

错误发生在删除使用 existingObjectWithID 获取的对象时,但在使用 objectWithID 或通过 executeFetchRequest 时不会发生。

但是,由于文档表明 existingObjectWithID 是一种更安全的使用方法,我们宁愿不更改它并可能在其他地方引入崩溃。

例子

在下面的输出中,创建了 5 个子对象,然后将其一一删除。

设置

Children by fetchRequest in mainContext: 5
Children by fetchRequest in backgroundContext: 5
Children by parent relationship in mainContext: 5
Children by parent relationship in backgroundContext: 5

Parent on mainContext: {
    children =     (
        "93139831-EAC9-46AF-9B93-7AFBCAA3C380",
        "19E51ADE-4524-4285-9DF3-4B0DDE58FAA2",
        "73082A38-ECC3-45FA-995E-3ADD46671A46",
        "6D7752E3-44BF-4418-A9DD-607896167510",
        "CB325763-E340-4FF2-96E8-67206794C91B"
    );
    id = "69B4180C-91BB-4B4D-8F01-B1612C7B6B0E";
}

Parent on backgroundContext: {
    children =     (
        "19E51ADE-4524-4285-9DF3-4B0DDE58FAA2",
        "6D7752E3-44BF-4418-A9DD-607896167510",
        "73082A38-ECC3-45FA-995E-3ADD46671A46",
        "CB325763-E340-4FF2-96E8-67206794C91B",
        "93139831-EAC9-46AF-9B93-7AFBCAA3C380"
    );
    id = "69B4180C-91BB-4B4D-8F01-B1612C7B6B0E";
}

删除 child

*** Deleted child with ID: 93139831-EAC9-46AF-9B93-7AFBCAA3C380
*** Deleted child with ID: 19E51ADE-4524-4285-9DF3-4B0DDE58FAA2
*** Deleted child with ID: 73082A38-ECC3-45FA-995E-3ADD46671A46
*** Deleted child with ID: 6D7752E3-44BF-4418-A9DD-607896167510
*** Deleted child with ID: CB325763-E340-4FF2-96E8-67206794C91B

删除后

Children by fetchRequest in mainContext: 0
Children by fetchRequest in backgroundContext: 0
Children by parent relationship in mainContext: 0
Children by parent relationship in backgroundContext: 5

Parent on mainContext: {
    children =     (
    );
    id = "69B4180C-91BB-4B4D-8F01-B1612C7B6B0E";
}

Parent on backgroundContext: {
    children =     (
        "19E51ADE-4524-4285-9DF3-4B0DDE58FAA2",
        "6D7752E3-44BF-4418-A9DD-607896167510",
        "73082A38-ECC3-45FA-995E-3ADD46671A46",
        "CB325763-E340-4FF2-96E8-67206794C91B",
        "93139831-EAC9-46AF-9B93-7AFBCAA3C380"
    );
    id = "69B4180C-91BB-4B4D-8F01-B1612C7B6B0E";
}

后台上下文中的 CDIParent 如何保留其子项,而在同一上下文中获取 CDIChild 却不返回任何值?

删除后改用 objectWithID

Children by fetchRequest in mainContext: 0
Children by fetchRequest in backgroundContext: 0
Children by parent relationship in mainContext: 0
Children by parent relationship in backgroundContext: 0

Parent on mainContext: {
    children =     (
    );
    id = "4C812CAB-4075-4A5C-9150-FDAEB4A6D238";
}

Parent on backgroundContext: {
    children =     (
    );
    id = "4C812CAB-4075-4A5C-9150-FDAEB4A6D238";
}

目前,我们使用 executeFetchRequest 作为解决方法,但问题表明我们的 CoreData 设置存在根本问题。

测试项目

我已经创建了一个测试应用程序来调试这个问题,可以在这里下载:

https://dl.dropboxusercontent.com/u/29710262/StackOverflow/CoreDataIssue.zip

主要源代码

//
//  AppDelegate.m
//  CoreDataIssue
//

#import "AppDelegate.h"
#import "CDIParent.h"
#import "CDIChild.h"
#import <CoreData/CoreData.h>

//-----------------------------------------------------------------
@implementation AppDelegate
//-----------------------------------------------------------------

//-----------------------------------------------------------------
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
//-----------------------------------------------------------------
{
    [self initCoreData];
    [self initObjects];
    [self recreateIssue];

    return YES;
}

#pragma mark - Private

//-----------------------------------------------------------------
- (void)initObjects;
//-----------------------------------------------------------------
{
    __block NSError *error = nil;

    // Create 5 entities on backgroundContext
    [self.backgroundContext performBlockAndWait:^{
        CDIParent *parent = [CDIParent parentInContext:self.backgroundContext error:&error];

        for (NSUInteger i = 0; i < 5; i++) {
            [CDIChild childInParent:parent error:&error];
        }

        // Save contexts
        [self saveContext:self.backgroundContext];
        [self.mainContext performBlockAndWait:^{
            [self saveContext:self.mainContext];
        }];
    }];

    [self debugChildrenWithComment:@"Created objects"];
}

//-----------------------------------------------------------------
- (void)recreateIssue;
//-----------------------------------------------------------------
{
    [self debugParents];

    // Remove all entities
    CDIParent *parent = [self parentInContext:self.mainContext];
    while (parent.children.count > 0) {
        [self deleteChild:parent.children.allObjects.firstObject];
    }

    [self debugParents];
}

//-----------------------------------------------------------------
- (void)deleteChild:(CDIChild *)child;
//-----------------------------------------------------------------
{
    __block NSError *error = nil;
    NSString *logID = child.childID;
    NSManagedObjectID *objectID = child.objectID;

    // Remove on backgroundContext
    [self.backgroundContext performBlockAndWait:^{

        // Lookup child in backgroundContext
        CDIChild *object = (CDIChild *) [self.backgroundContext existingObjectWithID:objectID error:&error];

        // Delete child
        [self.backgroundContext deleteObject:object];

        // Save contexts
        [self saveContext:self.backgroundContext];
        [self.mainContext performBlockAndWait:^{
            [self saveContext:self.mainContext];
        }];
    }];

    [self debugChildrenWithComment:[NSString stringWithFormat:@"Deleted child with ID: %@", logID]];
}

//-----------------------------------------------------------------
- (CDIParent *)parentInContext:(NSManagedObjectContext *)context;
//-----------------------------------------------------------------
{
    NSError *error = nil;
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Parent"];
    CDIParent *parent = [context executeFetchRequest:fetchRequest error:&error].firstObject;

    if (error != nil) {
        NSLog(@"Error: %@", error);
    }

    return parent;
}

//-----------------------------------------------------------------
- (void)debugChildrenWithComment:(NSString *)comment;
//-----------------------------------------------------------------
{
    NSLog(@"*** %@", comment);
    NSError *error = nil;
    NSFetchRequest *fetchRequest = nil;

    // First, log children by fetch request

    fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Child"];

    NSLog(@"Children by fetchRequest in mainContext: %lu", (unsigned long) [self.mainContext countForFetchRequest:fetchRequest error:&error]);
    NSLog(@"Children by fetchRequest in backgroundContext: %lu", (unsigned long) [self.backgroundContext countForFetchRequest:fetchRequest error:&error]);

    // Second, log children by relationship

    fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Parent"];

    {
        CDIParent *parent = (CDIParent *) [self.mainContext executeFetchRequest:fetchRequest error:&error].firstObject;
        NSLog(@"Children by parent relationship in mainContext: %lu", (unsigned long) parent.children.count);
    }

    {
        CDIParent *parent = (CDIParent *) [self.backgroundContext executeFetchRequest:fetchRequest error:&error].firstObject;
        NSLog(@"Children by parent relationship in backgroundContext: %lu", (unsigned long) parent.children.count);
    }

    if (error != nil) {
        NSLog(@"Error: %@", error);
    }

    NSLog(@"\n");
}

//-----------------------------------------------------------------
- (void)debugParents;
//-----------------------------------------------------------------
{
    NSLog(@"Parent on mainContext: %@", [[self parentInContext:self.mainContext] log]);
    NSLog(@"Parent on backgroundContext: %@", [[self parentInContext:self.backgroundContext] log]);
}

#pragma mark - Core Data

//-----------------------------------------------------------------
- (void)initCoreData;
//-----------------------------------------------------------------
{
    NSError *error = nil;

    // Create Model

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

    // Create Persistent Store Coordinate

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataIssue.sqlite"];

    if ([[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error] == NO) {
        NSLog(@"Error while removing store: %@", error);
    }

    self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    // Create Contexts

    self.mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [self.mainContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
    [self.mainContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];

    self.backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [self.backgroundContext setParentContext:self.mainContext];
    [self.backgroundContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];

    [self debugChildrenWithComment:@"Core Data initialized"];
}

//-----------------------------------------------------------------
- (void)saveContext:(NSManagedObjectContext *)managedObjectContext;
//-----------------------------------------------------------------
{
    NSError *error = nil;

    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

#pragma mark - Application's Documents directory

//-----------------------------------------------------------------
- (NSURL *)applicationDocumentsDirectory;
//-----------------------------------------------------------------
{
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

//-----------------------------------------------------------------
@end

最佳答案

在您的 recreateIssue 方法中,您在主上下文中获取一个父对象并获得它的一个子对象,然后您将子对象传递给 deleteChild:,删除后台上下文中的 child 。您应该在其注册的相同上下文中删除子项。

考虑更改以下代码行...

CDIParent *parent = [self parentInContext:self.mainContext];

...就像这样...

CDIParent *parent = [self parentInContext:self.backgroundContext];

这是一步一步发生的事情:

  1. 新的托管对象插入到子上下文中并分配临时托管对象 ID。

  2. 子上下文已保存。保存只是将插入的对象传播到其父上下文,但不会更新存储。临时托管对象 ID 不变。

  3. 保存父上下文,并为其托管对象分配永久托管对象 ID。子上下文中的托管对象 ID 仍未更改。

  4. 对象是通过子上下文中的托管对象 ID 使用父上下文中的永久 ID 获得的。这导致在子上下文中创建来自父存储的托管对象的副本。保存子上下文时,删除内容会传播到父上下文。这对插入子上下文中的原始对象没有影响,它们仍然具有临时 ID。

  5. 最后,主上下文被保存,将删除传播到持久存储。被删除的对象不再出现在父上下文中,但是插入子上下文中的原始托管对象,仍然带有它们的临时 ID,仍然存在,因为它们从未被直接保存到持久存储中(而是它们的更改被合并到父上下文中)上下文),上下文从未重置。

解决方案

一旦对象被持久化,就将它们提取到子存储中(如上所示),或者在父上下文保存后的任何时间简单地在子上下文上调用 reset,如下所示下面:

[self.backgroundContext reset];

顺便说一句,我注意到您的自定义日志记录实现掩盖了重要的细节,特别是通过掩盖临时和永久托管对象 ID 之间的差异。在调试器中检查上下文的 registeredObjects 数组可以立即看出这些差异。您最好直接将数组传递给 NSLog,而不是使用自定义代码来描述对象。

关于ios - CoreData - 在后台上下文中删除对象时的不一致行为(附加测试项目),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23633960/

相关文章:

ios - 如何使用正则表达式检查私有(private)/本地 IP 地址

objective-c - 如何处理NSXMLParser中另一个标签内的标签

ios - 在 Xcode 11 中,实时预览按钮下方的按钮是什么?

ios - 一维条码扫描器 IOS (Xcode)

objective-c - 我如何将 C 和 Objective-C 合并到同一个文件中?

ios - 用 NSObject 替换 NSMutableArray 并在 UITableView 中读取它

ios - CoreData - 多个持久存储

ios - CoreData困境: entity without inverse relationship

ios - 核心数据 - 如何在第二个 TableView Controller 上重新加载 NSSet(子对象)

ios - 使用 HMCatalog 应用程序时出现 HomeKit 问题