Saving master data across multiple threads

I'm a bit confused about keeping Core Data multithreading.

I have the following setup NSManagedObjectContext

(same as MagicalRecord

):

SavingContext (NSPrivateQueueConcurrencyType) has child DefaultContext(NSMainQueueConcurrencyType)

      

Each save thread has its own context ( NSPrivateQueueConcurrencyType

) DefaultContext

as its parent.

So the question is , how can I rely on storing the same type across different threads if I need to guarantee uniqueness?

Here's a small example of a test (Test is a subclass of 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

      

First, it checks to see if Test

the newly created thread context (which has a parent DefaultContext

) exists , and if not, create it in the current thread context.

And here is the test code:

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];
});

      

It just starts two operations and tries to keep the same data. After creating the test, I save all contexts (current thread, context context and root save context). Most of the time there will be 2 tests. You can modify and add a semaphore to ensure that both threads are executing simultaneously.

Update 09/20/2014 I added the code provided by mitrenegade (see below) and now I Test.m

have a function:

-(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;
}

      

With two threads creating the same value (test case is at the beginning of the post) I have the following output:

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

      

But if I look at the base sqlite file, then there are no records at all (which means they are stuck in the main context)

+3


source to share


3 answers


There doesn't seem to be any validation of your real objects, so just because two objects with a "uniqueField" attribute set to "1" doesn't mean they can't exist at the same time, according to the model you are provided.

While both threads are running, each one inserts a new object with some value ("1") associated with some attribute ("uniqueField"). When Core Data merges contexts, there are no rules that say this is prohibited, so there will be two objects in the main context. These are unique objects with unique object identifiers. The same will happen if you create two "Person" objects with "name" = "John".

Master data automatically invokes specific validation methods for each field if you format your signature correctly as shown here.

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

In subclass NSManagedObject (Test.m) you need to have a signed method

-(BOOL)valide<YourFieldName>:error:

      



So try adding this code to your Test.m and put a breakpoint on it. This method should be called when the context is saved.

-(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;
}

      

When the context is saved, this check is automatically called by the master data. you can do whatever you want inside; I add logic that selects and compares the score.

Edit: I asked this question shortly after this thread and got some answers, but nothing super unambiguous. So I want to say that my answer works for our current situation, but apparently not very efficient. However, I have not yet found a solution that works for multiple threads without doing anything in validateForInsert. As far as I can tell, there is no way to simply set the parameter to be unique in the database.

Making a fetch request in validateForInsert is too expensive

+3


source


MagicalRecord already does most of the work required to perform the save in the background. Have a look +[MagicalRecord saveWithBlock]

(or [MagicalRecordStack saveWithBlock]

in MR 3.0). This method will send save operations to the background for you. However, for this to work correctly, you must update your data in the background context so as not to cross streaming boundaries. Typically use the following pattern:



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.

      

+1


source


Check out this link for optimal concurrency setting.

The best way I've found in coordinating persistence across multiple threads is with notifications ( NSManagedObjectContextDidSaveNotification

). When one context saves the system, it sends notifications to other contexts with an object containing the identifiers of the affected objects. This context can then use these objects and combine them.

See this post for details on customization .

0


source







All Articles