It seems I tried everything but it seems it works in main thread only. For example:
[SomeClass MR_createEntity];
[[NSManagedObjectContext MR_defaultContext] MR_saveWithOptions:MRSaveSynchronously completion:^(BOOL success, NSError *error) {
if (success) {
NSLog(#"You successfully saved your context.");
} else if (error) {
NSLog(#"Error saving context: %#", error.description);
}
}];
If this code is run in main thread then success == YES otherwise (in background thread) it gives success == NO. In both cases error == nil.
So is it impossible to call the saving in background thread?
Completion blocks are always called from the main thread, here's an example that should work:
Person *person = ...;
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
Person *localPerson = [person MR_inContext:localContext];
localPerson.firstName = #"John";
localPerson.lastName = #"Appleseed";
} completion:^(BOOL success, NSError *error) {
self.everyoneInTheDepartment = [Person findAll];
}];
Reference: https://github.com/magicalpanda/MagicalRecord/blob/master/Docs/Working-with-Managed-Object-Contexts.md
Finally I hadn't to create a workable project with fully background MagicalRecord work.
The best solution for me is to update database in the main thread only and to read the database in any thread (including background). Additionally I show custom progress view on database updating.
Related
according to this tutorial (https://github.com/magicalpanda/MagicalRecord/blob/master/Docs/Working-with-Managed-Object-Contexts.md) I tried to find my device and update it.
Person *person = ...;
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
Person *localPerson = [person MR_inContext:localContext];
localPerson.firstName = #"John";
localPerson.lastName = #"Appleseed";
} completion:^(BOOL success, NSError *error) {
self.everyoneInTheDepartment = [Person findAll];
}];
So I made:
CDDevice *device = [CDDevice MR_findFirstByAttribute:#"deviceName"
withValue:uniqueName];
Which found my device. After few IF statements where i test if device have proper session and authorization code I want to update it.
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
CDDevice * localDevice = [device MR_inContext:localContext];
[localDevice updateFromDictionary:messageDictionary];
} completion:^(BOOL success, NSError *error) {
NET_LOG(#"Updating current device %#", device);
}];
But all the time my localDevice is nil. Is it because MR_findFirstByAttribute running in different context? What is correct way to update my device?
All of this happing on my custom serial queue, because this code is in network part of project. (Receviver method with GCDAsyncUdpSocket )
Make sure you save your data before retrieving it in the background context.
Like the title says how does one go about accessing an NSManagedObject that has been created in one block and then needs to be accessed in the other. I have the following implementation and was wondering if it's correct.
__block Person *newPerson;
#weakify(self);
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
newPerson = [Person MR_createInContext:localContext];
newPerson.name = #"Bob";
} completion:^(BOOL success, NSError *error) {
#strongify(self);
// Called on main thread
PersonViewController *personVC = [[PersonViewController alloc] initWithPerson:newPerson];
[self.navigationController pushViewController:personVC animated:YES];
}];
Am I correct in not needing to access newPerson from a localContext in the completion handler because it'll be executed on the main thread?
EDIT
It looks like the following is the proposed way:
__block NSManagedObjectID *newPersonObjectID;
#weakify(self);
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
Person *newPerson = [Person MR_createInContext:localContext];
newPerson.name = #"Bob";
newPersonObjectID = newPerson.objectID;
} completion:^(BOOL success, NSError *error) {
#strongify(self);
// Called on main thread
Person *savedPerson = [[NSManagedObjectContext MR_defaultContext] objectWithID:newPersonObjectID];
PersonViewController *personVC = [[PersonViewController alloc] initWithPerson:savedPerson];
[self.navigationController pushViewController:personVC animated:YES];
}];
Solution
This answer and comments lead to the following solution.
A TemporaryID is being assigned to the object whilst it's being saved and therefore when trying to fetch the object with the TempID an exception occurs.
Rather than creating a whole new fetch request what can be done is asking the context to obtain the permanent IDs early and than acquiring the permanent ID of the object. For example:
__block NSManagedObjectID *newPersonObjectID;
#weakify(self);
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
Person *newPerson = [Person MR_createInContext:localContext];
newPerson.name = #"Bob";
[localContext obtainPermanentIDsForObjects:#[newPerson] error:NULL];
newPersonObjectID = newPerson.objectID;
} completion:^(BOOL success, NSError *error) {
#strongify(self);
// Called on main thread
Person *savedPerson = [[NSManagedObjectContext MR_defaultContext] objectWithID:newPersonObjectID];
PersonViewController *personVC = [[PersonViewController alloc] initWithPerson:savedPerson];
[self.navigationController pushViewController:personVC animated:YES];
}];
You can't directly pass managed objects between contexts. Each NSManagedObject can only be accessed by its own context.
You'll need to pass its objectID to the completion block, then have the main context fetch the object by calling one of the following methods:
-(NSManagedObject *)objectWithID:(NSManagedObjectID *)objectID
This will create a fault to an object with the specified objectID, whether or not it actually exists in the store. If it doesn't exist, anything that fires the fault will fail with an exception.
-(NSManagedObject *)existingObjectWithID:(NSManagedObjectID *)objectID
error:(NSError **)error
This will fetch the object from the store that has that ID, or return nil if it doesn't exist. Unlike objectWithID, the object won't be faulted; all its attributes will have been retrieved.
In either case, your local context must have saved the Person object to the store for the main context to be able to fetch it.
More details about objectID can be found in the Core Data Programming Guide
Edit by User Asking Question
This answer and comments lead to the correct solution.
A TemporaryID is being assigned to the object whilst it's being saved and therefore when trying to fetch the object with the TempID an exception occurs.
Rather than creating a whole new fetch request what can be done is asking the context to obtain the permanent IDs early and than acquiring the permanent ID of the object. For example:
__block NSManagedObjectID *newPersonObjectID;
#weakify(self);
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
Person *newPerson = [Person MR_createInContext:localContext];
newPerson.name = #"Bob";
[localContext obtainPermanentIDsForObjects:#[newPerson] error:NULL];
newPersonObjectID = newPerson.objectID;
} completion:^(BOOL success, NSError *error) {
#strongify(self);
// Called on main thread
Person *savedPerson = [[NSManagedObjectContext MR_defaultContext] objectWithID:newPersonObjectID];
PersonViewController *personVC = [[PersonViewController alloc] initWithPerson:savedPerson];
[self.navigationController pushViewController:personVC animated:YES];
}];
You should access the objectID outside of your block (on the main thread, for example) and then use it within your block. Something like:
NSManagedObjectID *objectID = appDelegate.myObject.objectId;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// use objectID here.
}
I am using nested contexts pattern to support multithreaded work with CoreData.
I have CoredDataManager singleton class and the inits of contexts are:
self.masterContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.masterContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
self.mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.mainContext.parentContext = self.masterContext;
For each insert operation on response from web service I use API of my CoreDataManager to get new managed context:
- (NSManagedObjectContext *)newManagedObjectContext {
NSManagedObjectContext *workerContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
workerContext.parentContext = self.mainContext;
return workerContext;
}
It looks something like (PlayerView class is subclass of NSManagedObject class):
[PlayerView insertIfNeededByUniqueKey:#"playerViewId" value:playerViewId inBackgroundWithCompletionBlock:^(NSManagedObjectContext *context, PlayerView *playerView) {
playerView.playerViewId = playerViewId;
playerView.username = playerViewDictionary[#"name"];
[context saveContextWithCompletionBlock:^{
//do something
} onMainThread:NO];//block invocation on background thread
}];
saveContextWithCompletionBlock method is implemented in NSManagedObjectContext category:
- (void)saveContextWithCompletionBlock:(SaveContextBlock)completionBlock onMainThread:(BOOL)onMainThread {
__block NSError *error = nil;
if (self.hasChanges) {
[self performBlock:^{
[self save:&error];
if (error) {
#throw [NSException exceptionWithName:NSUndefinedKeyException
reason:[NSString stringWithFormat:#"Context saving error: %#\n%#\n%#", error.domain, error.description, error.userInfo]
userInfo:error.userInfo];
}
if (completionBlock) {
if (onMainThread && [NSThread isMainThread]) {
completionBlock();
} else if (onMainThread) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
} else if ([NSThread isMainThread]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
completionBlock();
});
} else {
completionBlock();
}
}
}];
}
}
Then on some stage I'm calling method of CoreDataManager to save master context:
- (void)saveMasterContext; {
__block NSError *error;
[self.mainContext performBlock:^{
[self.mainContext save:&error];
[self treatError:error];
[self.masterContext performBlock:^{
[self.masterContext save:&error];
[self treatError:error];
}];
}];
}
I have two main classes, subclasses of NSManagedObject - PlayerView and Post.
PlayerView has relation one to many to Post.
PlayerView is saved and is ok. The Post is never saved and I get error:
CoreData: error: Mutating a managed object 0x17dadd80 (0x17daf930) after it has been removed from its context.
I think, that the problem is in contexts saving logic.
First of all, the error you're experiencing usually happens when the context in which you created the new managed object goes away (released) before you had the chance to save it.
Secondly, the best way to make sure the context is saved in the appropriate thread is to use performBlock or performBlockAndWait instead of trying to figure out which thread the context belongs to. Here's a sample "save" function that saves the context safely:
+ (BOOL)save:(NSManagedObjectContext *)context {
__block BOOL saved = NO;
[context performBlockAndWait: {
NSError *error;
saved = [context save:&error];
if (!saved) {
NSLog("failed to save: %#", error);
}
}]
return saved;
}
As for using nested private contexts (with main thread context as the parent), our team experienced some issues with that model (can't recall exactly what it was), but we decided to listen for NSManagedObjectContextDidSaveNotification and use mergeChangesFromContextDidSaveNotification to update contexts.
I hope this helps.
A great tutorial by Bart Jacobs entitled: Core Data from Scratch: Concurrency describes two approaches in detail, the more elegant solution involves parent/child managed object contexts, including how to properly save context.
the idea is to import server JSON response into core data without blocking UI on main thread, I still need the imported entities afterwards, after a whole morning testing/googling, I failed to find the right way to do this.
__block NSMutableArray *cars;
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
for (NSDictionary *carObject in carObjects) {
Notification *car = [Notification MR_importFromObject:carObject inContext:localContext];
[cars addObject:car];
}
} completion:^(BOOL success, NSError *error) {
if (success) {
for (Car *car in cars) {
// data may have invalid data or be nil
// [Car findAll] will have correct data though
}
}
}];
interestingly, when I use following code, it works. seems the importing is done in background thread as well!
I really don't know in which context the importing is done, but the UI blocking issue is gone.
__block NSMutableArray *cars;
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
for (NSDictionary *carObject in carObjects) {
Notification *car = [Notification MR_importFromObject:carObject]; // not pass in localContext
[cars addObject:car];
}
} completion:^(BOOL success, NSError *error) {
if (success) {
for (Car *car in cars) {
// data may have invalid data or be nil
// [Car findAll] will have correct data though
}
}
}];
I have a Core Data stack with a main managed object context with NSMainQueueConcurrencyType.
The user can initiate a task on a managed object that can take a long time, so it is performed on a separate context:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
Person *samePerson = (Person *)[context objectWithID:person.objectID];
[context performBlock:^{
// BLOCK 1
// do lots of work
// then update the managed object
samePerson.value = someCalculatedValue;
// save the private context
NSError *error;
if (![context save:&error]) {
NSLog(#"Error: %#", error);
}
[mainMOC performBlock:^{
NSError *error;
if (![mainMOC save:&error]) {
NSLog(#"Error saving: %#", error);
}
}];
}];
This works fine, and the main MOC gets updated properly, NSFetchedResultsController hooked up to it perform properly, etc.
The problem is with deleting. I have this setup to delete objects:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
[context performBlock:^{
// BLOCK 2
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Person"];
NSError *error;
NSArray *all = [context executeFetchRequest:request error:&error];
if (!all) {
NSLog(#"Error fetching: %#", error);
} else {
for (NSManagedObject *person in all) {
[context deleteObject:person];
}
NSError *error;
if (![context save:&error]) {
NSLog(#"Error saving: %#", error);
}
[mainMOC performBlock:^{
NSError *error;
if (![mainMOC save:&error]) {
NSLog(#"Error saving: %#", error);
}
}];
}
}];
Now if I do this delete operation (Block 2) during the time it takes to perform the long-duration task (Block 1), then the delete operation finishes quickly, and saves to main context. After a while Block 1 finishes, and when it saves the mainMOC at its end, we get a seemingly obvious crash:
CoreData could not fulfill a fault for ...
My question is: how do I perform a task such as Block 1 with the possibility of its object being deleted?
If these are both background tasks that cannot run at the same time, try using semaphores to protect access to them.
eg. for an instance variable:
dispatch_semaphore_t _backgroundProcessingSemaphore;
Lazily initialised using something like:
- (dispatch_semaphore_t)backgroundProcessingSemaphore
{
if (!_backgroundProcessingSemaphore) {
_backgroundProcessingSemaphore = dispatch_semaphore_create(1);
}
return _backgroundProcessingSemaphore;
}
Surround the critical code with:
dispatch_semaphore_wait(self.backgroundProcessingSemaphore, DISPATCH_TIME_FOREVER);
// Critical code
dispatch_semaphore_signal(self.backgroundProcessingSemaphore);
Only one critical section of code can then run at any point in time. The block that calls dispatch_semaphore_wait will block if the semaphore is already taken, until it is freed up.
You also probably want to think about splitting your long-duration task up so that it will run in discrete batches if you're not already doing so - this is useful if the long running background task timer is about to expire while you still have work to do - you can stop and restart from the appropriate point on next launch.
Other options would involve forcing a save on block 1 before block 2 saves itself, but this starts to get messy. Much easier to ensure the two competing blocks cannot overlap.