Core Data Saving and Loading to Multiple Entities - objective-c

Core Data Noob here.
I have a project, it saves data to a single entity no problemo. However, because the entity has way too many properties (over 100) it gets a warning that I need to normalize it. OK, so i create a second entity to store more data with To-One relationships both ways.
Problem is that when i try to save data, data saves and reloads to the first entity, but will not save or reload from the second. I must be missing something simple.
Here is some code:
//ViewDidLoad
- (void)viewDidLoad
{
if (managedObjectContext == nil) {
managedObjectContext = [(CoreDataStuffAppDelegate *) [[UIApplication sharedApplication] delegate] managedObjectContext];
NSLog(#"After managedObjectContext: %#", managedObjectContext);
}
// [self addRecord];
[super viewDidLoad];
if ([self fetchData]) {
NSLog(#"after self fetchData patientArray count is %i", [parentArray count]);
if ([parentArray count] == 1) {
Parent *parentInfo = (Parent *)[parentArray objectAtIndex:0];
parentItem1.text = parentInfo.Mother;
NSLog(#"fetching on load %i", [parentArray count]);
//Try 1: just going after the relationship route (FAIL: cuz data wont store. stays null)
// childItem1.text = parentInfo.ParentToChild.Kid;
// Try 2: going the route of directly talking to the second entity
childItem1.text = child.Kid;
}
}
// Save Record
- (void)saveRecord{
if (managedObjectContext == nil) {
NSLog(#"there is no context, arrg");
}
NSLog(#"array count is %i", [parentArray count]);
if ([parentArray count] == 1) {
NSManagedObjectContext *context = managedObjectContext;//[parent managedObjectContext];
NSLog(#"context 1 is: %#", context);
Parent *parentInfo = (Parent *)[parentArray objectAtIndex:0];
parentInfo.Mother = parentItem1.text;
NSLog(#"data says: %#", parentInfo.Mother);
NSLog(#"text syas: %#", parentItem1.text);
// Try 1, save it through the child relationship thing. (failed)
// parentInfo.ParentToChild.Kid = childItem1.text;
// NSLog(#"childSave says: D: %# T: %#", parentInfo.ParentToChild.Kid , childItem1.text);
// Try 2. from Resipeas app
if (!child) {
self.child = [NSEntityDescription insertNewObjectForEntityForName:#"Child" inManagedObjectContext:context];
[parent addChildObject:child];
NSLog(#"I hit the child");
}
child.Kid = childItem1.text;
NSLog(#"1: childSave says: D: %# T: %#", parentInfo.ParentToChild.Kid , childItem1.text);
NSLog(#"2: childSave says: D: %# T: %#", child.Kid , childItem1.text);
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"context 2 is: %#", context);
NSLog(#"major fail %#", [error localizedDescription]);
// abort();
}
}
NSLog(#"saving stuff");
}
// FetchData
- (BOOL)fetchData {
NSLog(#"doing the fetch");
BOOL returnResult = FALSE;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
if (managedObjectContext == nil)
{
NSLog(#"ok making a new managed object context");
managedObjectContext = [(CoreDataStuffAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Parent" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:1];
NSError *error;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:fetchRequest error:&error] mutableCopy];
if (mutableFetchResults == nil) {
NSLog(#"Fetching Error");
} else {
[self setParentArray:mutableFetchResults];
returnResult = TRUE;
NSLog(#"Fetching went well");
}
[mutableFetchResults release];
[fetchRequest release];
return (returnResult);
}

Okay, firstly, I think you have a conceptual problem here because I have never seen the need for an entity with 100 attributes. Most entities have a around a half-dozen attributes and I think the most I've ever seen was around 20.
Core Data is first and foremost an object graph management system with persistence tossed in as an option. It is intended to implement the model layer of a Model-View-Controller(MVC) design. As such, Core Data is primarily about modeling data not storing it.
Entities are intended to represent some real-world object, condition or event. There aren't a lot of real-world things that have a 100 attributes. E.g. You want to make a detailed model of a person. You want things like first name, last name, address(with attributes for each address component), driver's license# and date of issue, place of employment with address, social security number etc. If you tried to cram all that into one Person entity you could end up with a dozens of attributes. However, if you look closely at the data you are modeling and the relationships within the data, you would note that in the real world, addresses, driver's license, places of employment etc are not actually attributes of real people but rather other real-objects related to real people. Therefore, the best approach would be to break out the attributes for those objects into separate entities and create relationships to the Person entity. This makes model more realistic as well. After all, more than one person can live at the same address or work at the same place.
So, you probably need to start over from scratch and rethink your data model design.
Make sure you understand the difference between entities and managedObjects. Entities are abstract and serve merely to define keys, value types and relationships for managedObjects. Entities are to managedObjects as classes are to instances.
You have two other problems:
(1) You can only use the dot syntax accessor forms e.g parentInfo.ParentToChild.Kid if you have defined custom NSManagedObject subclasses for your entities. Otherwise, you are using generic NSManagedObject instances and must use the key-value methods e.g. [parent setvalue:forKey].
(2) A fetch returns only objects of one entity. So if you have a Parent entity and a Child entity. Each fetch returns instances of either Parent or Child but never both (unless they both inherit from the fetch's entity.)

Related

Fixing delay in Core Data Storage

So I am building in a hide function into my application. In my settings menu I have a UISwitch that should allow the user to hide themselves. I have created the UISwitch's IBAction like so:
-(IBAction)hideUserToggle:(id)sender {
AppDelegate *newAppDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [newAppDelegate managedObjectContext];
NSManagedObject *newOwner;
NSEntityDescription *entityDesc = [NSEntityDescription entityForName:#"LoggedInUser" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDesc];
NSManagedObject *matches = nil;
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
newOwner = [NSEntityDescription insertNewObjectForEntityForName:#"LoggedInUser" inManagedObjectContext:context];
if (_hideUser.on) {
if ([objects count] == 0) {
NSLog(#"%#",[error localizedDescription]);
} else {
matches = objects[0];
[newOwner setValue:#"userHidden" forKeyPath:#"isHidden"];
NSLog(#"%#",[matches valueForKeyPath:#"isHidden"]);
}
} else {
if([objects count] == 0) {
NSLog(#"%#",[error localizedDescription]);
} else {
matches = objects[0];
[newOwner setValue:#"userNotHidden" forKeyPath:#"isHidden"];
NSLog(#"%#",[matches valueForKeyPath:#"isHidden"]);
}
}
}
This should set the value of the Core Data String that I use to determine whether a person is hidden or not, which I use later in my code as a conditional for loading data. However when I test this feature it doesn't seem to update the persistent data store (Core Data) when the user has flipped the switch. I have looked around everywhere and I found a reference to there being a delay in updating Core Data here -> Why does IOS delay when saving core data via a UIManagedDocument, however it doesn't seem to provide the answer to my problem.
I want to be able flip the switch and save that value so that when the user swipes over to another view controller it is immediately aware that the user has gone into "hiding" or offline so it does not show certain information.
A NSManagedObjectContext is a scratchpad. Changes you make within the context exist only within the context unless or until you save them to the context's parent (either the persistent store itself or another context).
You're not saving them. I'd assume you're therefore not seeing the change elsewhere because you're using different contexts. Meanwhile the change eventually migrates because somebody else happens to save.
See -save: for details on saving.
(aside: the key-value coding [newOwner setValue:#"userHidden" forKeyPath:#"isHidden"]-style mechanism is both uglier and less efficient than using an editor-generated managed object subclass; hopefully it's just there while you're debugging?)

Why is Core Data losing one of my values?

Inside my user object I have the following code to generate a new 'session' or continue the existing session if one exists.
Strangely it will keep other properties but just loses the 'user' property... user is in a one to many relationship with session, 1 user can have many sessions. (or will do, for the following test I am simply checking for any previous session and using it if it exists)
-(void)setupSessionStuff
{
// Create new Core Data request
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Session" inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];
// Create Sort Descriptors for request
NSSortDescriptor *startTimeSort = [[NSSortDescriptor alloc] initWithKey:#"startTime" ascending:NO selector:nil];
[request setSortDescriptors:[NSArray arrayWithObjects:startTimeSort, nil]];
[startTimeSort release];
[request setFetchLimit:1]; // Only get the most recent session
// Execute request
NSError *error = nil;
NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:&error];
if (results == nil) {
// Something went horribly wrong...
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1);
}
[request release];
Session *theSession = nil;
if ([results count] == 1) {
NSLog(#"existing session");
// Use existing Session
theSession = [results objectAtIndex:0];
NSLog(#"session.user: %#", [theSession valueForKey:#"user"]); // this is always null!
} else {
NSLog(#"new session");
// Create new Sesson
theSession = (Session *)[NSEntityDescription insertNewObjectForEntityForName:#"Session" inManagedObjectContext:[self managedObjectContext]];
// Add the Session to the User
NSLog(#"before: session.user: %#", theSession.user); // null
theSession.user = self;
NSLog(#"after: session.user: %#", theSession.user); // looks good
}
...
NSLog(#"before.save: session.user: %#", theSession.user); // good
// Save everything
error = nil;
if (![[self managedObjectContext] save:&error]) {
// Something went horribly wrong...
NSLog(#"Unresolved error: %#, %#, %#", error, [error userInfo],[error localizedDescription]);
exit(-1);
}
NSLog(#"after.save: session.user: %#", theSession.user); // still there..
}
Additionally I have opened up the Core Data sqlite file and examined with SQLite Manager. It looks like the relationship has been correctly saved, as I can see the userID stored in the session table.
-
Just added this at the start of my method as another test.
NSSet *set = self.session;
for(Session *sess in set) {
NSLog(#"startTime %#", sess.startTime);
NSLog(#"user %#", sess.user);
}
Strangely enough the user is set in this case!? So set here then not set a few lines later when I do the fetch request... ?
-
In response to feedback below
Have added this code after assigning session.user = self and both return the expected output. So it does look like the problem is with the subsequent fetch.
NSLog(#"self.session: %#", self.session);
NSLog(#"self.session: %#", [self valueForKey:#"session"]);
Also I agree that accessing my session's through self.session will let me work around my issue, but it doesn't solve what is going on here.
In other places I surely won't be able to walk from one entity to the other so need to confidence the fetch is going to pull everything in correctly.
Firstly, I would check that the relationship is set from the other side by logging self.session. If that shows as null then you have no reciprocal relationship set in the model.
I think that your fetch is poorly constructed. You are relying on the sort descriptor to provide the last used session but you only have a fetch limit of 1. You seem to think that the fetch will find all existing Session objects, sort them and then return the first one. However, sorts execute last so the fetch will find a single session object (because of the fetch limit) and will then apply the sort to that single object. This makes it likely that you will be dealing with a more or less random Session object.
You probably don't need a fetch at all because you are only interested in the Session objects in a relationship with the User object which is self. If you already have the object in one side of a relationship, you don't need to fetch, you just need to walk the relationship. All you really need to do is:
if ([self.session count]==0){
//...create new session and set relationship
}else{
//... find latest session
}
I think your problem with this particular chunk of code is in the reporting. When you get a return value, you use the self.user notation but where you get a null return you use valueForKey:.
While in theory both return the same value, they don't have to because they don't use the same mechanism to return the value. The self-dot notation calls an accessor method while valueForKey: may or may not depending on the specifics of a class' implementation. I would test if the self.session returns a value where valueForKey: does not.
Well I found the problem and solved my issue...
After examining the memory address of my session entity I noticing that it was changing between runs. Investigating further I discovered that where I had been testing some code earlier in another class, creating a new session entity, but not saving it, well, it was being saved after all - when I issued the save in my code above!

Illegal attempt to establish a relationship in core data

I'm trying to build a relationship between two objects on a background thread and Core Data keeps firing off "llegal attempt to establish a relationship"
Here is some basic code of what is happening:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
#synchronized(self) {
return persistentStoreCoordinator;
}
return nil;
}
- (void)startBackgroundTask {
[self performSelectorInBackground:#selector(backgroundTask:) withObject:managedObjectID];
}
- (void)backgroundTask:(NSManagedObjectID *)pManagedObjectID {
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
NSManagedObject *object = [managedObjectContext objectWithID:pManagedObjectID];
NSManagedObject *childObject = [NSEntityDescription insertNewObjectForEntityForName:#"ChildObject" inManagedObjectContext:managedObjectContext];
[object setValue:childObject forKey:#"childObject"];
[managedObjectContext save:nil];
}
I'm correctly creating a "thread-safe" background thread managed object context and fetching the object via the object id so unsure why it's not working.
Also compare the [object managedObjectContext] contexts results in a "true" equal condition.
Any help greatly appreciated,
-Robert
This was rather stupid.
In my code versus this example I put up I was creating a Transient Object and not inserting it into the background managed object context.

Core Data unique attributes

Is it possible to make a Core Data attribute unique, i.e. no two MyEntity objects can have the same myAttribute?
I know how to enforce this programatically, but I'm hoping there's a way to do it using the graphical Data Model editor in xcode.
I'm using the iPhone 3.1.2 SDK.
Every time i create on object I perform a class method that makes a new Entity only when another one does not exist.
+ (TZUser *)userWithUniqueUserId:(NSString *)uniqueUserId inManagedObjectContext:(NSManagedObjectContext *)context
{
TZUser *user = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"TZUser" inManagedObjectContext:context];
request.predicate = [NSPredicate predicateWithFormat:#"objectId = %#", uniqueUserId];
NSError *executeFetchError = nil;
user = [[context executeFetchRequest:request error:&executeFetchError] lastObject];
if (executeFetchError) {
NSLog(#"[%#, %#] error looking up user with id: %i with error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), [uniqueUserId intValue], [executeFetchError localizedDescription]);
} else if (!user) {
user = [NSEntityDescription insertNewObjectForEntityForName:#"TZUser"
inManagedObjectContext:context];
}
return user;
}
From IOS 9 there is a new way to handle unique constraints.
You define the unique attributes in the data model.
You need to set a managed context merge policy "Merge policy singleton objects that define standard ways to handle conflicts during a save operation" NSErrorMergePolicy is the default,This policy causes a save to fail if there are any merge conflicts.
- (NSManagedObjectContext *)managedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
[_managedObjectContext setMergePolicy:NSOverwriteMergePolicy];
return _managedObjectContext;
}
The various option are discussed at Apple Ducumentation Merge Policy
It is answered nicely here
Zachary Orr's Answer
and he has kindly also created a blogpost and sample code.
Sample Code
Blog Post
The most challenging part is to get the data Model attributes editable.The Secret is to left click and then right click, after you have clicked the + sign to add a constraint.
I've decided to use the validate<key>:error: method to check if there is already a Managed Object with the specific value of <key>. An error is raised if this is the case.
For example:
- (BOOL)validateMyAttribute:(id *)value error:(NSError **)error {
// Return NO if there is already an object with a myAtribute of value
}
Thanks to Martin Cote for his input.
You could override the setMyAttribute method (using categories) and ensure uniqueness right there, although this may be expensive:
- (void)setMyAttribute:(id)value
{
NSArray *objects = [self fetchObjectsWithMyValueEqualTo:value];
if( [objects count] > 0 ) // ... throw some exception
[self setValue:value forKey:#"myAttribute"];
}
If you want to make sure that every MyEntity instance has a distinct myAttribute value, you can use the objectID of the NSManagedObject objects for that matter.
I really liked #DoozMen approach!!
I think it's the easiest way to do what i needed to do.
This is the way i fitted it into my project:
The following code cycles while drawing a quite long tableView, saving to DB an object for each table row, and setting various object attributes for each one, like UISwitch states and other things: if the object for the row with a certain tag is not present inside the DB, it creates it.
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"Obiettivo" inManagedObjectContext:self.managedObjectContext];
request.predicate = [NSPredicate predicateWithFormat:#"obiettivoID = %d", obTag];
NSError *executeFetchError = nil;
results = [[self.managedObjectContext executeFetchRequest:request error:&executeFetchError] lastObject];
if (executeFetchError) {
NSLog(#"[%#, %#] error looking up for tag: %i with error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), obTag, [executeFetchError localizedDescription]);
} else if (!results) {
if (obbCD == nil) {
NSEntityDescription *ent = [NSEntityDescription entityForName:#"Obiettivo" inManagedObjectContext:self.managedObjectContext];
obbCD = [[Obiettivo alloc] initWithEntity:ent insertIntoManagedObjectContext:self.managedObjectContext];
}
//set the property that has to be unique..
obbCD.obiettivoID = [NSNumber numberWithUnsignedInt:obTag];
[self.managedObjectContext insertObject:obbCD];
NSError *saveError = nil;
[self.managedObjectContext save:&saveError];
NSLog(#"added with ID: %#", obbCD.obiettivoID);
obbCD = nil;
}
results = nil;
Take a look at the Apple documentation for inter-property validation. It describes how you can validate a particular insert or update operation while being able to consult the entire database.
You just have to check for an existing one :/
I just see nothing that core data really offers that helps with this. The constraints feature, as well as being broken, doesn't really do the job. In all real-world circumstances you simply need to, of course, check if one is there already and if so use that one (say, as the relation field of another item, of course). I just can't see any other approach.
To save anyone typing...
// you've download 100 new guys from the endpoint, and unwrapped the json
for guy in guys {
// guy.id uniquely identifies
let g = guy.id
let r = NSFetchRequest<NSFetchRequestResult>(entityName: "CD_Guy")
r.predicate = NSPredicate(format: "id == %d", g)
var found: [CD_Guy] = []
do {
let f = try core.container.viewContext.fetch(r) as! [CD_Guy]
if f.count > 0 { continue } // that's it. it exists already
}
catch {
print("basic db error. example, you had = instead of == in the pred above")
continue
}
CD_Guy.make(from: guy) // just populate the CD_Guy
save here: core.saveContext()
}
or save here: core.saveContext()
core is just your singleton, whatever holding your context and other stuff.
Note that in the example you can saveContext either each time there's a new one added, or, all at once afterwards.
(I find tables/collections draw so fast, in conjunction with CD, it's really irrelevant.)
(Don't forget about .privateQueueConcurrencyType )
Do note that this example DOES NOT show that you, basically, create the entity and write on another context, and you must use .privateQueueConcurrencyType You can't use the same context as your tables/collections .. the .viewContext .
let pmoc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
pmoc.parent = core.container.viewContext
do { try pmoc.save() } catch { fatalError("doh \(error)")}

Programmatically Update an attribute in Core Data

I've looked through all the class documentation for Core Data and I can't find away to programmatically update values in a core data entity. For example, I have a structure similar to this:
id | title
============
1 | Foo
2 | Bar
3 | FooFoo
Say that I want to update Bar to BarBar, I can't find any way to do this in any of the documentation.
In Core Data, an object is an object is an object - the database isn't a thing you throw commands at.
To update something that is persisted, you recreate it as an object, update it, and save it.
NSError *error = nil;
//This is your NSManagedObject subclass
Books * aBook = nil;
//Set up to get the thing you want to update
NSFetchRequest * request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"MyLibrary" inManagedObjectContext:context]];
[request setPredicate:[NSPredicate predicateWithFormat:#"Title=%#",#"Bar"]];
//Ask for it
aBook = [[context executeFetchRequest:request error:&error] lastObject];
[request release];
if (error) {
//Handle any errors
}
if (!aBook) {
//Nothing there to update
}
//Update the object
aBook.Title = #"BarBar";
//Save it
error = nil;
if (![context save:&error]) {
//Handle any error with the saving of the context
}
The Apple documentation on using managed objects in Core Data likely has your answer. In short, though, you should be able to do something like this:
NSError *saveError;
[bookTwo setTitle:#"BarBar"];
if (![managedObjectContext save:&saveError]) {
NSLog(#"Saving changes to book book two failed: %#", saveError);
} else {
// The changes to bookTwo have been persisted.
}
(Note: bookTwo must be a managed object that is associated with managedObjectContext for this example to work.)
Sounds like you're thinking in terms of an underlying relational database. Core Data's API is built around model objects, not relational databases.
An entity is a Cocoa object—an instance of NSManagedObject or some subclass of that. The entity's attributes are properties of the object. You use key-value coding or, if you implement a subclass, dot syntax or accessor methods to set those properties.
Evan DiBiase's answer shows one correct way to set the property—specifically, an accessor message. Here's dot syntax:
bookTwo.title = #"BarBar";
And KVC (which you can use with plain old NSManagedObject):
[bookTwo setValue:#"BarBar" forKey:#"title"];
If I'm understanding your question correctly, I think that all you need to keep in mind is managed objects are really no different than any other Cocoa class. Attributes have accessors and mutators you can use in code, through key value coding or through bindings, only in this case they're generated by Core Data. The only trick is you need to manually declare the generated accessors in your class file (if you have one) for your entity if you want to avoid having to use setValue:ForKey:. The documentation describes this in more detail, but the short answer is that you can select your attributes in the data model designer, and choose Copy Obj-C 2.0 Method Declarations from the Design menu.
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSArray *temp = [[NSArray alloc]initWithObjects:#"NewWord", nil];
[object setValue:[temp objectAtIndex:0] forKey:#"title"];
// Save the context.
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
I think this piece of code will give you the idea ;)