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

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.

Related

Where should NSManagedObjectContext be created?

I've recently been learning about Core Data and specifically how to do inserts with a large number of objects. After learning how to do this and solving a memory leak problem that I met, I wrote the Q&A Memory leak with large Core Data batch insert in Swift.
After changing NSManagedObjectContext from a class property to a local variable and also saving inserts in batches rather than one at a time, it worked a lot better. The memory problem cleared up and the speed improved.
The code I posted in my answer was
let batchSize = 1000
// do some sort of loop for each batch of data to insert
while (thereAreStillMoreObjectsToAdd) {
// get the Managed Object Context
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
managedObjectContext.undoManager = nil // if you don't need to undo anything
// get the next 1000 or so data items that you want to insert
let array = nextBatch(batchSize) // your own implementation
// insert everything in this batch
for item in array {
// parse the array item or do whatever you need to get the entity attributes for the new object you are going to insert
// ...
// insert the new object
let newObject = NSEntityDescription.insertNewObjectForEntityForName("MyEntity", inManagedObjectContext: managedObjectContext) as! MyManagedObject
newObject.attribute1 = item.whatever
newObject.attribute2 = item.whoever
newObject.attribute3 = item.whenever
}
// save the context
do {
try managedObjectContext.save()
} catch {
print(error)
}
}
This method seems to be working well for me. The reason I am asking a question here, though, is two people (who know a lot more about iOS than I do) made comments that I don't understand.
#Mundi said:
It seems in your code you are using the same managed object context,
not a new one.
#MartinR also said:
... the "usual" implementation is a lazy property which creates the
context once for the lifetime of the app. In that case you are reusing
the same context as Mundi said.
Now I don't understand. Are they saying I am using the same managed object context or I should use the same managed object context? If I am using the same one, how is it that I create a new one on each while loop? Or if I should be using just one global context, how do I do it without causing memory leaks?
Previously, I had declared the context in my View Controller, initialized it in viewDidLoad, passed it as a parameter to my utility class doing the inserts, and just used it for everything. After discovering the big memory leak is when I started just creating the context locally.
One of the other reasons I started creating the contexts locally is because the documentation said:
First, you should typically create a separate managed object context
for the import, and set its undo manager to nil. (Contexts are not
particularly expensive to create, so if you cache your persistent
store coordinator you can use different contexts for different working
sets or distinct operations.)
What is the standard way to use NSManagedObjectContext?
Now I don't understand. Are they saying I am using the same managed
object context or I should use the same managed object context? If I
am using the same one, how is it that I create a new one on each while
loop? Or if I should be using just one global context, how do I do it
without causing memory leaks?
Let's look at the first part of your code...
while (thereAreStillMoreObjectsToAdd) {
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
managedObjectContext.undoManager = nil
Now, since it appears you are keeping your MOC in the App Delegate, it's likely that you are using the template-generated Core Data access code. Even if you are not, it is highly unlikely that your managedObjectContext access method is returning a new MOC each time it is called.
Your managedObjectContext variable is merely a reference to the MOC that is living in the App Delegate. Thus, each time through the loop, you are merely making a copy of the reference. The object being referenced is the exact same object each time through the loop.
Thus, I think they are saying that you are not using separate contexts, and I think they are right. Instead, you are using a new reference to the same context each time through the loop.
Now, your next set of questions have to do with performance. Your other post references some good content. Go back and look at it again.
What they are saying is that if you want to do a big import, you should create a separate context, specifically for the import (Objective C since I have not yet made time to learn Swift).
NSManagedObjectContext moc = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
You would then attach that MOC to the Persistent Store Coordinator. Using performBlock you would then, in a separate thread, import your objects.
The batching concept is correct. You should keep that. However, you should wrap each batch in an auto release pool. I know you can do it in swift... I'm just not sure if this is the exact syntax, but I think it's close...
autoreleasepool {
for item in array {
let newObject = NSEntityDescription.insertNewObjectForEntityForName ...
newObject.attribute1 = item.whatever
newObject.attribute2 = item.whoever
newObject.attribute3 = item.whenever
}
}
In pseudo-code, it would all look something like this...
moc = createNewMOCWithPrivateQueueConcurrencyAndAttachDirectlyToPSC()
moc.performBlock {
while(true) {
autoreleasepool {
objects = getNextBatchOfObjects()
if (!objects) { break }
foreach (obj : objects) {
insertObjectIntoMoc(obj, moc)
}
}
moc.save()
moc.reset()
}
}
If someone wants to turn that pseudo-code into swift, it's fine by me.
The autorelease pool ensures that any objects autoreleased as a result of creating your new objects are released at the end of each batch. Once the objects are released, the MOC should have the only reference to objects in the MOC, and once the save happens, the MOC should be empty.
The trick is to make sure that all object created as part of the batch (including those representing the imported data and the managed objects themselves) are all created inside the autorelease pool.
If you do other stuff, like fetching to check for duplicates, or have complex relationships, then it is possible that the MOC may not be entirely empty.
Thus, you may want to add the swift equivalent of [moc reset] after the save to ensure that the MOC is indeed empty.
This is a supplemental answer to #JodyHagins' answer. I am providing a Swift implementation of the pseudocode that was provided there.
let managedObjectContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = (UIApplication.sharedApplication().delegate as! AppDelegate).persistentStoreCoordinator // or wherever your coordinator is
managedObjectContext.performBlock { // runs asynchronously
while(true) { // loop through each batch of inserts
autoreleasepool {
let array: Array<MyManagedObject>? = getNextBatchOfObjects()
if array == nil { break }
for item in array! {
let newEntityObject = NSEntityDescription.insertNewObjectForEntityForName("MyEntity", inManagedObjectContext: managedObjectContext) as! MyManagedObject
newObject.attribute1 = item.whatever
newObject.attribute2 = item.whoever
newObject.attribute3 = item.whenever
}
}
// only save once per batch insert
do {
try managedObjectContext.save()
} catch {
print(error)
}
managedObjectContext.reset()
}
}
These are some more resources that helped me to further understand how the Core Data stack works:
Core Data Stack in Swift – Demystified
My Core Data Stack

Core Data: Re-assign parent entity

Using Core Data, what's the proper way to re-assign the parent entity?
I have a Car entities that has one-to-many relationship with wheels and is cascaded delete (if the car is deleted, all its wheels are also deleted). Each wheel can only have one Car parent. How do I re-assign the wheel to a different Car?
For example:
Car-A has Wheel1, Wheel2, Wheel3, Wheel4 as its children.
I want to move Wheel1, Wheel2, Wheel3, Wheel4 to a new parent called Car-B.
Here's what I've done:
for (Wheel *wheel in carA.wheels) {
wheel.car = carB;
}
However, the above throws error:
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <_NSFaultingMutableSet: 0x7fe0537dd2b0> was mutated while being enumerated.'
Besides the error being thrown, I also feel simply re-assign the parent entity as above is not the proper way.
Any advice would be greatly appreciated. Thanks.
You are mutating the set directly within the for-loop which leads to the error. Assuming, the set is a NSMutableSet, the proper way to do this would be:
NSArray* wheels = [carA.wheels allObjects];
for (NSManagedObject* wheel in wheels)
{
[carA removeObject:wheel]
[carB addObject:wheel]
}
You cannot mutate a collection that you are enumerating. If you think about it, it is quite logical that this has to lead to problems.
The solution is to separate the items first.
NSSet *wheels = carA.wheels
carA.wheels = nil;
for (Wheel *wheel in wheels) {
wheel.car = carB;
}

Assigning data in a to-many relationship

I'm making an iOS app with different types of quizzes, with all the questions and answer choices stored in CoreData.
I created two entities, one called QuestionData that has attributes such as 'question' 'answer1' (meaning, answer choice 1), 'answer2' etc, and then another entity called Quiz, with attributes like 'name' and 'quizId' and I created a to-many relationship on Quiz called 'dataData' that will refer to all the questions/answers etc.
When I created some test data (to experiment with the relationships), I first created the questionData, then set the name and id for the Quiz, and then I tried to set the data in the relationship like this,
[quizInfo setValue:questionData forKey:#"quizData"];
Which was supposed to store/set all the questions/answers in the quizData key of the Quiz entity. However, I got this unacceptable type error, telling me that it 'desired' an NSSet, but i instead gave it a type of QuestionData.
'NSInvalidArgumentException', reason: 'Unacceptable type of value for to-many relationship: property = "quizData"; desired type = NSSet; given type = QuestionData; value = <QuestionData: 0x89447a0> (entity: QuestionData; id: 0x8919900 <x-coredata:///QuestionData/t6B583F60-5794-47FA-8A51-
I understand the concept of a 'set' however, I'm not sure how I can make questionData a set when it comes time to do this
[quizInfo setValue:questionData forKey:#"quizData"];
Full code:
NSManagedObjectContext *context = [self managedObjectContext];
QuestionData *questionData = [NSEntityDescription
insertNewObjectForEntityForName:#"Questiondata"
inManagedObjectContext:context];
[questionData setValue:#"do you like big basketballs" forKey:#"question"];
[questionData setValue:#"yes" forKey:#"answer1"];
[questionData setValue:#"no" forKey:#"answer2"];
[questionData setValue:#"maybe" forKey:#"answer3"];
[questionData setValue:#"maybe" forKey:#"correctAnswer"];
Quiz *quizInfo = [NSEntityDescription
insertNewObjectForEntityForName:#"Quiz"
inManagedObjectContext:context];
[quizInfo setValue:#"1" forKey:#"quizId"];
[quizInfo setValue:#"sportsquiz" forKey:#"name"];
[quizInfo setValue:questionData forKey:#"quizData"];
You need to wrap your questionData inside a set. Since you only have one question at this time you can use
[quizInfo setValue:[NSSet setWithObject:questionData] forKey:#"quizData"];
When you have more than one question you can either use:
[quizInfo setValue:[NSSet setWithObjects:question1Data,question2Data,question3Data,nil] forKey:#"quizData"];
Or you could put your questions into an NSMutableArray and use
[quizInfo setValue:[NSSet setWithArray:questionArray] forKey:#"quizData"];

iOS Core Data "optimistic locking failure"

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.

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