Default value for a Relationship in Core Data - objective-c

I created a model with two entities. Entity A has a relationship (to-one) to entity B. My problem is, that I have no idea how to set up a default value for the relationship. If I add a new entry to the entity A I get "no value" for the value of the relationship (action sent to the corresponding NSArrayControler or [newObjectEntityA initWithEntity:....] . What I'm missing is a way to assign a default value for relationships like for attributes.
Example:
Entity A
attrib a (has a default value)
attrib b (has a default value)
relation to B (has no default value)
Of course a workaround is something like this:
[newObjectEntityA setValue: defaultValueInEntityB forKey:#"relationToB"];
but I think there must be a more convenient solution.
Thanks in advance,
Thomas

You should override awakeFromInsert instead, this method is designed for adding in default values and other startup operations.

I would create a Factory, that creates new objects of A and preconfigure it according your needs.

okay... I got the problem solved (more or less). What I did is to subclass the NSManagedObject and I overriden the method -(void) initWithEntity:insertIntoManagedObjectContext:
#implementation Locos
static NSManagedObject *defaultType;
- (id)initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context
{
self=[super initWithEntity:entity insertIntoManagedObjectContext:context];
if(self==nil)
{
[self release];
return(nil);
}
if(defaultType==nil)
{
NSEntityDescription* locoType=[NSEntityDescription entityForName:#"LocoType" inManagedObjectContext:context];
if(locoType==nil) return(self);
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:locoType];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"Default==TRUE"];
[request setPredicate:predicate];
[context executeFetchRequest:request error:nil];
NSArray *array=[context executeFetchRequest:request error:nil];
defaultType=[array objectAtIndex:0];
}
if(defaultType!=nil)
{
[self setPrimitiveValue: defaultType forKey:#"Type"];
}
return(self);
}
#end
It sets the relationship "Type" in the entity "Locos" to the first record in the entity "LocoType" which has the attribute "Default" set to true. It works, but there is something I don't understand. I get a very strange effect if I simplify the method to:
#implementation Locos
- (id)initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context
{
self=[super initWithEntity:entity insertIntoManagedObjectContext:context];
if(self==nil)
{
[self release];
return(nil);
}
NSEntityDescription* locoType=[NSEntityDescription entityForName:#"LocoType" inManagedObjectContext:context];
if(locoType==nil) return(self);
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:locoType];
NSPredicate *predicate=[NSPredicate predicateWithFormat:#"Default==TRUE"];
[request setPredicate:predicate];
[context executeFetchRequest:request error:nil];
NSArray *array=[context executeFetchRequest:request error:nil];
[self setPrimitiveValue: [array objectAtIndex:0] forKey:#"Type"];
return(self);
}
#end
in this case, if I push a button which is bound to add: of an array controller (which is bound to the entity 'Locos') I always get TWO new entries! I run the debugger and the method initWithEntry is called only ONCE. I also get rid of the problem as soon as I remove the line
[context executeFetchRequest:request error:nil];
It also doesn't happen if the fetch is called only once as in the first code example where I store the record i the static variable.
Does anybody has any idea what happens there??

Related

Core Data Edit/Save Attributes in an Entity

I am struggling with the editing/saving in Core Data and need some help in this. I am using NSFetchedResultsController and have an entity named Golfer with attributes- first_name, last_name, email_id and others in Core Data. So, I know how to add and remove golfers from the database.
I am working on one view controller called ViewManager (kinda base view for all my classes) and it has 2-3 Custom UIViews inside it. I animate them in and out whenever I need them.
I add a golfer to the tableview, then on didSelectRow tableview method, I present my edit View inside the same ViewManager controller and try to update the textfields in the edit view using the following code, but it's updating at random indexes in the tableview and not working for me. Any help would be greatly appreciated.
- (IBAction)saveEditGolfersView:(id)sender
{
AppDelegate * applicationDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSManagedObjectContext * context = [applicationDelegate managedObjectContext];
// Retrieve the entity from the local store -- much like a table in a database
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Golfer" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
// Set the sorting -- mandatory, even if you're fetching a single record/object
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"first_name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1,nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release]; sortDescriptors = nil;
[sortDescriptor1 release]; sortDescriptor1 = nil;
NSError * error;
NSArray * objects = [context executeFetchRequest:request error:&error];
for(int i = 0; i<[objects count]; i++)
{
Golfer * golfguy = [objects objectAtIndex:i];
golfguy.first_name = mEditFirstName.text;
golfguy.middle_name = mEditMiddleName.text;
golfguy.last_name = mEditLastName.text;
golfguy.email_id = mEditEmailField.text;
golfguy.contactNumber = mEditContactNum.text;
golfguy.picture = mEditPictureView.image;
NSLog(#"name-%#", golfguy.first_name);
}
[request release]; request = nil;
error = nil;
[context save:&error];
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut
animations:^ {
mEditGolfersView.frame = CGRectMake(-480, mEditGolfersView.frame.origin.y, mEditGolfersView.frame.size.width, mEditGolfersView.frame.size.height);
}
completion:^(BOOL finished) {
mEditGolfersView.hidden = YES;
}];
}
If I have read this code correct, then a call to -(IBAction)saveEditGolfersView:(id)sender will set all the Golfers with the exact properties, which I expect is not what you want.
I am not quite sure what the problem is, but I hypothesize that you need an NSPredicate to go along with your NSFetchRequest in order to change the correct Golfer(s).
Maybe I missed something, but this code says to me "hey, I'm going to load all of the Golfer in the database, order them by their first name, and then set all of their properties to the exact same text fields on this page". Just sounds like bad news...
To edit just one golfer, be sure to store the golfer you are editing in a property some where. Since you keep the managedObjectContext stored on the applicationDelegate, it will stay alive and thus keep your core data objects alive. That would avoid the expensive fetch that you are doing in the save view. If, however, you do not want to keep a reference to the golfer object, each NSManagedObject has an objectId, which is the identifier used by core data. You could use the objectId in a fetch predicate like so:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"objectId == %#", self.editingGolferObjectId];
[request setPredicate:predicate];
I would choose to keep reference to the object in your case, rather than the objectId

Core data opening file dirty, inconsistently missing values, fetch returns empty array

I have a document based application running with core data. The object model has three entities with several properties. It seems to be working mostly alright—I can fill in some information and save it, no problem. When I go to open the resulting file, however, it always opens "dirty," before I've even touched anything, and a few of the fields are sometimes blank.
What I mean is, sometimes you open the file and those fields show up empty and other times you open the file and they show up with the proper data. The properties that are blank are associated with only one of the entities and are displayed within the same NSTabView. They are some NSStrings displayed as both values in text fields and labels.
Update:
Thanks to #ughoavgfhw's advice, I switched to an XML store and found two problems: that I was creating a new entity each time the document was opened in the [MyDocument init] instead of loading the saved one from the persistent store, but now I'm having problems fetching that one.
In the resulting XML file after a save, it does include this (the entity and properties that are giving me trouble):
<object type="STORY" id="z102">
<attribute name="title" type="string">test 6</attribute>
<attribute name="descript" type="string">this is a test</attribute>
</object>
and I attempt to fetch it with this:
- (Story *)getSavedStory {
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Story" inManagedObjectContext:[self managedObjectContext]];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSError *error = nil;
NSArray *array = [[self managedObjectContext] executeFetchRequest:request error:&error];
if (array == nil) {
NSLog(#"%#",error);
return nil;
} else {
return [array lastObject];
}
}
After opening that persistent store, that request returns an empty array (and no error). Any tips on where to go from here?
Without code, all I can do is guess, but I would guess that you are doing some setup when you load the document. You do not disable undo registration, which is why you are having it marked as "dirty". There are a few reasons that data could not be loaded correctly. The two most likely situations are that: a) you override the data during your initialization, or b) the data is not being saved correctly, and therefore cannot be loaded correctly.
Here is how to disable undo registration:
NSManagedObjectContext *moc; //In your document subclass, get this with [self managedObjectContext];
[moc processPendingChanges];
[[moc undoManager] disableUndoRegistration];
//Make changes here
[moc processPendingChanges];
[[moc undoManager] enableUndoRegistration];
Update for new information:
Don't make any changes to core data in the init method. The windowControllerDidLoadNib: method is a better choice because everything has been loaded at that point. Here is an example that checks for an existing Story entity and creates a new one if needed:
- (void)windowControllerDidLoadNib:(NSWindowController *)windowController {
[super windowControllerDidLoadNib:windowController];
NSFetchRequest *req = [[NSFetchRequest alloc] init];
[req setEntity:[NSEntityDescription entityForName:#"Story" inManagedObjectContext:[self managedObjectContext]]];
NSError *err = nil;
NSArray *objs = [[self managedObjectContext] executeFetchRequest:req error:&err];
[req release];
if(!objs) {
[[NSAlert alertWithError:err] runModal];
return;
}
NSManagedObject *story = nil;
if([objs count] == 0) {
[[self managedObjectContext] processPendingChanges];
[[self undoManager] disableUndoRegistration];
story = [NSEntityDescription insertNewObjectForEntityForName:#"Story" inManagedObjectContext:[self managedObjectContext]];
//Additional setup
[[self managedObjectContext] processPendingChanges];
[[self undoManager] disableUndoRegistration];
} else story = [objs lastObject];
}

Core Data & Generating Model Entities

Standard newbie question. I've created a data model for an iOS application. I am able to create, update and delete entities within the model from various views by using the NSEntityDescription object.
Say if I had a mutable array of objects returned from a fetch request. How can I loop through each one when I do not have a generated object definition from the entity model? By generated object definition I mean, a header and body class definition of the entity described in the data model package.
All CoreData entities derive from NSManagedObject and all the database data from those can be accessed via key value encoding. The minimum you need to know can be gained from the Model. You don't necessarily require the headers.
For example an entity PersonEntity which has a relationship to NameEntity with attribute firstname
NSArray *results = [managedObjectContext queryEntityForName:#"PersonEntity" predicateFormat:nil argumentArray:nil];
for(NSManagedObject *object in results)
{
NSString *name = [object valueForKeyPath:#"nameobject.firstname";
[self doSomething:name];
}
queryEntityForName is my own category. You might find it useful.
#implementation NSManagedObjectContext(VMQueryAdditions)
-(NSArray *)queryEntityForName:(NSString *)name predicateFormat:(NSString *)pstring argumentArray:(NSArray *)arr
{
NSEntityDescription *entity = [NSEntityDescription entityForName:name inManagedObjectContext:self];
NSFetchRequest *fetch = [[[NSFetchRequest alloc] init] autorelease];
[fetch setEntity:entity];
NSPredicate *pred;
if(pstring)
{
if(arr) pred = [NSPredicate predicateWithFormat:pstring argumentArray:arr];
else pred = [NSPredicate predicateWithFormat:pstring];
[fetch setPredicate:pred];
}
NSError *error = nil;
NSArray *results = [self executeFetchRequest:fetch error:&error];
if (error) {
NSLog(#"MOC Fetch - Unresolved error %#, %#", error, [error userInfo]);
return [NSArray array];
}
return results;
}
#end

NSTokenFieldCell Subclass to force use of Core Data To-Many Relationship

I have come across an interesting conundrum (of course, I could just being doing something horribly wrong).
I would like an NSTokenField to "represent" a relationship in a Core Data Application. The premise is such: You click on a Note from a TableView (loaded from the Notes Array Controller). The token field is then bound (through "value") to the Notes Array Controller selection.Tags. Tags is a to-many relationship on the entity Notes.
Obviously, an NSTokenField will not accept the NSSet that the Array Controller Provides it. To get around this, I subclassed NSTokenFieldCell and overrode its objectValue and setObjectValue: methods. I thought that I could simply translate the NSSet that was being provided to the NSArray that the NSTokenFieldCell expected. (Note: I originally tried overriding these methods on a NSTokenField subclass; however, they were not being called.)
So, I came up with said code:
- (void)setObjectValue:(NSSet*)object {
tagsList = [object copy];
NSMutableArray *displayList = [[NSMutableArray alloc] init];
for (id newObject in tagsList) {
[displayList addObject:[newObject valueForKey:#"Name"]];
}
[super setObjectValue:displayList];
}
- (id)objectValue {
NSArray *displayList = [super objectValue];
NSEntityDescription *tagEntity = [NSEntityDescription
entityForName:#"Tag"
inManagedObjectContext:[appDelegate
managedObjectContext]];
NSMutableSet *returnValue = [[NSMutableSet alloc] init];
for (NSString *token in displayList) {
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:tagEntity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"Name == %#", token];
[request setPredicate:predicate];
NSError *error;
NSArray *results = [[appDelegate managedObjectContext] executeFetchRequest:request error:&error];
if (results == nil) {
NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName:#"Tag" inManagedObjectContext:[appDelegate managedObjectContext]];
[object setValue:token forKey:#"Name"];
[returnValue addObject:object];
} else {
[returnValue addObject:[results objectAtIndex:0]];
}
}
return returnValue;
}
It crashes. :( And, surprisingly it crashes on the line that calls [super objectValue]. It gives me the error:
-[NSConcreteAttributedString countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance ...
Sigh. The sad thing is that when I go into the Core Data XML file and give the Note a Tag, it displays correctly, and [super setObjectValue:] is passed an array of strings. However, as soon as I enter something else and mouse away, I get the error.
I am not sure what to do about this. Can anyone spot anything horribly wrong with this? Thanks.
UPDATE:
If it makes a difference, I do not have a delegate configured for the TokenField.
In typical SO fashion, I found the answer to my own question. It was silly to begin with. I simply needed another ArrayController bound to the Notes selection.Tags set. Then, I bound the NSTokenField to the ArrangedObjects of that Controller, implemented some delegate methods. Boom. Simple.
Silly me.

Adding unique objects to Core Data

I'm working on an iPhone app that gets a number of objects from a database. I'd like to store these using Core Data, but I'm having problems with my relationships.
A Detail contains any number of POIs (points of interest). When I fetch a set of POI's from the server, they contain a detail ID. In order to associate the POI with the Detail (by ID), my process is as follows:
Query the ManagedObjectContext for the detailID.
If that detail exists, add the poi to it.
If it doesn't, create the detail (it has other properties that will be populated lazily).
The problem with this is performance. Performing constant queries to Core Data is slow, to the point where adding a list of 150 POI's takes a minute thanks to the multiple relationships involved.
In my old model, before Core Data (various NSDictionary cache objects) this process was super fast (look up a key in a dictionary, then create it if it doesn't exist)
I have more relationships than just this one, but pretty much every one has to do this check (some are many to many, and they have a real problem).
Does anyone have any suggestions for how I can help this? I could perform fewer queries (by searching for a number of different ID's), but I'm not sure how much this will help.
Some code:
POI *poi = [NSEntityDescription
insertNewObjectForEntityForName:#"POI"
inManagedObjectContext:[(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext]];
poi.POIid = [attributeDict objectForKey:kAttributeID];
poi.detailId = [attributeDict objectForKey:kAttributeDetailID];
Detail *detail = [self findDetailForID:poi.POIid];
if(detail == nil)
{
detail = [NSEntityDescription
insertNewObjectForEntityForName:#"Detail"
inManagedObjectContext:[(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext]];
detail.title = poi.POIid;
detail.subtitle = #"";
detail.detailType = [attributeDict objectForKey:kAttributeType];
}
-(Detail*)findDetailForID:(NSString*)detailID {
NSManagedObjectContext *moc = [[UIApplication sharedApplication].delegate managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:#"Detail" inManagedObjectContext:moc];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
#"detailid == %#", detailID];
[request setPredicate:predicate];
NSLog(#"%#", [predicate description]);
NSError *error;
NSArray *array = [moc executeFetchRequest:request error:&error];
if (array == nil || [array count] != 1)
{
// Deal with error...
return nil;
}
return [array objectAtIndex:0];
}
Check out the section titled "Batch Faulting" on the page titled "Core Data Performance" in Xcode's Core Data Programming Guide that Norman linked to in his answer.
Only fetching those managedObjects whose ids are IN a collection (NSSet, NSArray, NSDictionary) of ids of the objects returned by the server may be even more efficient.
NSSet *oids = [[NSSet alloc] initWithObjects:#"oid1", #"oid2", ..., nil];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"oid IN %#", oids];
[oids release];
UPDATE: I worked this tip into a solution for the acani usersView. Basically, after downloading a JSON response of users, the iPhone uses the popular open source JSON framework to parse the response into an NSArray of NSDictionary objects, each representing a user. Then, it makes an NSArray of their uids and does a batch fetch on Core Data to see if any of them already exist on the iPhone. If not, it inserts it. If so, it updates the ones that do exist only if their updated attribute is older than that of the one from the server.
I've gotten all this to work really well, thanks to Norman, who put me on the right path. I'll post my helper class here for others.
Basically, my helper class will look up if an NSManagedObject exists for some ID, and can create it for some ID. This executes quickly enough for me, with 1,000 find/create operations taking around 2 seconds on my iPhone (I also did a few other things there, pure find/create is likely faster).
It does this by caching a dictionary of all the NSManagedObjects, and checking that cache rather than executing a new NSFetchRequest.
A couple of modifications that could help things speed up even further:
1. Get only selected properties for the NSManagedObjects
2. Only get the identifier property for the NSManagedObject into a dictionary, instead of the whole object.
In my performance testing, the single query wasn't the slow part (but with only 1,000 items, I'd expect it to be fast). The slow part was the creation of the items.
#import "CoreDataUniquer.h"
#implementation CoreDataUniquer
//the identifying property is the field on the NSManagedObject that will be used to look up our custom identifier
-(id)initWithEntityName:(NSString*)newEntityName andIdentifyingProperty:(NSString*)newIdProp
{
self = [super init];
if (self != nil) {
entityName = [newEntityName retain];
identifyingProperty = [newIdProp retain];
}
return self;
}
-(NSManagedObject*)findObjectForID:(NSString*)identifier
{
if(identifier == nil)
{
return nil;
}
if(!objectList)
{
NSManagedObjectContext *moc = [(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription
entityForName:entityName inManagedObjectContext:moc];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSError *error;
NSArray *array = [moc executeFetchRequest:request error:&error];
objectList = [[NSMutableDictionary dictionary] retain];
for (NSManagedObject* p in array) {
NSString* itemId = [p valueForKey:identifyingProperty];
[objectList setObject:p forKey:itemId];
}
}
NSManagedObject* returnedObject = [objectList objectForKey:identifier];
return returnedObject;
}
-(NSManagedObject*)createObjectForID:(NSString*)identifier
{
NSManagedObject* returnedObject = [NSEntityDescription
insertNewObjectForEntityForName:entityName
inManagedObjectContext:[(AppDelegate*)[UIApplication sharedApplication].delegate managedObjectContext]];
[returnedObject setValue:identifier forKey:identifyingProperty];
[objectList setObject:returnedObject forKey:identifier];
return returnedObject;
}
- (void) dealloc
{
DESTROY(entityName);
DESTROY(identifyingProperty);
[super dealloc];
}
#end
This page provides some help on optimizing performance:
http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/Articles/cdPerformance.html#//apple_ref/doc/uid/TP40003468-SW1
While not very efficient, why not just build them in-memory with a NSDictionary? Read everything from Core Data into a NSDictionary then merge in your data, replacing everything in Core Data.