Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Data saving objects in background issue

What I'm trying todo in a nutshell is I am using a background queue to save JSON objects pulled from a web service to the Core Data Sqlite3 database. The saving takes place on a serialized background queue I've created via GCD, and saved to a secondary instance of NSManagedObjectContext that is created for that background queue. Once the save is complete I need to update the instance of NSManagedObjectContext that is on the main thread with the newly created/updated objects. The problem I am having though is the instance of NSManagedObjectContext on the main thread is not able to find the objects that were saved on the background context. Below is a list of actions I'm taking with code samples. Any thoughts on what I'm doing wrong?

  • Create a background queue via GCD, run all pre-processing logic and then save the background context on that thread:

.

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path, and return all created/updated objects in an array
    NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

    // save all changes object context
    [self saveManagedObjectContext];
});
  • The "saveManagedObjectContext" method basically looks at which thread is running and saves the appropriate context. I have verified that this method is working correctly so I will not place the code here.

  • All of this code resides in a singleton, and in the singleton's init method I am adding a listener for the "NSManagedObjectContextDidSaveNotification" and it calls the mergeChangesFromContextDidSaveNotification: method

.

// merge changes from the context did save notification to the main context
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
{
    NSThread *currentThread = [NSThread currentThread];

    if ([currentThread.name isEqual:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]) {

        // merge changes to the primary context, and wait for the action to complete on the main thread
        [_managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

        // on the main thread fetch all new data and call the completion block
        dispatch_async(dispatch_get_main_queue(), ^{

            // get objects from the database
            NSMutableArray *objects = [[NSMutableArray alloc] init];
            for (id objectID in savedObjectIDs) {
                NSError *error;
                id object = [_managedObjectContext existingObjectWithID:objectID error:&error];
                if (error) {
                    [self logError:error];
                } else if (object) {
                    [objects addObject:object];
                }
            }

            // remove all saved object IDs from the array
            [savedObjectIDs removeAllObjects];
            savedObjectClass = nil;

            // call the completion block
            //completion(objects);
            saveCompletionBlock(objects);

            // clear the saved completion block
            saveCompletionBlock = nil;
        });
    }
}

As you can see in the method above I am calling the "mergeChangesFromContextDidSaveNotification:" on the main thread, and I have set the action to wait until done. According to the apple documentation the background thread should wait until that action is complete before it continues with the rest of the code below that call. As I mentioned above once I run this code everything seems to work, but when I try to print out the fetched objects to the console I don't get anything back. It seems that the merge is not in fact taking place, or possibly not finishing before the rest of my code runs. Is there another notification that I should be listening for to ensure that the merge has completed? Or do I need to save the main object context after the merge, but before the fecth?

Also, I apologize for the bad code formatting, but it seems that SO's code tags don't like method definitions.

Thanks guys!

UPDATE:

I've made the changes that were recommended below, but still having the same problem. Below is the updated code I have.

This is the code that invokes the background thread saving processes

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path
    NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

    // listen for the merge changes from context did save notification
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // save all changes object context
    [self saveManagedObjectContext];
});

This is the code that is called with by the NSManagedObjectContextDidSaveNotification notification

    // merge changes from the context did save notification to the main context
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    NSThread *currentThread = [NSThread currentThread];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // get objects from the database
        NSMutableArray *objects = [[NSMutableArray alloc] init];
        for (id objectID in savedObjectIDs) {
            NSError *error;
            id object = [[self managedObjectContext] existingObjectWithID:objectID error:&error];
            if (error) {
                [self logError:error];
            } else if (object) {
                [objects addObject:object];
            }
        }

        // remove all saved object IDs from the array
        [savedObjectIDs removeAllObjects];
        savedObjectClass = nil;

        // call the completion block
        //completion(objects);
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}

UPDATE:

So I found the solution. Turns out that the way I was saving out the object IDs on the background thread and then trying to use them on the main thread to re-fetch them wasn't working out. So I ended up pulling the inserted/updated objects from the userInfo dictionary that is sent with the NSManagedObjectContextDidSaveNotification notification. Below is my updated code that is now working.

As before this code starts the pre-prossesing and saving logic

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    // set the thead name
    NSThread *currentThread = [NSThread currentThread];
    [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

    [self logMessage:[NSString stringWithFormat:@"(%@) saveJSONObjects:objectMapping:class:completion:", [managedObjectClass description]]];

    // if there is not already a background context, then create one
    if (!_backgroundQueueManagedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
        if (coordinator != nil) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path
    [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (completion) {
        saveCompletionBlock = completion;
    }

    // listen for the merge changes from context did save notification
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // save all changes object context
    [self saveManagedObjectContext];
});

This is the modified method that handles the NSManagedObjectContextDidSaveNotification

- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // pull the objects that were saved from the notification so we can get them on the main thread MOC
        NSDictionary *userInfo = [notification userInfo];
        NSMutableArray *modifiedObjects = [[NSMutableArray alloc] init];
        NSSet *insertedObject = (NSSet *)[userInfo objectForKey:@"inserted"];
        NSSet *updatedObject = (NSSet *)[userInfo objectForKey:@"updated"];

        if (insertedObject && insertedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[insertedObject allObjects]];
        }
        if (updatedObject && updatedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[updatedObject allObjects]];
        }

        NSMutableArray *objects = [[NSMutableArray alloc] init];

        // iterate through the updated objects and find them in the main thread MOC
        for (NSManagedObject *object in modifiedObjects) {
            NSError *error;
            NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
            if (error) {
                [self logError:error];
            }
            if (obj) {
                [objects addObject:obj];
            }
        }

        modifiedObjects = nil;

        // call the completion block
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}
like image 236
miken.mkndev Avatar asked Oct 02 '12 14:10

miken.mkndev


2 Answers

I'm going to throw this out there. Stop following the best practices for concurrency listed in the Core Data Programming Guide. Apple has not updated it since adding nested contexts which are MUCH easier to use. This video goes into full detail: https://developer.apple.com/videos/wwdc/2012/?id=214

Setup your primary context to use your main thread (appropriate for handling UI):

NSManagedObjectContext * context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setPersistentStoreCoordinator:yourPSC];

For any object you create that may be doing concurrent operations, create a private queue context to use

NSManagedObjectContext * backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[backgroundContext setParentContext:context];
//Use backgroundContext to insert/update...
//Then just save the context, it will automatically sync to your primary context
[backgroundContext save:nil];

The QueueConcurrencyType refers to the queue the context will do it's fetch (save and fetch request) operations on. The NSMainQueueConcurrencyType context does all it's work on the main queue, which makes it appropriate for UI interaction. A NSPrivateQueueConcurrencyType does it on it's own private queue. So when you call save on the backgroundContext, it merges it's private data calling the parentContext using performBlock as appropriate automatically. You don't want to call performBlock on the private queue context in case it happens to be on the main thread which will cause a deadlock.

If you want to get really fancy, You can create a primary context as a private queue concurrency type (which is appropriate for background saving) with a main queue context for just your UI and then child contexts of your main queue context for background operations (like imports).

like image 119
Fruity Geek Avatar answered Sep 17 '22 11:09

Fruity Geek


I see you've worked out an answer that works for you. But I have been having some similar issues and wanted to share my experience and see if it is at all helpful to you or others looking at this situation.

Multi-threaded Core Data stuff is always a little confusing to read, so please excuse me if I misread your code. But it appears that there could be a simpler answer for you.

The core issue you had in the first attempt is that you saved off managed object IDs (supposedly the object identifiers that can be passed between threads) to a global variable for use on the main thread. You did this on a background thread. The problem was that you did this BEFORE saving to the background thread's managed object context. Object IDs are not safe to pass to another thread/context pair prior to a save. They can change when you save. See the warning in the documentation of objectID: NSManagedObject reference

You fixed this by notifying your background thread of the save, and inside that thread, grabbing the now-safe-to-use-because-the-context-has-been-saved object IDs from the notification object. These were passed to the main thread, and the actual changes were also merged into the main thread with the call to mergeChangesFromContextDidSaveNotification. Here's where you might save a step or two.

You are registering to hear the NSManagedObjectContextDidSaveNotification on the background thread. You can register to hear that same notification on the main thread instead. And in that notification you will have the same object IDs that are safe to use on the main thread. The main thread MOC can be safely updated using mergeChangesFromContextDidSaveNotification and the passed notification object, since the method is designed to work this way: mergeChanges docs. Calling your completion block from either thread is now safe as long as you match the moc to the thread the completion block is called on.

So you can do all your main thread updating stuff on the main thread, cleanly separating the threads and avoiding having to pack and repack the updated stuff or doing a double save of the same changes to the persistent store.

To be clear - the Merge that happens is on the managed object contextand its in-memory state - the moc on the main thread is updated to match the one on the background thread, but a new save isn't necessary since you ALREADY saved these changes to the store on the background thread. You have thread safe access to any of those updated objects in the notification object, just as you did when you used it on the background thread.

I hope your solution is working for you and you don't have to re-factor - but wanted to add my thoughts for others who might see this. Please let me know if I've misinterpreted your code and I'll amend.

like image 22
giff Avatar answered Sep 19 '22 11:09

giff