iOS Core Data "optimistic locking failure" - ios

I've got a really frustrating problem that I'd like help understanding and perhaps even fixing.
I'm learning Core Data by building a rather simple app.
My model is as follows:
User
Attributes
name
gender
dob
Relationships
hasCompletedItem - "A user can complete many list items, A list item can be completed by many users"
isInAgeGroup - "A user is in one AgeGroup, an AgeGroup can contain multiple users"
AgeGroup
Attributes
title
Relationships
hasItems - "An AgeGroup has many ListItems associated with it, a ListItem can only be associated with one AgeGroup"
hasUsers - "An AgeGroup has many users associated with it, A user can only be in 1 AgeGroup"
ListItem
Attributes
itemText
Relationships
completedByUser - "A list item can be completed by many users, a user can complete many list items".
forAgeGroup - "A list item is assigned to a single AgeGroup, an AgeGroup can have multiple listItems"
My program is set up as follows:
The AppDelegate handles creating the CoreData stack.
didFinishLaunchingWithOptions accesses MenuViewController and passes through the managedObjectContext for it to use:
id navigationController = [[self window] rootViewController];
id controller = [navigationController topViewController];
[controller setManagedObjectContext:[self managedObjectContext]];
The MenuViewController displays. It is a UIViewController with a few buttons which each segue to other view controllers.
When the 'Start' button is pressed on the MenuViewController, the existing managedObjectContext is passed on in the prepareForSegue method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"selectprofile"]) {
[[segue destinationViewController] setManagedObjectContext:[self managedObjectContext]];
}
}
The SelectProfileTableViewController is then shown. It lists each User in a table. It has a button to allow adding new Users. The data for the tableview is provided by an NSFetchedResultsController which basically just fetches all records from the User entity.
Tapping the "Add" button will load the AddProfileViewController but not before passing on the managedObjectContext:
AddProfileViewController *viewController = (AddProfileViewController *)[[segue destinationViewController] topViewController];
[viewController setManagedObjectContext:self.managedObjectContext];
The AddProfileViewController is just a UIViewController. It has textFields for the name, D.O.B, etc.
It has a method addProfileDone that gets called when the user taps the "Done" button. In this method a new User managedObject is created and its attributes are set:
User *newMO = [NSEntityDescription insertNewObjectForEntityForName:#"User" inManagedObjectContext:_managedObjectContext];
newMO.name = self.nameTextField.text;
newMO.dob = _dob;
// etc
Next, it also attempts to set the relationship between this new User entity and it's corresponding AgeGroup entity.
NSFetchRequest *ageGroupRecordRequest = [NSFetchRequest fetchRequestWithEntityName:#"AgeGroup"];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"title"
ascending:YES];
[ageGroupRecordRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
// Make a predicate to find the correct AgeGroup based on what was calculated
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(title == %#)", ageGroup];
[ageGroupRecordRequest setPredicate:predicate];
// Run the fetch request, should only ever get 1 result back.
NSError *fetchError;
// Result will be an array with a single AgeGroup entity
NSArray *fetchedObjects = [_managedObjectContext executeFetchRequest:ageGroupRecordRequest error:&fetchError];
// Set up the relationship using the fetched entity
// Crashes when saving if below line is uncommented
newMO.isInAgeGroup = fetchedObjects[0];
As noted in the code, when the line is uncommented and the managedObjectContext goes to save, i get the following error:
CoreData: error: failed to resolve optimistic locking failure: optimistic locking failure with (null)
CoreData: error: failed to resolve optimistic locking failure. Old save request was: { inserts ((
"0x942dd40 "
)), updates ((
"0x8e4ed60 "
)), deletes () locks () }
2014-04-12 20:00:09.482 ChekList[6076:60b] CoreData: error: failed to resolve optimistic locking failure. Next attempt will be: { inserts ((
"0x942dd40 "
)), updates ((
"0x8e4ed60 "
)), deletes () locks () }
sql: BEGIN EXCLUSIVE
annotation: getting max pk for entityID = 3
annotation: updating max pk for entityID = 3 with old = 4017 and new = 4018
sql: COMMIT
sql: BEGIN EXCLUSIVE
sql: UPDATE ZAGEGROUP SET Z_OPT = ? WHERE Z_PK = ? AND (Z_OPT = ? OR Z_OPT IS NULL)
details: SQLite bind[0] = (int64)1
details: SQLite bind[1] = (int64)6
details: SQLite bind[2] = nil
sql: ROLLBACK
sql: SELECT Z_PK,Z_OPT FROM ZAGEGROUP WHERE Z_PK IN (6) ORDER BY Z_PK
annotation: sql execution time: 0.0006s
My understanding is that basically something else has modified the context, and during the save CoreData has noticed this and stopped. I've read about changing the MergePolicy on the ManagedObjectContext but I don't really want to do this without knowing why I have to.
It's interesting to note that if I comment out the line that attempts to set the relationship it works fine. (except of course the relationship isn't set)
As far as I can see I am passing the managedObjectContext correctly to each view controller. I have also made sure that there are not multiple contexts accessing the same persistent store.
Is it likely to be the FetchedResultsController in the previous View Controller that is modifying the context for some reason?
Is anyone able to offer some information and perhaps a possible solution? I'd rather not have to change the merge policy. I can't see why I should have to considering its a rather simple example. I've been pulling my hair out most of the day on this.
I can't help but think it's most likely something simple I'm missing.
Thanks,
Brett.

I was able to figure out the solution.
Turns out that my whole problem was caused by my preloaded database.
I have a preloaded database that I copy over in the AppDelegate. Turns out that something was wrong with it.
I found this out by commenting out the lines that copied the database over and instead manually added the preloaded data in the AppDelegate. I was able to add data and also set the relationships.
From there I created a new preloaded database and it works fine.
I also found a great Apple developer example -
https://developer.apple.com/library/ios/samplecode/iPhoneCoreDataRecipes/Introduction/Intro.html
Which also showed me an alternative way of adding new entities. In this case they create the actual entity in the prepareForSegue method and pass just the entity not the entire context.
I changed my app to use that method as well (before I figured out that the database was the issue) but it still crashed. I've decided to keep it that way though as it seems more elegant.

I had this same issue but the suggested solution wasn't working for me. What worked was to create the context using concurrency type: NSMainQueueConcurrencyType even though I don't need it and I'm not using concurrency at all but somehow copying the preloaded database created the optimistic locking failure. Then wrap the save call in a the context's performBlockAndWait:
To create the context:
[[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
To save changes:
__block NSError *error = nil;
__block BOOL savedOK = NO;
[managedObjectContext performBlockAndWait:^{
savedOK = [managedObjectContext save:&error];
}];
This is even documented in iOS API, look for Concurrency inside NSManagedObjectContext class.

Related

Objective-C. Coredata managed context for keep Predicate in NSFetchedResultsController

1) I have NSFetchedResultsController with some Collection Views and I use defaultManagedObjectContext to keep context for it, which one is NSMainQueueConcurrencyType and it set, as a child for NSPrivateQueueConcurrencyType.
2) I have NSArray of NSStrings taken from CoreData in same defaultManagedObjectContext. This string I use like predicate, like filters option in window below. Filter works as it should.
3) And I have method to change Collection View element (for example, set LIKE) in another backgroundManagedObjectContext (NSPrivateQueueConcurrencyType), it set as child for defaultManagedObjectContext. Likes button works as it should.
But when I trying to use together (Predicate and Set like method), CoreData down with error:
The left hand side for an ALL or ANY operator must be either an
NSArray or an NSSet.
Should I use another context to keep predicate?
As child for defaultManagedObjectContext?
Predicate like this:
[NSEntityDescription entityForName:#"NMObject"
inManagedObjectContext:self.managedObjectContext];
self.selectedTypes = [[NSArray alloc] initWithObjects:#"музей", nil];
_predicate = [NSPredicate predicateWithFormat:#"ANY types.typeObjValue IN %#", self.selectedTypes];
So, NMObject has relationship with NMTypeObj as NMObject types <<-----> NMTypeObj objects
Thank you everybody!
The problem was with predicate format. if relationship is NMObject types <<--Many to One--> NMTypeObj objects is possibly to use ANY. Correct relationship for ANY is, for example, NMObject types <--One to Many-->> NMTypeObj objects
So, in my case, I change predicate to:
_predicate = [NSPredicate predicateWithFormat:#"types.typeObjValue IN %#", self.selectedTypes];

Illegal attempt to establish a relationship 'object' between objects in different contexts

I know that several issues possibly duplicated being, however, no pointed solution solved my problem, so I decided to post my specific case.
I'm working with CoreData in my application, and some objects are instantiated without being effectively saved on the ground, my startup code in these cases is as follows:
-(id)initEntity:(NSManagedObjectContext*)context{
AppDelegate appDelegate * = [[UIApplication sharedApplication] delegate];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Endereco" inManagedObjectContext: appDelegate.managedObjectContext];
self = (Endereco*)[[Endereco alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];
return self;
}
However, an attribute of this object is the municipality that is already saved on the base, and is selected by a ActionSheet:
if (actionSheet == actionSheetMunicipios) {
Municipio *municipio = [municipios objectAtIndex:buttonIndex-1];
endereco.municipio = municipio;
[textMunicipio setText:endereco.municipio.nome];
}
in line
endereco.municipio = municipio;
I get the following error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a
relationship' municipio 'between objects in different contexts.
The error is clear, I am trying to establish a relationship of objects with different contexts, but in my case, in which the Parent object is not saved on the base, and that the child object is already there, how could I solve?
Your comments seem to indicate you know the answer. Add endereco to the context (use insertIntoManagedObjectContext: context rather than insertIntoManagedObjectContext: nil). It's not a matter of being saved; you need to make sure that the two objects are in the same context. There is no way around that. You cannot create cross-context relationships in properties (you can in fetched properties, but it's complicated and this doesn't seem like a case where you want it).
I managed to solve the problem by adding the Endereco in the managedContext of the Municipio:
if (actionSheet == actionSheetMunicipios) {
Municipio *municipio = [municipios objectAtIndex:buttonIndex-1];
[municipio.managedObjectContext insertObject:endereco];
[endereco setMunicipio:municipio];
[textMunicipio setText:endereco.municipio.nome];
}
I do not know if it's the best solution, but it worked perfectly in this case.

Multi Context CoreData with Threads

UPDATE : I suppose the issue is that the parent context is not updated when the child context is saved. Still, need help.
I have tried many examples of Multi-context (Parent-Child) Core Data.
Previously my app was using the traditional way of storing data, i.e. I used an OperationQueue where I fetch the data from the server and save to the DB using a MOC and on saving fire a notification to the mainMOC to mergeChanges : NSManagedObjectContextDidSaveNotification.
Without disturbing the flow of the app, (i.e. removing the OperationQueue), I tried to implement the Parent-Child ManagedObjectContext Relationship where I used a privateMOC with concurrencyType as NSPrivateQueueConcurrencyType which has a persistantStoreCoordinator, and the mainMOC with concurrenyType as NSMainQueueConcurrencyType which is a child of the privateMOC. And in the Queue I have a tempMOC with concurrencyType as NSPrivateQueueConcurrencyType which is a child of the mainMOC.
While saving, I nest the performBlock of the three MOCs as -
[tempMOC performBlock:^{
if (![tempMOC save:&error]) {
NSLog(#"Error : %#",error);
}
[mainMOC performBlock:^{
if (![mainMOC save:&error]) {
NSLog(#"Error : %#",error);
}
[privateMOC performBlock:^{
if (![privateMOC save:&error]) {
NSLog(#"Error : %#",error);
}
}];
}];
}];
I am getting Errors like CoreData 1560 and 1570 while the mainMOC is trying to save. NSValidationErrorKeyerror where it says some value is nil.
Is it that the changes of the tempMOC are not going to the mainMOC ? I did not dig in but as far as I know, it should not be nil.
What could possibly be the error? Please Help.
UPDATE : I tried to print the objects of tempMOC and I see proper values like :
<Element_Name: 0xc0b59c0> (entity: Element_Name; id: 0xc07ca90 <x-coredata:///Element_Name/t2DCD57A8-4C1A-4AF7-A10E-5B9603E2BB8730> ; data: {
tag1 = nil;
tag2 = 3430065;
tag3 = 600;
tag4 = N;
tag5 = "2013-10-29 00:00:00 +0000";
tag6 = nil;
tag7 = 327842701;
relation = "0xbf1f760 <x-coredata://87C54A94-701E-4108-826E-4D98A53380F9/Relation/p1>";
tag8 = "Some_Value";
I tried to print the objects of mainMOC and I see nil value instead of the data like :
<Element_Name: 0xbd47a50> (entity: Element_name; id: 0xc0b14b0 <x-coredata:///Element_Name/t2DCD57A8-4C1A-4AF7-A10E-5B9603E2BB8740> ; data: {
tag1 = nil;
tag2 = nil;
tag3 = 0;
tag4 = nil;
tag5 = nil;
tag6 = nil;
tag7 = nil;
relation = "0xbd586c0 <x-coredata://87C54A94-701E-4108-826E-4D98A53380F9/relation/p1>";
tag8 = nil;
I just hit the same problem and found a solution. Without the rest of your code I cannot assure this would solve your problem, but it did solve mine.
I was instantiating some NSManagedObject classes, modifying some of their properties and THEN inserting them in the temp or child NSManagedObjectContext. All the properties were showing just fine like in your case.
But when I saved that context and the changes got pushed to the parent NSManagedObjectContext, all properties were nullified (like in your case).
I have not observersed this behaviour when using only one NSManagedObjectContext, and I have not experimented with the older NSManagedObjectContextDidSaveNotification pattern.
The solution is of course to add the NSManagedObject to a context just after initialisation, and before any property assignments are done.
vshall,
If you already have a background insertion MOC pattern working, why are you trying to move to a parent-child MOC situation? It isn't faster. And, judging from what I can see about your implementation, you end up blocking the main thread.
There are many good reasons to use the parent-child MOC relationship. Most of them involve creating scratch or read-only MOCs. The second big use case is to have your main MOC be a child of a private concurrent MOC. That way saves are "fast" and done on the background thread. Background insertion into a child concurrent MOC of the main MOC, in my experience, is slower and causes the UI to stutter.
In answer to your question, you are trying to access items before your embedded set of saves has finished. Hence, your data is corrupt and you get exceptions.
Andrew

Optimising Core Data / Magical Record - findFirstByAttribute - Core Data

I have the following method which is called within a FOR Loop and is called several times, each time iterating through an NSDictionary object to create and set a note object :
- (BOOL)updateById:(NSString *)entityId
withData:(NSDictionary *)dataDictionary {
DLog(#"Updating %#", [_entityClass description]);
if (_entityIdentifier == nil) {
DLog(#"entityIdentifier has not been set");
}
NSManagedObjectContext *context = ContextForThread;
id note = [_entityClass findFirstByAttribute:_entityIdentifier
withValue:entityId
inContext:context]; //This is running slowly ?
[note setValuesFromDictionary:dataDictionary];
BOOL changes = YES;
if ([note changedValues].count == 0) {
changes = NO;
DLog(#"Has NOT changed - Dont save");
}
else {
DLog(#"Has changed");
}
return changes;
}
I am trying to optimise this code and have noticed that the findFirstByAttribute method seems to be rather slow. Is there anyway I can optimise this method ?
Fundamentally, the problem is that you're doing a lot of fetches, and lots of fetches mean lots of work. Your goal here should be to reduce the number of fetches, most likely by doing them all in one shot and then refactoring your code to use the results. For example, if the entityId values are known in advance:
Fetch all instances using the known entityId values. I don't know if MR has a shortcut for this. Using Core Data directly, you'd something like the following with the fetch. The results of the fetch would be all instances where the value of _entityIdentifier is in the entityIds array:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"%K in %#", _entityIdentifier, entityIds);
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey: _entityIdentifier ascending:YES];
Refactor your method above so that you pass in both a managed object and the dictionary of values you want to assign to that object.
There are other ways to approach this, but one way or another you should fetch multiple objects at once instead of doing a separate fetch for each one.
Setting the attribute to be indexed should help a bit.
Other than that consider doing batch updates if you call this method very often.
You could use MR_findAllWithPredicate to make a single DB query and the update values for each retrieved object.

iOS multiple entites for one managedObjectContext

There is a core data model with two entities i my iOS application.
Entity: Unit / Attributes: unit_name - NSString
->> Relationship to Exercise (one to many)
Entity: Exercise / Attributes: exercise_name - NSString .
So one unit can have many exercises.
In my table view controller are all available exercises listed.
(So in the first time, i make a fetch request for the Exercise entity and the managedObjectContext points to this entity.
If i want to save a "NEW" unit with exercises the save function doesn't work.
There is no error at all, but the unit table is still empty.
Here is the code for the save function:
Units *newUnit = [NSEntityDescription insertNewObjectForEntityForName:#"Units" inManagedObjectContext:[self.coreDataHelper managedObjectContext]];
newUnit.unit_name = unitTextField.text;//Attribute
newUnit.exercises = exerciseSet;//Relationship (NSSet)
NSError *error = nil;
if (![[self.coreDataHelper managedObjectContext]save:&error]) {
NSLog(#"There was an error! %#", error);
}
else {
NSLog(#"Success!");
}
It seems like the managedObjectContext still points to the Exercise entity. (Because it was initialized the first time with this entity) the coreDataHelper has the NSPersistentStoreCoordinator, the NSManagedObjectContext, the NSManagedObjectModel and some methods to read write and delete.
Thanks for help!
Just to verify that everything is connected the way it ought to be, add
NSAssert(self.coreDataHelper, #"null coreDataHelper");
NSAssert(self.coreDataHelper.managedObjectContext, #"null MOC");
NSLog(#"available entities: %#",
self.coreDataHelper.managedObjectContext.persistentStoreCoordinator.managedObjectModel.entitiesByName);
You ought to see "Units" as one of your entities, if you've set everything up correctly.
And then after your insertion of newUnit, verify that it worked:
NSAssert(newUnit, #"newUnit didn't get inserted");
This smells like a logic error to me, by the way: you're creating a new Units instance every time you save? Are you sure you don't want to use a find-or-create pattern instead?

Resources