So here's the deal:
// A. Inserting
Item *item = (Item *)[NSEntityDescription insertNewObjectForEntityForName:#"Item" inManagedObjectContext:managedObjectContext];
NSError *error = nil;
[managedObjectContext save:&error];
..
[item setItemID:#"15"];
[managedObjectContext save:&error];
NSLog(#"Error: %#", error); // outputs (null)
// B. Fetching all records
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Item"];
request.returnsObjectsAsFaults = NO;
NSArray *allItems = [managedObjectContext executeFetchRequest:request error:nil];
NSLog(#"All Items: %#", allItems);
Now, this outputs a huge list, containing the previously inserted item:
"<Item: 0x7eb7bc0> (entity: Item; id: 0x7eb71c0 <x-coredata://BC6EB71C-47C0-4445-905D-7D42E6FC611B/Item/p2> ; data: {\n itemID = 15;\n})"
So far so good, but I want to check whether this particular item does exist (I know it may sound strange in this context, but it really makes sense here). However, the predicate I'm using fails (and I don't see why):
// C. Fetching a single record
NSFetchRequest *singleRequest = [[NSFetchRequest alloc] initWithEntityName:#"Item"];
singleRequest.predicate = [NSPredicate predicateWithFormat:#"itemID == %#", #"15"];
NSError *error = nil;
NSArray *results = [managedObjectContext executeFetchRequest:singleRequest error:&error];
NSLog(#"Error: %#", error); // outputs (null) again
NSLog(#"Results: %#", results); // outputs () ...
I don't really understand how to "fix" this.
Here are some other facts:
Using persistent SQLite store with CoreData (pretty much default configuration, not even relationships, just plain key-value in 3 tables).
The itemIDs always are strings
When reopening the app, the second code block, does return an item (= the item inserted in the previous run). Could it be that save: writes to disk asynchronously, and that the NSPredicate only filters items wrote to disk?
Part A happens in a different method, but on the same thread as B and C. C is directly below B and both are placed in the same method.
If you're comparing strings, try this :
#"itemID LIKE %#"
Have a read of this, the section titled 'String Comparisons"
Okay got it. I used #synthesize instead of #dynamic in the particular model's .m-file. Didn't know it would be such a big problem .. :)
For some reason, updating the SQLite-database goes wrong when using #synthesize ..
Related
I have a very weird problem that has stumped me rather!
I have a core data entity that i have just added some new attributes to:
deleted - Boolean
deletedDate - Date
I have the following code, that upon pressing sets both those values on the core data object:
- (IBAction)deleteButtonInTable:(id)sender {
//Get the ID of the currently selected item in the table
NSInteger selected = [self.tweetTableView rowForView:sender];
//Create a predicate and fetch the objects from Core Data
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSPredicate *testForTrue = [NSPredicate predicateWithFormat:#"approved == NO"];
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"postDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, nil];
[request setPredicate:testForTrue];
[request setSortDescriptors:sortDescriptors];
[request setEntity:[NSEntityDescription entityForName:#"Tweet" inManagedObjectContext:_managedObjectContext]];
//Setup the Request
[request setEntity:[NSEntityDescription entityForName:#"Tweet" inManagedObjectContext:_managedObjectContext]];
//Assign the predicate to the fetch request
NSError *error = nil;
//Create an array from the returned objects
NSArray *fetchedObjects = [_managedObjectContext executeFetchRequest:request error:&error];
Tweet *selectedTweet = [fetchedObjects objectAtIndex:selected];
if (selectedTweet) {
selectedTweet.deleted = [NSNumber numberWithBool:TRUE];
selectedTweet.deletedDate = [NSDate date];
NSLog(#"%#",selectedTweet);
[self refreshTableView];
if (! self.tweetTableView){
NSLog(#"Tableview doesn't exist!!)");
}
[[self tweetTableView] reloadData];
[[self managedObjectContext] commitEditing];
[self saveAction:nil];
}
if ([self.autoWriteTweets isEqualToString:#"YES"]){
[self writeTweetsToXML];
[self saveAction:nil];
}
}
Now, if i watch the object in xcode with some breaks, i can see the attribute change on the object as i pass through the function, but i have an Table displaying a datasource, which is filtered to only show objects that have the deleted bool set to true, and nothing ever shows up there.
Now, to make things even more confusing i have a function that exports an array of the objects:
-(void)writeTweetsToXML{
//Create new fetch request
NSFetchRequest *request = [[NSFetchRequest alloc] init];
//Set new predicate to only fetch tweets that have been favourited
NSPredicate *filterFavourite = [NSPredicate predicateWithFormat:#"approved == YES"];
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:self.exportSort ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, nil];
[request setSortDescriptors:sortDescriptors];
//Setup the Request
[request setEntity:[NSEntityDescription entityForName:#"Tweet" inManagedObjectContext:_managedObjectContext]];
[request setResultType:NSDictionaryResultType];
//Assign the predicate to the fetch request
[request setPredicate:filterFavourite];
NSError *error = nil;
//Create an array from the returned objects
NSArray *tweetsToExport = [_managedObjectContext executeFetchRequest:request error:&error];
NSAssert2(tweetsToExport != nil && error == nil, #"Error fetching events: %#\n%#", [error localizedDescription], [error userInfo]);
//NSString *documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
//NSString *path = [NSString stringWithFormat:#"%#/tweets.xml", documents];
NSString *writeerror;
if(tweetsToExport) {
NSString * exportLocationFull = [[NSString alloc]initWithFormat:#"%#/tweets.xml",self.exportLocation];
BOOL success = [tweetsToExport writeToFile:exportLocationFull atomically:YES];
NSLog(#"Write Status = %d to %#", success, exportLocationFull);
}
else {
NSLog(#"%#",writeerror);
}
}
Now, when i look at the exported file, two things happen which are odd!
Firstly, an object that i have seen have it's deleted value set to true, exports with the value as 0.
Secondly, the deletedDate attribute does not export at all, every, despite it being in the core data model. I can't see any way this can happen as i am doing no specific filtering on the export.
It's like a getter/setter somewhere is broken, but i have checked the class files and everything is as it should be and set to #dynamic.
Any help would be greatly appreciated as i'm a bit lost as to what the hell is going on.
People had warned me about core data's quirks, but this is just plain odd!
Cheers
Gareth
Note 1
As an aside, i am using the exact same code from the first section to set other attributes on objects that are filtered and that seems to work fine!
You should not name an Core Data attribute "deleted", that conflicts with the
isDeleted method of NSManagedObject.
Compare https://stackoverflow.com/a/16003894/1187415 for a short analysis of that problem.
There are other attribute names that cause conflicts, e.g. "updated" (compare Cannot use a predicate that compares dates in Magical Record). Unfortunately, there are no warnings at compile time or runtime,
and the documentation on what acceptable attribute names are is also quite vague.
Things to check:
Did you save your core data entities with [managedObjectContext save:&error] at the appropriate places (e.g. before displaying the new table view data)? Did you check the error variable?
Did you migrate your model correctly with a new model version?
Are you reading the correct attributes and displaying them correctly (in UI or log statements)?
BTW, in your code you are setting the request entity twice.
Try saving the mananged object context before loading the table view.
The boolean deleted may be 0 before and not be changed or it may be auto-initialized (there is an field in the inspector to set default values) to 0. Date fields on the other hand are nil by default.
P.S. Use [NSNumber numberWithBoolean:YES] in Objective-C.
Regarding one answer in another post: is that use executeFetchRequest in a loop a bad practice? I saw that usage in Stanford CS193p project "Photomania" (click link to download project). The relevant code is below:
The [FlickrFetcher recentGeoreferencedPhotos] is used to fetch photos from Flickr API, which happens in a background thread. But the loop that execute fetch request happens in main thread.
- (void)fetchFlickrDataIntoDocument:(UIManagedDocument *)document
{
dispatch_queue_t fetchQ = dispatch_queue_create("Flickr fetcher", NULL);
dispatch_async(fetchQ, ^{
NSArray *photos = [FlickrFetcher recentGeoreferencedPhotos];
// perform in the NSMOC's safe thread (main thread)
[document.managedObjectContext performBlock:^{
for (NSDictionary *flickrInfo in photos) {
// This is the method that will call executeFetchRequest
[Photo photoWithFlickrInfo:flickrInfo inManagedObjectContext:document.managedObjectContext];
}
[document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
}];
});
dispatch_release(fetchQ);
}
Here is the factory method that first try to fetch objects from context (according to a pass-in object, which is fetched from flickr API). If result is nil, insert that object into context.
+ (Photo *)photoWithFlickrInfo:(NSDictionary *)flickrInfo
inManagedObjectContext:(NSManagedObjectContext *)context
{
Photo *photo = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Photo"];
request.predicate = [NSPredicate predicateWithFormat:#"unique = %#", [flickrInfo objectForKey:FLICKR_PHOTO_ID]];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"title" ascending:YES];
request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSError *error = nil;
NSArray *matches = [context executeFetchRequest:request error:&error];
if (!matches || ([matches count] > 1)) {
// handle error
} else if ([matches count] == 0) {
photo = [NSEntityDescription insertNewObjectForEntityForName:#"Photo" inManagedObjectContext:context];
photo.unique = [flickrInfo objectForKey:FLICKR_PHOTO_ID];
photo.title = [flickrInfo objectForKey:FLICKR_PHOTO_TITLE];
photo.subtitle = [flickrInfo valueForKeyPath:FLICKR_PHOTO_DESCRIPTION];
photo.imageURL = [[FlickrFetcher urlForPhoto:flickrInfo format:FlickrPhotoFormatLarge] absoluteString];
photo.whoTook = [Photographer photographerWithName:[flickrInfo objectForKey:FLICKR_PHOTO_OWNER] inManagedObjectContext:context];
} else {
photo = [matches lastObject];
}
return photo;
}
I already replied in your question Core data: executeFetchRequest vs performFetch.
Here what I wrote:
Executing the request within a loop could have impact on performances
but I would not be worried on that. Under the hood Core Data maintains
a sort of cache mechanism. Every time you perform a request, if data
are not in the cache, Core Data executes a round trip on your store
(e.g. sql file) and populate the cache with the objects it has
retrieved. If you perform the same query, the round trip will not
performed again due to the cache mechanism. Anyway, you could avoid to
execute a request within the run loop, simply moving that request
outside the loop.
In this case the request within the for loop is ok since you need to find the possible matches for the current (NSDictionary *)flickrInfo.
An alternative way, it could be to move the request outside the method
+ (Photo *)photoWithFlickrInfo:(NSDictionary *)flickrInfo
inManagedObjectContext:(NSManagedObjectContext *)context;
So for example, modify this method to accomodate a NSArray of results like:
+ (Photo *)photoWithFlickrInfo:(NSDictionary *)flickrInfo photoResults:(NSArray*)results
inManagedObjectContext:(NSManagedObjectContext *)context;
Replace the first snippet of code with the following
- (void)fetchFlickrDataIntoDocument:(UIManagedDocument *)document
{
dispatch_queue_t fetchQ = dispatch_queue_create("Flickr fetcher", NULL);
dispatch_async(fetchQ, ^{
NSArray *photos = [FlickrFetcher recentGeoreferencedPhotos];
// perform in the NSMOC's safe thread (main thread)
[document.managedObjectContext performBlock:^{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Photo"];
NSArray *results = [context executeFetchRequest:request error:&error];
for (NSDictionary *flickrInfo in photos) {
// This is the method that will call executeFetchRequest
[Photo photoWithFlickrInfo:flickrInfo photoResult:results inManagedObjectContext:document.managedObjectContext];
}
[document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
}];
});
dispatch_release(fetchQ);
}
In this case through the request you retrieve all the stored photos. The array (of managed objects) is passed to +(Photo*)photoWithFlickrInfo:photoResults:inManagedObjectContext:.
Now within +(Photo *)photoWithFlickrInfo:photoResults:inManagedObjectContext: you need to set a predicate for results that find the possible candidate based on [flickrInfo objectForKey:FLICKR_PHOTO_ID];. The motivation is quite simple: you have move the request outside the loop and now you need to retrieve the specific one. So, for example, you could do like:
+ (Photo *)photoWithFlickrInfo:(NSDictionary *)flickrInfo photoResults:(NSArray*)results
inManagedObjectContext:(NSManagedObjectContext *)context
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"unique == %#", [flickrInfo objectForKey:FLICKR_PHOTO_ID]];
NSArray* filteredPredicate = [results filterUsingPredicate:predicate];
// now filteredPredicate is the same as matches in the second snippet of your code.
// do the other code here..
}
Summarizing
Both approaches are valid. By means of them you can retrieve a photo already created or create a new one.
That's why loop is unavoidable. Am I wrong on this?
No, since you can try to follow my approach but the approach provided in Standford Course has a greater performance than the one I posted. I didn't made any performance test but if you are interested in you can do it yourself and analyze results by Instruments.
Simple tip
A simple change in the Standford code could be to perform Core Data operation in background preventing the main thread to be blocked. This approach could be useful if you have a lot of data. If data is minimal leave it as is.
I have the following code that currently clears all the objects in my NSManagedObjectContext:
- (void)clearObjectList:(NSString *)identifier
{
// TODO: Delete any entries with the identifier at the start of the object's name
NSLog(#"Clearing the URL list...");
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSFetchRequest * fetch = [[NSFetchRequest alloc] init];
[fetch setEntity:[NSEntityDescription entityForName:#"URL" inManagedObjectContext:context]];
NSArray * result = [context executeFetchRequest:fetch error:nil];
for (id basket in result)
{
// Code here to check if we should delete this object
[context deleteObject:basket];
}
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.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
//abort();
}
}
The Data model I have for URL is:
dateAccessed: Date
name: String
url: String
I want to access the objects key name to determine if it should be removed. How do I access this?
Try this:
for(URL *basket in result)
{
if([basket.name isEqualToString:identifier])
[context deleteObject:basket];
}
When looping through your results, you know that all you'll get back is NSManagedObjects. Or, if you've created NSManagedObject subclasses, you'll only get back URL objects. Thus, you can replace id with either NSManagedObject * or URL * in your for loop.
If you did create subclasses, which I'd recommend, you can access the name with dot notation: basket.name. If you didn't, you can access it by calling [basket valueForKey:#"name"].
In Xcode 3.X, you were supposed to right-click the whitespace in the fetch request template's predicate editor to specify a variable input rather than a hard-coded predicate.
Where is this in XCode 4? I've held option, right-clicked, option-clicked, etc and cannot figure it out....
I don't think X4 has the variable anymore.
Instead, I think you have to choose an expression and then provide a variable of the form $VARNAME.
For example, given and entity Alpha with an attribute aString, I created a fetch request template bobFetch with an expression of aString == $TESTVAR.
Alpha *a=[NSEntityDescription insertNewObjectForEntityForName:#"Alpha" inManagedObjectContext:self.moc];
a.aString=#"steve";
[self saveContext];
NSDictionary *subVars=[NSDictionary dictionaryWithObject:#"steve" forKey:#"TESTVAR"];
NSFetchRequest *fetchRequest = [self.managedObjectModel fetchRequestFromTemplateWithName:#"bobRequest" substitutionVariables:subVars];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Alpha" inManagedObjectContext:self.moc];
[fetchRequest setEntity:entity];
If logged fetchRequest reports:
<NSFetchRequest: 0x4d17480> (entity: Alpha; predicate: (aString == "steve"); sortDescriptors: ((null)); type: NSManagedObjectResultType; )
... and can then be used normally.
NSError *error = nil;
NSArray *fetchedObjects = [self.moc executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
NSLog(#"fetch error = %#",error);
}
NSLog(#"fetchObjects = %#",fetchedObjects);
Kind of clumsy for a graphical environment but it works.
This has been tweaked in Xcode 4. In order to use substitution variables, you need to choose "Expression" from the popup menu (i.e. instead of an attribute name) and you can enter the equivalent like this: name == $SEARCH_NAME
If you were to just enter a $VARIABLE value in the field for each attribute, you'll get the wrong result. In fact, some attributes won't allow that such as Date attributes where you are forced to enter a value.
Of course you can use multiple variables from there on.
Then it's just as before with executing the fetch request:
NSString *searchName = #"Mr Squiggle";
NSDictionary *subs = [NSDictionary dictionaryWithObject:searchName forKey:#"SEARCH_NAME"];
NSManagedObjectModel *model = [self managedObjectModel];
NSFetchRequest *req = [model fetchRequestFromTemplateWithName:#"trainerByName" substitutionVariables:subs];
NSError *error = nil;
NSArray *results = [[self managedObjectContext] executeFetchRequest:req error:&error];
NSLog(#"Found %ld record.", [results count]);
Note you can also do away with the attributes popup and just click the button on the top right of the editor (looks like lines right beside the default grid view button) and just enter your expression straight away. This is a good way of seeing how some things like dates get translated.
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.