核心数据以一对多关系保存在后台
Posted
技术标签:
【中文标题】核心数据以一对多关系保存在后台【英文标题】:Core data save in Background with one-to-many relationship 【发布时间】:2014-01-18 13:42:51 【问题描述】:在一个具有一对多关系(一个“测试”,多个“度量”)的核心数据应用程序中,我曾经有这样的代码:
在 AppDelegate.m 中:
- (NSManagedObjectContext *)managedObjectContext
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
return _managedObjectContext;
在 TableViewController.m 中:
- (NSManagedObjectContext *)managedObjectContext
NSManagedObjectContext *context = nil;
id contextDelegate = [[UIApplication sharedApplication] delegate];
if ([contextDelegate performSelector:@selector(managedObjectContext)])
context = [contextDelegate managedObjectContext];
return context;
- (void)saveEntryButton:(id)sender
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
// Update existing test
self.test.number = self.numberTextField.text;
else // Create new test
self.test = [NSEntityDescription insertNewObjectForEntityForName:@"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
if (isSaving)
NSManagedObjectContext *context = [test managedObjectContext];
self.measure = [NSEntityDescription insertNewObjectForEntityForName:@"Measure" inManagedObjectContext:context];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
NSLog(@"Can't Save! %@ %@", error, [error localizedDescription]);
效果很好,但当然,[NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
可能需要几秒钟并阻塞 UI,所以我想在后台进行。
我花了几个小时阅读有关核心数据中并发性的所有内容(而且我对它很陌生),但我没有找到任何关于我的问题的信息:如何处理一对多关系背景保存?
到目前为止我已经尝试过:
在 AppDelegate.m 中
- (NSManagedObjectContext *)managedObjectContext
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator];
//_managedObjectContext = [[NSManagedObjectContext alloc] init];
//[_managedObjectContext setPersistentStoreCoordinator:coordinator];
return _managedObjectContext;
在 TableViewController.m 中
- (void)saveEntryButton:(id)sender
NSManagedObjectContext *context = [self managedObjectContext];
if (self.test)
// Update existing test
self.test.number = self.numberTextField.text;
else // Create new test
self.test = [NSEntityDescription insertNewObjectForEntityForName:@"Test" inManagedObjectContext:context];
self.test.number = self.numberTextField.text;
NSError *error = nil;
// Save the object to persistent store
if (![context save:&error])
NSLog(@"Can't Save! %@ %@", error, [error localizedDescription]);
if (isSaving)
NSManagedObjectContext *context = [test managedObjectContext];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.parentContext = context;
[temporaryContext performBlock:^
self.measure = [NSEntityDescription insertNewObjectForEntityForName:@"Measure" inManagedObjectContext:temporaryContext];
[test addWithMeasureObject:measure];
NSData *newDataArray = [NSKeyedArchiver archivedDataWithRootObject:plotDataArray];
self.measure.dataArray = newDataArray;
// push to parent
NSError *error;
if (![temporaryContext save:&error])
// handle error
NSLog(@"error");
// save parent to disk asynchronously
[context performBlock:^
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
NSError *error;
if (![context save:&error])
// handle error
NSLog(@"error");
];
];
当然,我收到一个 SIGABRT 错误,因为“测试”和“测量”不在同一个上下文中……我尝试了很多不同的东西,但我真的迷路了。 提前感谢您的帮助。
【问题讨论】:
【参考方案1】:我在这里看到两个问题:后台异步保存以及如何处理不同上下文中的对象。
首先是关于储蓄。你确定它会阻止你的UI线程而不是调用archivedDataWithRootObject
吗?如果保存本身相对较快,您可以考虑在后台队列中仅调用 archivedDataWithRootObject
,然后将结果传回主队列,您将在主队列中在 UI 上下文中进行保存。
如果还是保存时间过长,可以使用苹果推荐的后台异步保存方式。你需要两个上下文。一种上下文——我们称之为背景——是私有队列并发类型。它还配置了持久存储协调器。另一个上下文——我们称之为 UI——是主队列并发类型。它被配置为背景上下文作为父级。
使用用户界面时,您使用的是 UI 上下文。因此,在此上下文中插入、修改和删除所有托管对象。然后当你需要保存时:
NSError *error;
BOOL saved = [UIContext save:&error];
if (!saved)
NSLog(@“Error saving UI context: %@“, error);
else
NSManagedObjectContext *parent = UIContext.parentContext;
[parent performBlock:^
NSError *parentError;
BOOL parentSaved = [parent save:&parentError];
if (!parentSaved)
NSLog(@“Error saving parent: %@“, parentError);
];
UI 上下文的保存非常快,因为它不会将数据写入磁盘。它只是将更改推送到其父级。而且由于 parent 是私有队列并发类型,并且您在 performBlock
的块内进行保存,因此该保存发生在后台,而不会阻塞主线程。
现在关于您的示例中不同上下文中的不同托管对象。正如您所发现的,您不能将一个上下文中的对象设置为另一个上下文中对象的属性。您需要选择需要进行更改的上下文。然后将其中一个对象的 NSManagedObjectID 传输到目标上下文。然后使用上下文的方法之一从 ID 创建一个托管对象。最后将此对象设置为另一个对象的属性。
【讨论】:
非常感谢您的回答。你完全正确,主要问题是'archivedDataWithRootObject'本身,而不是保存!你让我很开心;) 当您将工作卸载到后台队列时,请确保您用作此方法输入的数据以线程安全的方式使用。如果那里使用了一些托管对象,则需要从对象 ID 重新实现它。【参考方案2】:基本上你在正确的轨道上,但缺少几个关键要素;
首先,您需要将测试从主要上下文转移到次要上下文 - 这是通过以下方式完成的;
//这是保存在你的main managedObjectContext中的对象;
NSManagedObjectID *currentTest = test.objectID;
可以在后台线程上创建用于添加相关对象的辅助上下文。您可以同时使用 NSBlockOperation 进行二次保存和创建上下文。
这是一个简单的例子,使用连接到 IBAction 的标准人/地址示例
- (IBAction)button1Click:(id)sender
NSError *saveError = nil;
// create instance of person to save in our primary context
Person *newParson = [[Person alloc]initIntoManagedObjectContext:self.mainContext];
newParson.name = @"Joe";
[self.mainContext save:&saveError];
//get the objectID of the Person saved in the main context
__block NSManagedObjectID *currentPersonid = newParson.objectID;
//we'll use an NSBlockOperation for the background processing and save
NSBlockOperation *addRelationships = [NSBlockOperation blockOperationWithBlock:^
// create a second context
NSManagedObjectContext *secondContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[secondContext setPersistentStoreCoordinator:coordinator];
NSError *blockSaveError = nil;
/// find the person record in the second context
Person *differentContextPerson = (Person*)[secondContext objectWithID:currentPersonid];
Address *homeAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
homeAddress.address = @"2500 1st ave";
homeAddress.city = @"New York";
homeAddress.state = @"NY";
homeAddress.zipcode = @"12345";
Address *workAddress = [[Address alloc]initIntoManagedObjectContext:secondContext];
workAddress.address = @"100 home Ave";
workAddress.city = @"Newark";
homeAddress.state = @"NJ";
homeAddress.zipcode = @"45612";
[differentContextPerson addAddressObject:homeAddress];
[differentContextPerson addAddressObject:workAddress];
[secondContext save:&blockSaveError];
];
[addRelationships start];
上面的initIntoManagedObjectContext是NSManagedObject子类中的简单辅助方法如下;
- (id)initIntoManagedObjectContext:(NSManagedObjectContext *)context
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:context];
self = [super initWithEntity:entity insertIntoManagedObjectContext:context];
return self;
Apple 文档中关于 NSBlockOperation 的重要说明: 您必须在将使用它的线程上创建托管上下文。如果您使用 NSOperation,请注意它的 init 方法是在与调用者相同的线程上调用的。因此,您不能在队列的 init 方法中为队列创建托管对象上下文,否则它与调用者的线程相关联。相反,您应该在 main(对于串行队列)或 start(对于并发队列)中创建上下文。
【讨论】:
感谢您的详细回答。 Alexei 下面的一个告诉我,后台保存不是主要问题,但是您的回答对我更好地理解后台线程中保存的核心数据有很大帮助。干杯。以上是关于核心数据以一对多关系保存在后台的主要内容,如果未能解决你的问题,请参考以下文章
在插入新的子对象时,父子对象传递与多级的一对多关系。 IOS核心数据