核心数据 - 使用数组保存导致 'Illegal attempt to establish a relationship'

Core data - Save with an array causing 'Illegal attempt to establish a relationship'

在核心数据中,我有一个通过桥实体连接的关系

问题中的关系是卡片 -> 段 -> 战士

一个段(或 SegmentRow)附加了一组战士。

如下图所示;

该应用程序当前的工作方式是,用户选择一名战士添加到细分行中。一旦他们点击保存,段行和分配给该行的战士就会被保存。

但我一直收到关系错误;

Illegal attempt to establish a relationship 'fighters' between objects in different contexts (source = Segment -- Name: Row 0 with 0 fighters , destination = Some fighter)'

一些注意事项:

卡片对象还不存在,这是因为需要先保存段;然后可以保存卡片对象。

所以我要保存的代码是这样的;

+ (void) saveSegments:(NSArray *)segments inContext:(NSManagedObjectContext *)context  withCompletion:(void (^)(BOOL success, NSError *error))completion
{
    int i = 0;
    for (FCSegmentRow *row in segments) {

        FCSegment *segment = [FCSegment MR_createEntityInContext:context];
        segment.name = [NSString stringWithFormat:@"Row %lu", (unsigned long)i];


        // Fighters
        NSMutableSet *fighters = [NSMutableSet set];

        for (FCFighter *fighter in row.fighters) {
            if (fighter) {
                [fighters addObject:fighter];
            }
        }

        segment.fighters = [NSSet setWithSet:fighters];


        i++;
    }
    [context MR_saveToPersistentStoreAndWait];


    // Output
    NSArray *allSegments = [self segmentsInContext:context error:nil];
    for (FCSegment *segment in allSegments) {
        NSLog(@"%@", [segment description]);
    }
}

FCSegmentRow 只是一个带有 NSArray *fighters 的简单 NSObject;

在我的代码中,我尝试设置一个 mutable 集合并向该集合添加战士,然后将其保存到核心数据。

另外,我尝试将保存放在一个块中等待完成;但是这个问题继续发生。

这似乎是因为战斗机与我正在创建的片段处于不同的上下文中。

所以,我的问题是——如何存储片段及其战士(关系)

编辑:战士是在哪里以及如何创造的?

参考问题

  1. 战士在自己的页面(视图控制器)上,有自己的托管对象上下文。

  2. 管理段的页面允许用户从战士列表中挑选;这将启动战士页面并等待 "return" selected 战士的完成块。

  3. 战士数据来自Firebase,加载到Core data中。

战士视图控制器就这样配置了;

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = @"Pick Fighter";

    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    self.context = [NSManagedObjectContext MR_context];

    [self reloadData];
}

// ... 

-(void) reloadData
{
        self.fighterList = [FCFighter findFightersInContext:self.context];

    [self.tableView reloadData];
}

如上所示,fighters 视图控制器有自己的上下文

编辑: 补充说明:

一张卡片由很多段组成,我称之为段行。

在 "segment row view controller" 上每个 table 单元格有 2 个按钮(一个段有 2 个战士),用户可以按下任一按钮并触发战士页面,让用户 select所需的战士。

我试图将与段行视图控制器中相同的上下文传递到战士页面,但发生的是当我按下第二个按钮时它丢失了第一个按钮的数据。

即: 按下按钮 A - 选择一名战士.. Joe Bloggs 按下按钮 B - 选择一名战士.. Mike Smith 当我return到段行时;按下按钮A的战斗机消失了;当我检查数据时 (null).

启动战士页面的代码如下;

UIStoryboard *mainStoryboard = [UIStoryboard mainStoryboard];
    FCFightersTableViewController *vc = (FCFightersTableViewController *) [mainStoryboard instantiateViewControllerWithIdentifier:@"FCFightersTableViewController"];
    vc.tableCellSelectable = YES;
    vc.excludeFighters = [selectedFightersMutable copy];
    //vc.context = self.context;
    vc.completionBlock = ^(FCFightersTableViewController *vc, FCFighter *fighter) {

        NSLog(@"Picked fighter - %@ (uuid: %@)", fighter.name, fighter.uuid);

        [fightersInRow setObject:fighter atIndexedSubscript:fighterIdx];
        segmentRow.fighters = [fightersInRow copy];


        NSLog(@"selected fighter - %@ (uuid: %@)", fighter.name, fighter.uuid);

        [fightersInRow setObject:fighter atIndexedSubscript:fighterIdx];
        segmentRow.fighters = [fightersInRow copy];

        [self.tableView beginUpdates];
        [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        [self.tableView endUpdates];
    };

只是想知道为什么要使用多个上下文?它的管理方式更加复杂,我只发现在使用多线程时有必要 - 例如,在实例化大量对象时可能会使 UI 的主线程变慢。 Coredata 基本上可以在一个上下文中很好地完成它的工作,你不会注意到任何缓慢。管理多个上下文需要大量计划,并且在您的情况下可能完全没有必要(但如果您别无选择,我们深表歉意)。

另一件事 - 你不应该去创建事物的 NSSet 然后将它们添加到其他对象。 Coredata 涵盖了所有内容(特别是如果您从 coredata 自动创建对象)。 NSSet 将准备就绪,您只需要在主上下文中创建对象并设置它们的关系属性 - parents 等

我接受@pbasdf 的回答。

这是我所做的。我没有使用全局上下文,而是保留它以便每个视图控制器都有自己的 NSManagedObjectContext。

当用户在 SegmentRow(页面 A)上时,用户可以按下一个按钮来启动战士列表(页面 B);返回完成块;

// (Page A) fires Page B

    UIStoryboard *mainStoryboard = [UIStoryboard mainStoryboard];
        FCFightersTableViewController *vc = (FCFightersTableViewController *) [mainStoryboard instantiateViewControllerWithIdentifier:@"FCFightersTableViewController"];

        vc.completionBlock = ^(FCFightersTableViewController *vc, NSManagedObjectID *fighterObjectID) {

            FCFighter *fighter = [self.context objectWithID:fighterObjectID];
            NSLog(@"Picked fighter - %@ (uuid: %@)", fighter.name, fighter.uuid);

            [fightersInRow replaceObjectAtIndex:fighterIdx withObject:fighter];
            segmentRow.fighters = [fightersInRow copy];

            [self.tableView beginUpdates];
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            [self.tableView endUpdates];
        };

请注意,我根据上述评论将其从 setObject 更改为 replaceObject

根据 pbasdf 的回答,我也得到了 ManagedObjectID 并使用上下文来获取战斗机。

// (Page B) fighters page fires the completionBlock
-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
 [tableView deselectRowAtIndexPath:indexPath animated:NO];

    FCFighter *fighter = nil;
        fighter = [self.fighterList objectAtIndex:indexPath.row];

    if (self.completionBlock)
    {
        self.completionBlock(self, fighter.objectID);
    }

    [self.navigationController dismissViewControllerAnimated:YES completion:^{
        [MBProgressHUD hideHUDForView:self.view animated:YES];
    }];
}

当我来救它的时候,我现在;

// Save the segments to Core data
    for (FCSegmentRow *row in segments) {

        FCSegment *segment = [FCSegment MR_createEntityInContext:context];
        segment.name = [NSString stringWithFormat:@"Row %lu", (unsigned long)i];

        [segment addFighters:[NSSet setWithArray:row.fighters]];
    }
    [context MR_saveToPersistentStoreAndWait];


    // Output log
    NSArray *allSegments = [self segmentsInContext:context error:nil];
    for (FCSegment *segment in allSegments) {
        NSLog(@"%@", [segment description]);
        for (FCFighter *fighter in [segment.fighters allObjects]) {
            NSLog(@"Fighter -- %@", fighter.name);
        }
    }

虽然我得到一个核心数据错误,但我会把它放在另一个问题中,当我点击输出日志时,它会显示给定段的战士。

我必须做一些检查,看看它是否正确地持久化和获取对象;但除此之外,我接受@pbasdf 的回答。

非常感谢