我对Core Data多线程保存有点困惑。
我有NSManagedObjectContext
次设置(与MagicalRecord
相同):
SavingContext (NSPrivateQueueConcurrencyType) has child DefaultContext(NSMainQueueConcurrencyType)
每个保存线程都有自己的上下文(NSPrivateQueueConcurrencyType
),其中DefaultContext
为父级。
所以问题是:如果我需要保证唯一性,我如何依赖在不同线程上保存相同的类型?
这是一个小测试示例(Test是NSManagedObject的子类):
@implementation Test
+ (instancetype) testWithValue:(NSString *) str {
[NSThread sleepForTimeInterval:3];
Test *t = [Test MR_findFirstByAttribute:@"uniqueField" withValue:str];
if (!t) {
NSLog(@"No test found!");
t = [Test MR_createEntity];
}
t.uniqueField = str;
return t;
}
@end
它首先检查新创建的线程上下文(具有父Test
)中是否有DefaultContext
,如果没有 - 在当前线程上下文中创建它。
这是测试代码:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2;
[queue addOperationWithBlock:^{
[Test operationWithValue:@"1"];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
}];
[queue addOperationWithBlock:^{
[Test operationWithValue:@"1"];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"Total tests: %lu", (unsigned long)[Test MR_countOfEntities]);
[Test MR_truncateAll];
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
});
它只运行两个操作,并尝试保存相同的数据。创建测试后,我保存所有上下文(当前线程,默认上下文和根节省上下文)。大多数时候会有2个测试。您可以修改和添加信号量,以确保两个线程同时进行检查。
2014年9月20日更新
我添加了由mitrenegade提供的代码(见下文),现在我的Test.m
有一个功能:
-(BOOL)validateUniqueField:(id *)ioValue error:(NSError **)outError {
// The property being validated must not already exist
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([self class])];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"uniqueField == %@ AND self != %@", *ioValue, self];
int count = [self.managedObjectContext countForFetchRequest:fetchRequest error:nil];
if (count > 0) {
NSLog(@"Thread: %@ (isMain: %hhd), Validation failed!", [NSThread currentThread], [NSThread isMainThread]);
return NO;
}
NSLog(@"Thread: %@ (isMain: %hhd), Validation succeeded!", [NSThread currentThread], [NSThread isMainThread]);
return YES;
}
有两个线程创建相同的值(测试样本在post的开头)我有以下输出:
2014-09-20 11:48:53.824 coreDataTest[892:289814] Thread: <NSThread: 0x15d38940>{number = 3, name = (null)} (isMain: 0), Validation succeeded!
2014-09-20 11:48:53.826 coreDataTest[892:289815] Thread: <NSThread: 0x15e434a0>{number = 2, name = (null)} (isMain: 0), Validation succeeded!
2014-09-20 11:48:53.830 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:53.833 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:53.837 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:53.839 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:56.251 coreDataTest[892:289750] Total tests: 2
但是,如果我查看底层的sqlite文件,根本就没有记录(这意味着它们停留在Main Context中)
答案 0 :(得分:3)
似乎没有对您的实际对象进行任何类型的验证检查,因此将“uniqueField”属性设置为“1”的两个对象并不意味着它们不能同时存在,根据你提供的模型。
当两个线程都在运行时,每个线程都插入一个新对象,其中某个值(“1”)与某个属性(“uniqueField”)相关联。当Core Data合并上下文时,没有规则说这是禁止的,因此主上下文中将有两个对象。它们是具有唯一objectID的唯一对象。如果您使用“name”=“John”创建了两个“Person”对象,则会发生同样的事情。
如果正确格式化签名,核心数据会自动为每个字段调用某些验证方法,如此处所示。
在NSManagedObject子类(Test.m)中,您需要一个带签名的方法
-(BOOL)valide<YourFieldName>:error:
因此,请尝试将此代码添加到Test.m中,并在其上添加一个断点。保存上下文时应调用此方法。
-(BOOL)validateUniqueField:(id *)ioValue error:(NSError * __autoreleasing *)outError{
// The property being validated must not already exist
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([self class])];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"uniqueField == %@", *ioValue];
int count = [self.managedObjectContext countForFetchRequest:fetchRequest error:nil];
if (count > 0) {
if (outError != NULL) {
NSString *errorString = NSLocalizedString(
@"Object must have unique value for property",
@"validation: nonunique property");
NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString };
*outError = [[NSError alloc] initWithDomain:nil
code:0
userInfo:userInfoDict];
}
return NO;
}
return YES;
}
当上下文保存时,核心数据会自动调用此验证。你可以在这里做任何你想做的事;我正在添加执行获取的逻辑并比较计数。
编辑:我在这个主题后不久提出了这个问题,并得到了一些答案,但没有什么超级明确的。所以我想把它说出来,我的答案适用于我们目前的情况,但显然效率不高。但是,我还没有找到适用于多线程的解决方案,而无需在validateForInsert中执行任何操作。据我所知,没有办法只将参数设置为数据库中唯一的。
Is doing a fetch request in validateForInsert overly expensive
答案 1 :(得分:1)
MagicalRecord已经实现了在后台队列上执行保存所需的大部分工作。查看+[MagicalRecord saveWithBlock]
(或MR 3.0中的[MagicalRecordStack saveWithBlock]
)。此方法将为您分配保存操作到后台队列。但是,为了使其正常工作,您必须在背景上下文中进行数据更新,以便不跨越线程边界。通常,使用以下模式:
Test *testObj = ///....
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
Test *localTest = [testObj MR_inContext:localContext];
//localTest.property = newValue
}]; ///Exiting the block will save the context and your changes.
答案 2 :(得分:0)
查看this link以获得最佳的并发设置。
我在协调多个线程之间的保存时找到的最好方法是通过通知(NSManagedObjectContextDidSaveNotification
)。当一个上下文保存时,系统使用包含受影响对象的ID的对象向其他上下文发送通知。然后,这些上下文可以使用这些对象并合并保存。
有关设置的详细说明,请查看this post.