Magical Record detect no changes in default context - ios

I am using Magical Record for my app so I can make use of their core data stack that should automatically propagate my changes in my worker context (core data thread) to the default context (of the main thread). I have been creating and updating my objects in a core data write queue and everything was working fine.
Then I ran into this issue that Magical Record was able to save my changes on my worker context, but when it tries to save to the default context, it detects no changes and therefore doesn't save.
Where did I do wrong? In the rest of my app, I am creating and updating in pretty much the same way and it works. Please help. Thank you!
Below is the related code:
Where no changes were detected after all these changes:
dispatch_async(CoreDataWriteQueue(), ^{
if (self.person) {
FTPerson *localPerson = [FTPerson fetchWithID:self.person.id];
[localPerson setName:self.nameField.text];
[localPerson trainWithImages:self.addedImages];
} else {
FTPerson *newPerson = [[FTPerson alloc] initWithName:self.nameField.text andInitialTrainingImages:self.addedImages];
FTGroup *localGroup = [FTGroup fetchWithID:self.group.id];
[newPerson addGroup:localGroup];
}
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
});
I have also tried the saveWithBlock method and no luck:
dispatch_async(CoreDataWriteQueue(), ^{
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
if (self.person) {
FTPerson *localPerson = [FTPerson fetchWithID:self.person.id];
[localPerson setName:self.nameField.text];
[localPerson trainWithImages:self.addedImages];
} else {
FTPerson *newPerson = [[FTPerson alloc] initWithName:self.nameField.text andInitialTrainingImages:self.addedImages];
FTGroup *localGroup = [FTGroup fetchWithID:self.group.id];
[newPerson addGroup:localGroup];
}
}];
});
And here is where I created the person and group objects:
dispatch_async(CoreDataWriteQueue(), ^{
FTPerson *newPerson = [[FTPerson alloc] initWithName:#"test" andInitialTrainingImages:#[[UIImage imageNamed:#"test.jpg"]]];
});
dispatch_async(CoreDataWriteQueue(), ^{
FTGroup *newGroup = [[FTGroup alloc] init];
[self setGroup:newGroup];
});
Also the init methods:
Person
- (id)initWithName:(NSString *)name andInitialTrainingImages:(NSArray *)images {
NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
self = [FTPerson MR_createInContext:context];
self.name = name;
self.id = [[NSUUID UUID] UUIDString];
self.objectIDString = [[self.objectID URIRepresentation] absoluteString];
self.trainingImages = [[NSMutableArray alloc] init];
[context MR_saveToPersistentStoreAndWait];
return self;
}
Group
- (id)init {
NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
self = [FTGroup MR_createInContext:context];
self.id = [[NSUUID UUID] UUIDString];
self.didFinishTraining = NO;
self.didFinishProcessing = NO;
self.photosTrained = #(0);
self.lastProcessedDate = [NSDate date];
[context MR_saveToPersistentStoreAndWait];
return self;
}
FIXED
So the problem ended up being unrelated to magical record.
I was adding objects to my core data NSSet incorrectly. Instead of making it a NSMutableSet and do addObject:, I should have:
NSMutableSet *mutablePhotos = [self mutableSetValueForKey:#"photos"];
[mutablePhotos addObject:photo];

So the problem ended up being unrelated to magical record. I was adding objects to my core data NSSet incorrectly. Instead of making it a NSMutableSet and do addObject:, I should have:
NSMutableSet *mutablePhotos = [self mutableSetValueForKey:#"photos"];
[mutablePhotos addObject:photo];

I don't think you should be using your own dispatch queues when dealing with Core Data. The whole point of the -performBlock: methods on NSManagedObjectContext is that Core Data takes care of executing the provided block on the correct queue.
Now to answer your question. First, are you using MagicalRecord 2.x or 3.0? If 2.x, please make sure you use the develop branch, or alternatively grab the latest release (v2.3beta5 at this time). There are a lot of improvements in the latest dev and release branches.
Second, I think it is important when multithreading with Core Data to do two things:
Always turn on concurrency debugging by editing your scheme and setting -com.apple.CoreData.ConcurrencyDebug 1
Always be very explicit as to which context you mean to use. There is a reason that the method -MR_contextForCurrentThread is no longer recommended by the MagicalRecord team. Modify your create methods to take an NSManagedObjectContext as a parameter so there is never a doubt as to what context is creating it.
Try making the following changes:
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
if (self.person) {
FTPerson *localPerson = [self.person MR_inContext:localContext];
[localPerson setName:self.nameField.text];
[localPerson trainWithImages:self.addedImages];
} else {
FTPerson *newPerson = [[FTPerson alloc] initWithName:self.nameField.text andInitialTrainingImages:self.addedImages inContext:localContext];
FTGroup *localGroup = [self.group MR_inContext:localContext];
[newPerson addGroup:localGroup];
}
}];
Person:
- (id)initWithName:(NSString *)name andInitialTrainingImages:(NSArray *)images inContext:(NSManagedObjectContext*)context {
self = [FTPerson MR_createInContext:context];
self.name = name;
self.id = [[NSUUID UUID] UUIDString];
self.objectIDString = [[self.objectID URIRepresentation] absoluteString];
self.trainingImages = [[NSMutableArray alloc] init];
[context MR_saveToPersistentStoreAndWait];
return self;
}
For Person, I notice you pass in an array of images but you assign an empty array to the new person entity. Do you mean to do that?
Group:
-(id)initWithContext:(NSManagedObjectContext*)context {
self = [FTGroup MR_createInContext:context];
self.id = [[NSUUID UUID] UUIDString];
self.didFinishTraining = NO;
self.didFinishProcessing = NO;
self.photosTrained = #(0);
self.lastProcessedDate = [NSDate date];
[context MR_saveToPersistentStoreAndWait];
return self;
}

Related

MR_saveToPersistentStoreAndWait not saving data from array

I've an NSArray in which I'm adding objects after user selects multiple rows from a tableview. After selecting user press confirm and it saves the data. So based on some example I found here I have implemented the code but it seems as it is only saving one value at a time. The last value that user selects:
- (IBAction)confirmPressed:(id)sender {
NSLog(#"Selected Are: %# - %#",selectedDX,selectedDesc);
for (NSString *code in selectedDX) {
if (!_dxToAddEdit) {
self.dxToAddEdit = [Diagnoses MR_createEntity];
}
[self.dxToAddEdit setCode:code];
[self.dxToAddEdit setCodeDescription:#"Sample Description"];
[self.dxToAddEdit setSuperBill:_forSuperBill];
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
}
[self.navigationController popViewControllerAnimated:YES];
}
You are working only with one managed object self.dxToAddEdit. And it will contain the last code from array. If you want to save multiple objects you should do the following:
NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext];
for (NSString *code in selectedDX) {
Diagnoses *newDiagnose = [Diagnoses MR_createEntityInContext:defaultContext];
newDiagnose.code = code;
newDiagnose.codeDescription = #"Sample Description";
newDiagnose.superBill = _forSuperBill;
}
// Save recently created objects to persistent store.
[defaultContext MR_saveToPersistentStoreAndWait];

Core Data Lazy Loading with NSPrivateQueueConcurrencyType and custom setter not working

Problem: Fetching a managed object using a background thread does not lazy load the NSManaged object relationship correctly when the NSManaged object that is related has a custom setter. Doing fetch on main thread with main concurrency type works without a problem. Why is this?
Work Around: If I create a custom getter on the relationship object and check for nil, I can force the NSManaged object to load by calling other variables that don't have custom setter methods.
Background
The core data layout is pretty simple. I have a Game managed object and a Turn managed object. The turn object is a one to one relationship with the game object. I always fetch the game object in order to access the turn object. TurnImp and GameImp are implementation classes that inherit from the Game and Turn object so I don't put getter/setter methods in auto generated code.
Code
The Fetch
//
//Stick command on background
//
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
//
//Load Game
//
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
CoreDataHelper *coreDataHelper = appDelegate.coreDataHelper;
NSManagedObjectContext *childMOC = [coreDataHelper createChildManagedObjectContext];
//the request
NSFetchRequest *fetchRequest = [NSFetchRequest new];
//the object entity we want
NSEntityDescription *entity = [NSEntityDescription entityForName:GAMEIMP_GAME inManagedObjectContext:childMOC];
[fetchRequest setEntity:entity];
//the predicate rules, the what
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"gameId == %#", #"1404110671234567"];
[fetchRequest setPredicate:predicate];
//the sorting rules
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]initWithKey:GAMEIMP_OBJECT_ID ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc]initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
//Fetch results
NSFetchedResultsController *resultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:childMOC sectionNameKeyPath:nil cacheName:nil];
NSError *error;
BOOL success = [resultsController performFetch:&error];
GameImp *game;
if (success) {
game = [resultsController.fetchedObjects objectAtIndex:0];
} else {
NSLog(#"Unable to get game. Error: %#", error);
}
TurnImp *turnImp = game.turn;
//Issue is here!!! Should be 3, instead 0 because lastRoundReward is nil.
int lastRoundReward = [turnImp.lastRoundReward intValue];
//Work around, call custom getter method. Now 3 is returned.
lastRoundReward = [turnImp getLastRoundReward];
}
This childMOC creation
-(NSManagedObjectContext*) createChildManagedObjectContext {
NSManagedObjectContext *childMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
childMOC.parentContext = self.mainManagedObjectContext;
return childMOC;
}
TurnImp Header
#interface TurnImp : Turn
#property(atomic) BOOL isValid;
- (void) setLastRoundReward: (int) lastRoundReward;
- (int) getLastRoundReward;
#end
TurnImp M
#implementation TurnImp
#synthesize isValid;
#synthesize lastRoundReward = _lastRoundReward;
/**
* Set the last round reward
* #param -
* #return -
*/
- (void) setLastRoundReward: (int) lastRoundReward {
_lastRoundReward = [NSNumber numberWithInt:lastRoundReward];
}
/**
* Get the int value of lastRoundReward
*/
- (int) getLastRoundReward {
//Note - HACK! Lazy loading not working, try another member
if (self.lastRoundReward == nil) {
//Force load
NSString *objectId = self.objectId;
}
return [self.lastRoundReward intValue];
}
Change childMoc to mainMoc and it works. MainMoc Code
//create the main MOC
_mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
More After Fixed Concurrency issue
[childMOC performBlock:^{
// Execute the fetch on the childMOC and do your other work.
NSError *error;
NSArray *results = [childMOC executeFetchRequest:fetchRequest error:&error];
if (results == nil) {
// Handle error
} else if (results.count == 1) {
GameImp *game = [results firstObject];
TurnImp *turnImp = game.turn;
//Issue is here!!! Should be 3, instead 0 because lastRoundReward is nil.
int lastRoundReward = [turnImp.lastRoundReward intValue];
//Work around, call variable objectId (not same as ObjectId)
NSString *objectId = turnImp.objectId;
//not it's 3...
lastRoundReward = [turnImp.lastRoundReward intValue];
}
}];
Work Around
I removed the following from TurnImp and it works as expected with the relationships.
#synthesize lastRoundReward = _lastRoundReward;
First, I have to confess that I have no idea what your problem statement means - what is lazy loading of a relationship supposed to do anyway?
However, a quick glance at your code reveals that you are creating a MOC with NSPrivateQueueConcurrencyType yet you are not properly wrapping its use inside an appropriate performBlock invocation.
When you clearly violate the Core Data Concurrency guidelines, you are playing in dangerous waters and will get undefined behavior.
Also, why create an instance of NSFetchedResultsController just to perform a fetch? That's overkill. Simply use a fetch request. Like so...
[childMOC performBlock:^{
// Execute the fetch on the childMOC and do your other work.
NSError *error;
NSArray *results = [childMOC executeFetchRequest:fetchRequest error:&error];
if (result == nil) {
// Handle error
} else if (results.count == 1) {
GameImp *game = [results firstObject];
TurnImp *turnImp = game.turn;
int lastRoundReward = [turn2.lastRoundReward intValue];
}
}];

Saving in Magical Record?

I have written a code to parse some JSON and save data to database via magical record:
NSMutableArray *resultsArray = [NSMutableArray array];
NSArray *timesArray = JSON[#"results"];
for (NSDictionary *record in timesArray) {
Time *newTime = [Time MR_createEntity];
newTime.distance = record[#"distance"];
newTime.time = record[#"time"];
newTime.date = [[MMXFormatter instance] dateFromString:record[#"date"]];
newTime.createdAt = [[MMXFormatter instance] dateFromString:record[#"createdAt"]];
newTime.updatedAt = [[MMXFormatter instance] dateFromString:record[#"updatedAt"]];
[resultsArray addObject:newTime];
}
[MagicalRecord saveWithBlock:nil];
The above code does not save to persistent store. I haven't used Magical Record in a while, and seems saving is different from what it used to be. How do i save my data now?
If you want to use saveWithBlock, the code should be
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
Time *newTime = [Time MR_createEntityInContext:localContext];
newTime.distance = ...
...
}
another way is just replace saveWithBlock with MR_saveToPersistentStoreAndWait
NSMutableArray *resultsArray = [NSMutableArray array];
NSArray *timesArray = JSON[#"results"];
for (NSDictionary *record in timesArray) {
Time *newTime = [Time MR_createEntity];
newTime.distance = record[#"distance"];
newTime.time = record[#"time"];
newTime.date = [[MMXFormatter instance] dateFromString:record[#"date"]];
newTime.createdAt = [[MMXFormatter instance] dateFromString:record[#"createdAt"]];
newTime.updatedAt = [[MMXFormatter instance] dateFromString:record[#"updatedAt"]];
[resultsArray addObject:newTime];
}
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
For More Understanding about CoreData With MegicalRecord I would recommend you to go through this tutorial
http://code.tutsplus.com/tutorials/easy-core-data-fetching-with-magical-record--mobile-13680

Update CoreData object instead of inserting new object

I am trying to update a current CoreData object however when ever I use my write method its just adding another object to the database, so when I try to read the objects I get dozens of them back depending how long it's been used for.
Here is my code
#pragma mark -- Write
- (void)writeSeriesSearchObj:(NSMutableDictionary *)SearchDic Name:(NSString *)name
{
// WRITE TO CORE DATA
NSManagedObjectContext *context = [self managedObjectContext];
SeriesSearchObj *searchObj = [NSEntityDescription insertNewObjectForEntityForName:#"SeriesSearchObj" inManagedObjectContext:context];
if ([name isEqualToString:#"Code"]) {
searchObj.code = [SearchDic objectForKey:#"Code"];
} else if ([name isEqualToString:#"Name"]) {
searchObj.name = [SearchDic objectForKey:#"Name"];
} else if ([name isEqualToString:#"Model"]) {
searchObj.modelID = [SearchDic objectForKey:#"ModelID"];
}
NSError *error = nil;
[self.managedObjectContext save:&error];
// test
NSMutableArray *temptestA = [self readSearchObj];
NSLog(#"%#", temptestA);
}
I suspect I am going wrong using insertNewObjectForEntityForName however I am not sure how else to write this method in order for the same object to be updated every time?
SeriesSearchObj *searchObj = [NSEntityDescription insertNewObjectForEntityForName:#"SeriesSearchObj" inManagedObjectContext:context];
Will always return you a new object.
Use NSFetch to obtain the existing object and then update it.

Cannot add Managed Object ID to NSMutableArray in GCD with Child Managed Object Child Context

I'm using GCD as well as Core Data. I'm aware Core Data is not thread safe, so I create child Managed Object Context (tempContext) with the mainContext as the parent. MainContext has a PCS to save data. What I'm doing is:
Pull posts from Facebook
Save each post to Core Data by creating a new Managed Object called Post, updating it and saving it
while I process each post, if it needs additional handling, I add the post.obectID to an NSMutableArray of Managed Object IDS. This NSMutableArray is used by another process to finish updating the posts. I am using object IDs because the separate process will not be in the same tempContext, thus I will fetch the Post from Core Data using the object ID.
The process gets data, and I see it is stored within store.data (I use the product called Base to view the physical database file's content). But, I cannot seem to store the post's Object ID. As you see in code, I am using NSLog to print out the post's object ID, and I see them, thus I know the object IDs are there. Code also shows I am doing [tempContext save] and I am doing [mainContext save:], thus I should now have permanent, not temporary object IDs for posts. Again, the data is within Core data's physical file, but the mutable array count is still = 0.
What am I doing wrong to save the object IDs to NSMutableArray? In code you will see commented out where I've tried running the [mutableArray addObject:p.objectID] even on the main Queue, but it does not matter.
Here is my code:
- (void)processPosts {
NSLog(#"-- Begin processPosts --");
dispatch_async(_processPostsQ, ^{
for (NSDictionary * info in _posts)
{
NSManagedObjectContext * tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tempContext.parentContext = mainMOContext;
// If Post exists, rewrite over top of it. otherwise create a new one
NSFetchRequest * fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription * entity = [[mainMOModel entitiesByName] objectForKey:#"Post"];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"fb_post_id = %#",
[info objectForKey:#"post_id"]];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sd = [NSSortDescriptor sortDescriptorWithKey:#"fb_post_id" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sd]];
NSError * error = nil;
NSArray * fetchResults = [tempContext executeFetchRequest:fetchRequest error:&error];
Post * p = [NSEntityDescription insertNewObjectForEntityForName:#"Post" inManagedObjectContext:tempContext];
if (fetchResults.count > 0)
{
p = [fetchResults objectAtIndex:0];
}
NSDictionary * appData = [[NSDictionary alloc]init];
appData = [info objectForKey:#"app_data"];
p.fb_actor_id = [info objectForKey:#"actor_id"];
p.fb_app_data = [self archivedData:[info objectForKey:#"app_data"]];
p.fb_attachment = [self archivedData:[info objectForKey:#"attachment"]];
p.fb_created_time = [info objectForKey:#"created_time"];
p.timestamp = [info objectForKey:#"updated_time"];
p.fb_attribution = NULL_TO_NIL([info objectForKey:#"attribution"]);
p.fb_message = NULL_TO_NIL([info objectForKey:#"message"]);
p.fb_type = NULL_TO_NIL([info objectForKey:#"type"]);
p.fb_post_id = [info objectForKey:#"post_id"];
p.fb_likes_info = [self archivedData:[info objectForKey:#"like_info"]];
p.fb_comments_info = [self archivedData:[info objectForKey:#"comment_info"]];
p.fb_parent_post_id = NULL_TO_NIL([info objectForKey:#"parent_post_id"]);
p.fb_permalink = NULL_TO_NIL([info objectForKey:#"permalink"]);
p.fb_photo_data = nil;
p.fb_place = NULL_TO_NIL([info objectForKey:#"places"]);
p.fb_source_id = [info objectForKey:#"source_id"];
p.social_network = #"facebook";
p.fkPostToFriend = [[FriendStore sharedStore]findFriendWithFBUID:[info objectForKey:#"source_id"] withMOContext:tempContext];
[tempContext save:&error];
dispatch_async(dispatch_get_main_queue(), ^{
NSError * error;
[mainMOContext save:&error];
});
if (appData.count > 0)
{
if ([[appData objectForKey:#"photo_ids"] count] > 0)
{
NSLog(#" -- photos need to be loaded for postID: %#",p.objectID);
//dispatch_async(dispatch_get_main_queue(), ^{
[_postsNeedingPhotos addObject:p.objectID];
//});
}
}
}
NSLog(#"ProcessPosts: num of posts needing photos: %d",_postsNeedingPhotos.count);
dispatch_async(_getPhotosQ, ^{
[self loadPhotoData];
});
NSLog(#"-- End processPosts --");
});
}
Thanks in advance for your help!
I'm an idiot. I forgot to initialize _postsNeedingPhotos with:
_postsNeedingPhotos = [[NSMutableArray alloc] init];
Now everything runs fine... :)

Resources