核心数据在多个线程中保存

时间:2014-09-10 04:37:55

标签: ios multithreading core-data magicalrecord

我对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中)

3 个答案:

答案 0 :(得分:3)

似乎没有对您的实际对象进行任何类型的验证检查,因此将“uniqueField”属性设置为“1”的两个对象并不意味着它们不能同时存在,根据你提供的模型。

当两个线程都在运行时,每个线程都插入一个新对象,其中某个值(“1”)与某个属性(“uniqueField”)相关联。当Core Data合并上下文时,没有规则说这是禁止的,因此主上下文中将有两个对象。它们是具有唯一objectID的唯一对象。如果您使用“name”=“John”创建了两个“Person”对象,则会发生同样的事情。

如果正确格式化签名,核心数据会自动为每个字段调用某些验证方法,如此处所示。

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/Validation.html

在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.