iOS Core Data how to properly initialize entity relationships? - core-data

I have a one to many relationship in my core data model. I need to create a new entity and save it. The entity has a one to many relationship which generated the following code:
- (void)addRelationshipEvent1:(NSSet *)values;
- (void)removeRelationshipEvent1:(NSSet *)values;
.
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
ApplicationRecord *newManagedObject = (ApplicationRecord*)[NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
newManagedObject.startDate = [NSDate date];
newManagedObject.stopDate = [[NSDate date] dateByAddingTimeInterval:120];
//keep adding individual dynamic properties
is it correct to set the -toMany relationship sets to nil initially? Or do I need to initialize an (empty?) set here and assign it? Would I be able to add extra objects later if I set the initial set to nil?
newManagedObject.relationshipEvent1 = nil;
newManagedObject.relationshipEvent2 = nil;
//...
// Save the context.
NSError *error = nil;
if (![context save:&error])
{
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}

Alex,
You don't need to intialize your relationships. Just use the supplied accessors or helper functions and Core Data takes care of it. IOW, only worry about the property/relationship when you need to actually use it.
Andrew

Related

App crashes with one attribute of core data

I have a pretty weird problem. I'm using coredata to save notes. I can access/save/edit all the attributes of the "Notes" entity, besides one : category.
-(void)editCategory {
NSFetchRequest *request = [[NSFetchRequest alloc]init];
NSEntityDescription *categRequest = [NSEntityDescription entityForName:#"Notes" inManagedObjectContext:_managedObjectContext];
request.predicate = [NSPredicate predicateWithFormat:#"text = %#", noteToEdit];
[request setEntity:categRequest];
//Error handling
NSError *error = nil;
NSMutableArray *mutableFetchResults = [[_managedObjectContext executeFetchRequest:request error:&error]mutableCopy];
if (mutableFetchResults == nil) {
NSLog(#"Error happened : %#", error);
}
Notes *editMe = [mutableFetchResults objectAtIndex:0];
[editMe setCategory:editCategoryText];
NSLog(#"Category from pickerview : %#", editCategoryText);
if (![_managedObjectContext save:&error]) {
NSLog(#"couldnt save : %#", error);
}
}
This line :
[editMe setCategory:editCategoryText];
is crashing. editCategoryText is a string, as the category attribute. The weird thing is that I'm using the exact same piece of code to change the title attribute, and I don't have any problem.
Log file :
2013-11-07 15:49:20.286 Simple Notes 1[16511:a0b] -[__NSCFString managedObjectContext]: unrecognized selector sent to instance 0x8dccc30
2013-11-07 15:49:20.293 Simple Notes 1[16511:a0b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFString managedObjectContext]: unrecognized selector sent to instance 0x8dccc30'
Do you have any idea why this attribute is behaving differently from the others ? Thank you.
Not at a computer so can't test this but:
Get rid of the mutableCopy. executeFetchRequest returns autoreleased objects, which you are then trying to copy, this turns into a garbage pointer, which happens to end up pointing to a string.
Actually it seems like it was a core data bug, I solved it by deleting my app in the simulator, deleting the core data model in xcode, built it again and performed a clean.

Hard CoreData + iCloud scenario

i'm having a very hard issue to solve. I've got this scenario:
My app uses CoreData to storing objects, I want to implement iCloud sync between devices... and my app requires an initial populated database.
The first time I launch my app, it's going to populate my database on the cloud and marks to YES some db'fields as "databaseInstalled". These fields are synced in the cloud too.
Now when another device launch the app for the first time, I was hoping to retrieve the field "databaseInstalled" to check whether inject or not some data but it's wrong...
If databaseInstalled is false, we inject data, if databaseInstalled it's true, we wait for iCloud sync.
The problem is that I retrieve the persistentStoreCoordinator asynchronically because of I don't want to block the app that is waiting to download data from iCloud...
So how can I know a priori if i need to populate the database or it has been filled on another device and I've just to download from iCloud the populated one?
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if((__persistentStoreCoordinator != nil)) {
return __persistentStoreCoordinator;
}
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
NSPersistentStoreCoordinator *psc = __persistentStoreCoordinator;
// Set up iCloud in another thread:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// ** Note: if you adapt this code for your own use, you MUST change this variable:
NSString *iCloudEnabledAppID = #"this is a secret!";
// ** Note: if you adapt this code for your own use, you should change this variable:
NSString *dataFileName = #"you do not have to know.sqlite";
// ** Note: For basic usage you shouldn't need to change anything else
NSString *iCloudDataDirectoryName = #"Data.nosync";
NSString *iCloudLogsDirectoryName = #"Logs";
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *localStore = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:dataFileName];
NSURL *iCloud = [fileManager URLForUbiquityContainerIdentifier:nil];
if (iCloud) {
NSLog(#"iCloud is working");
NSURL *iCloudLogsPath = [NSURL fileURLWithPath:[[iCloud path] stringByAppendingPathComponent:iCloudLogsDirectoryName]];
NSLog(#"iCloudEnabledAppID = %#",iCloudEnabledAppID);
NSLog(#"dataFileName = %#", dataFileName);
NSLog(#"iCloudDataDirectoryName = %#", iCloudDataDirectoryName);
NSLog(#"iCloudLogsDirectoryName = %#", iCloudLogsDirectoryName);
NSLog(#"iCloud = %#", iCloud);
NSLog(#"iCloudLogsPath = %#", iCloudLogsPath);
// da rimuovere
//[fileManager removeItemAtURL:iCloudLogsPath error:nil];
#warning to remove
if([fileManager fileExistsAtPath:[[iCloud path] stringByAppendingPathComponent:iCloudDataDirectoryName]] == NO) {
NSError *fileSystemError;
[fileManager createDirectoryAtPath:[[iCloud path] stringByAppendingPathComponent:iCloudDataDirectoryName]
withIntermediateDirectories:YES
attributes:nil
error:&fileSystemError];
if(fileSystemError != nil) {
NSLog(#"Error creating database directory %#", fileSystemError);
}
}
NSString *iCloudData = [[[iCloud path]
stringByAppendingPathComponent:iCloudDataDirectoryName]
stringByAppendingPathComponent:dataFileName];
//[fileManager removeItemAtPath:iCloudData error:nil];
#warning to remove
NSLog(#"iCloudData = %#", iCloudData);
NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
[options setObject:iCloudEnabledAppID forKey:NSPersistentStoreUbiquitousContentNameKey];
[options setObject:iCloudLogsPath forKey:NSPersistentStoreUbiquitousContentURLKey];
[psc lock];
[psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[NSURL fileURLWithPath:iCloudData]
options:options
error:nil];
[psc unlock];
}
else {
NSLog(#"iCloud is NOT working - using a local store");
NSLog(#"Local store: %#", localStore.path);
NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
[psc lock];
[psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:localStore
options:options
error:nil];
[psc unlock];
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"iCloud routine completed.");
Setup *install = [[Setup alloc] init];
if([install shouldMigrate]) {
HUD = [[MBProgressHUD alloc] initWithView:self.window.rootViewController.view];
HUD.delegate = self;
HUD.labelText = NSLocalizedString(#"Sincronizzazione del database", nil);
[self.window.rootViewController.view addSubview:HUD];
[HUD showWhileExecuting:#selector(installDatabase) onTarget:install withObject:nil animated:YES];
}
else {
[[NSNotificationCenter defaultCenter] postNotificationName:#"setupCompleted" object:self];
}
//[[NSNotificationCenter defaultCenter] postNotificationName:#"icloudCompleted" object:self userInfo:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:#"setupCompleted" object:self];
});
});
return __persistentStoreCoordinator;
}
You can't know whether or not there's going to be data available in iCloud until you finish syncing with iCloud. That means that you've got two options:
Make the user wait until the sync is done.
Start up with your default database and merge changes from iCloud when possible.
With iCloud, you need some strategy for resolving conflicts between local data and cloud data because you have to deal with the fact that users might change data on more than one device at the same time. Once you have that in place, it seems pretty clear that the second option above is the better one: users get to start using your app right away, and data from the cloud is merged when it's available.
I had exactly same problem.
Check out my question & my answer to it iCloud + CoreData - how to avoid pre-filled data duplication?
Actually it doesn't work 100% ok. If you dare to try it I can explain you how you might make it work 100% correctly (I haven't tried yet, though).
Taking into account that you have a lot of data to pre-populate my solution might now work out for you.
There is no way to determine whether a data store is being opened for the first time. At least not on iCloud Core Data store. Think of it, iCloud should also work off-line – that is, all changes should be buffered when the user is disconnected from the Internet and then uploaded when the connection is restored. There is no way to check whether a data store was initialized without potentially making the user wait for a few minutes (or even indefinitely if the device is off-line) to ask iCloud's copy of the data sore.
To solve this, you'll need to follow these four simple guidelines:
Have a way to de-duplicate pre-populated records.
Have a way to identify pre-populated records and differentiate it from user-entered ones.
Run the de-duplication process every time new transaction records came in from iCloud.
Only seed data records once per device/account combination.
You can read more details here: http://cutecoder.org/programming/seeding-icloud-core-data/

CoreData autosaving and not loading all data after autosave

I have an NSPersistentDocument subclass using NSManagedObject subclasses for my data.
When a new document is opened, I do some initializing of data structures (trivial amount of populating fields). What I've noticed is that the Untitled document gets autosaved, and when the application re-opens, that document gets loaded. If the application quits, the user doesn't (by default) get prompted with the save dialog. If the window closes, the user does.
First question:
I want to call up the save dialog when the user quits the application. I don't want this Untitled document hanging around (under normal circumstances). I either want it saved or trashed.
I attempted to fill out:
- (void)applicationWillTerminate:(NSNotification *)aNotification
In order to trigger the document to be saved. Calling save: on the context at this point gives an error. From what I can tell, this is because the user hasn't yet saved the file on their own. In addition, calling [self close]; or [[self windowForSheet] close]; close the window without saving.
How can I force the save dialog to come up? How can I trash the untitled document?
Second question (no, I can't count):
Since when the application starts, there may or may not be an Untitled document to deal with, I'm trying to keep track of the state in another model. I've already found that the initial data (to which I referred earlier) is present when the Untitled document came up. My other model has some metadata, including a success flag/state for the populated data. Once the populated data is all in place and correct, the state indicates as such. Unfortunately, while my populated data is being loaded when the app starts with a pre-existing Untitled document, the metadata class is not.
Please excuse the roughness of the code, at this point, I'm mucking it up until I can see that it's working how I want before I polish it back off:
- (bool) createGameState {
NSEntityDescription* description = [NSEntityDescription entityForName:[GameState name] inManagedObjectContext:[self managedObjectContext]];
NSFetchRequest* req = [[NSFetchRequest alloc] init];
[req setEntity:description];
NSError *error = nil;
NSArray *array = [[self managedObjectContext] executeFetchRequest:req error:&error];
[req release];
req = nil;
GameState* result = nil;
if (array) {
NSUInteger count = [array count];
if (!count) {
// Create the new GameState.
DebugLog(#"Creating GameState");
result = [NSEntityDescription insertNewObjectForEntityForName:[GameState name] inManagedObjectContext:[self managedObjectContext]];
[result setIsLoaded:[NSNumber numberWithBool:NO]];
} else {
if (count > 1) {
NSLog(#"WARNING: Potentially Corrupt Game State. found: %lu", count);
}
result = [array objectAtIndex:0];
if ([result isLoaded]) {
[self variantLoaded];
} else {
// In this case, we have an aborted set-up. Since the game isn't
// playable, just refuse to create the GameState. This will
// force the user to create a new game.
return NO;
}
}
} else {
DebugLog(#"error: %#", error);
}
[game setState:result];
return result;
}
Note that array is always present, and count is always zero. No, I'm not explicitly calling save: anywhere. I'm relying on the standard auto-save, or the user performing a save.
EDIT:
I installed the Core Data Editor app. It turns out the issue isn't on saving the data, but on loading it. (Note: Due to another issue, the app saves as binary when instructed to save as XML, which causes much head banging.)
I've broken it down to the simplest code, which should pick up all objects of type GameState in an array. It retrieves none, despite there clearly being objects of the appropriate type in the saved file:
NSManagedObjectContext* moc = [self managedObjectContext];
NSEntityDescription* entity = [NSEntityDescription entityForName:#"GameState" inManagedObjectContext:moc];
NSFetchRequest* req = [[NSFetchRequest alloc] init];
[req setEntity:entity];
NSError *error = nil;
NSArray *array = [moc executeFetchRequest:req error:&error];
Array is not null, but [array count] is 0.
At this point, I'm guessing it's something simple that I'm overlooking.
Second EDIT:
I added -com.apple.CoreData.SQLDebug 5 and saved as SQLite. The call to executeFetchRequest does not generate any debug logs. I do see the INSERT INTO ZGAMESTATE entry show up in the logs. It seems that executeFetchRequest is not getting passed to the backend.
Third EDIT (this one burns):
I created a new xcode project, using core data (as I had with the other). I copied just this one function (stubbing where necessary) and plopped a call to it in windowControllerDidLoadNib. In this new project, the code above works.
Found the problem.
I errantly was loading objects in Document's - (id) init call. Moved to windowControllerDidLoadNib (which is what I did in the test version) and it worked fine.

Setting up basic relationship with Fetch Requests

I am wanting to set up a basic relationship with two entities in Core Data, but the relationship is either not saving, or is not working properly and I'm not sure why.
The two entities are Character and Avatar, its a one-to-one relationship. A character can have 1 avatar. Technically, it should be a "one avatar can be owned by many characters", but I'll deal with that later.
I want to add characters and assign them an avatar.
There are already 10 avatars in Core Data and 1 character, both of which I've verified via the Terminal and SQLite.
The problem is, I'm having troubling "finding an avatar by a name and then saving the relationship to a character".
So far,
I set up a fetch request called: "frqAvatarWithName" where the Predicate has the following structure:
[quote]
name == $AVATAR_NAME
[/quote]
This is so: I can find an avatar with a certain name; and then I can create a relationship with a character.
Issue 1: It gets to execute the query but then never displays how many records there are.
I get a EXC_BAD_ACCESS error in debug mode and I have traced it back to the fetch request template handling -- so, this must be in error or I have done it wrong.
Issue 2: I am not sure if I am even setting up this "basic" relationship up properly.
[code]
// This code is meant to find an avatar with a certain name and then save the relationship
// between a character and said avatar.
// This is my app delegate file for the moment
// All the files are present, and I have deleted/recreated the app various times
-(void)characterMaker
{
NSLog(#"Inside characterMaker...");
NSError *error = nil;
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObjectModel *model = [self managedObjectModel];
// Find an avatar with a specific name
NSString *nameToFind = #"avt_player_1";
// Use a Fetch request template
NSDictionary *subs = [NSDictionary dictionaryWithObjectsAndKeys:nameToFind, #"AVATAR_NAME", nil];
NSFetchRequest *fetchRequest = [model fetchRequestFromTemplateWithName:#"frqAvatarWithName"
substitutionVariables:subs];
// Set the entity to use
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Avatar"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// Execute the query (it never even reaches this point)
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
// Handle the error
NSLog(#"Error -- %#", [error localizedDescription]);
abort();
}
NSLog(#"Found %# records", [fetchedObjects count]);
// Print out avatar names
for (Avatar *a in fetchedObjects)
{
NSLog(#"Name = %#", [a valueForKey:#"name"]);
}
// This is where I would use `a` and store it in a character entity, and thus create the relationship
[/code]
I gave up on this and did the whole project with the FMDatabase project and SQLite; I've been able to resolve the problem this way.
Thread closed.

Why is my app crashing when I modify a Core Data relationship in an NSOperation subclass?

Background
I've got the following tree of objects:
Name Project
Users nil
John nil
Documents nil
Acme Project Acme Project <--- User selects a project
Proposal.doc Acme Project
12:32-12:33 Acme Project
13:11-13:33 Acme Project
...thousands more entries here...
The user can assign a group to a project. All descendants get set to that project.
This locks up the main thread so I'm using NSOperations.
I'm using the Apple approved way of doing this, watching for NSManagedObjectContextDidSaveNotification and merging into the main context.
The Problem
My saves have been failing with the following error:
Failed to process pending changes before save. The context is still dirty after 100 attempts. Typically this recursive dirtying is caused by a bad validation method, -willSave, or notification handler.
What I've Tried
I've stripped all the complexities of my app away, and made the simplest project I could think of. And the error still occurs. I've tried:
Setting the max number of operations on the queue to 1 or 10.
Calling refreshObject:mergeChanges: at several points in the NSOperation subclass.
Setting merge policies on the managed object context.
Build and Analyze. It comes up empty.
My Question
How do I set relationships in an NSOperation without my app crashing? Surely this can't be a limitation of Core Data? Can it?
The Code
Download my project: http://synapticmishap.co.uk/CDMTTest1.zip
Main Controller
#implementation JGMainController
-(IBAction)startTest:(id)sender {
NSManagedObjectContext *imoc = [[NSApp delegate] managedObjectContext];
JGProject *newProject = [JGProject insertInManagedObjectContext:imoc];
[newProject setProjectName:#"Project"];
[imoc save];
// Make an Operation Queue
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1]; // Also crashes with a higher number here (unsurprisingly)
NSSet *allTrainingGroupsSet = [imoc fetchAllObjectsForEntityName:#"TrainingGroup"];
for(JGTrainingGroup *thisTrainingGroup in allTrainingGroupsSet) {
JGMakeRelationship *makeRelationshipOperation = [[JGMakeRelationship alloc] trainGroup:[thisTrainingGroup objectID] withProject:[newProject objectID]];
[queue addOperation:makeRelationshipOperation];
makeRelationshipOperation = nil;
}
}
// Called on app launch.
-(void)setupLotsOfTestData {
// Sets up 10000 groups and one project
}
#end
Make Relationship Operation
#implementation JGMakeRelationshipOperation
-(id)trainGroup:(NSManagedObjectID *)groupObjectID_ withProject:(NSManagedObjectID *)projectObjectID_ {
appDelegate = [NSApp delegate];
imoc = [[NSManagedObjectContext alloc] init];
[imoc setPersistentStoreCoordinator:[appDelegate persistentStoreCoordinator]];
[imoc setUndoManager:nil];
[imoc setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:imoc];
groupObjectID = groupObjectID_;
projectObjectID = projectObjectID_;
return self;
}
-(void)main {
JGProject *project = (JGProject *)[imoc objectWithID:projectObjectID];
JGTrainingGroup *trainingGroup = (JGTrainingGroup *)[imoc objectWithID:groupObjectID];
[project addGroupsAssignedObject:trainingGroup];
[imoc save];
trainingGroupObjectIDs = nil;
projectObjectID = nil;
project = nil;
trainingGroup = nil;
}
-(void)mergeChanges:(NSNotification *)notification {
NSManagedObjectContext *mainContext = [appDelegate managedObjectContext];
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
-(void)finalize {
appDelegate = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
imoc = nil;
[super finalize];
}
#end
#implementation NSManagedObjectContext (JGUtilities)
-(BOOL)save {
// If there's an save error, I throw an exception
}
#end
Data Model
Update 1
I've experimented some more, and even without the merge, the exception is still thrown. Just saving the managed object context in another thread after modifying a relationship is enough.
I have a shared persistent store coordinator with the app delegate. I've tried making a separate NSPersistentStoreCoordinator for the thread with the same URL as my data store, but Core Data complains.
I'd love to suggestions on how I can make a coordinator for the thread. The core data docs allude to there being a way of doing it, but I can't see how.
You are crossing the streams (threads in this case) which is very bad in CoreData. Look at it this way:
startTest called from a button (is IBAction, assuming button tap) on Main thread
Your for loop creates a JGMakeRelationship object using the initializer trainGroup: withProject: (this should be called init, and probably call super, but that's not causing this issue).
You create a new managed object context in the operation, on the Main thread.
Now the operation queue calls the operations "main" method from a worker thread (put a breakpoint here and you'll see it's not on the main thread).
Your app goes boom because you've accessed a Managed object Context from a different thread than the one you created it on.
Solution:
Initialize the managed object context in the main method of the operation.

Resources