Multiple fetch requests with complex predicates - objective-c

I have a couple of views which present data from a CoreData entity in my app. To retrieve the data required for the views I often have to implement multiple fetchRequests which feels wrong - perhaps I'm still making basic mistakes like thinking of CoreData in too much of a SQL database sense.
In fact for one of my views I have 22 fetch requests which may well be the correct way of achieving what I need but as an iPhone/Objective-C novice I can't help questioning my approach. Here is a snippet of my code showing 2 of many fetchRequest, could you give me a nudge in the right direction if I am doing it wrong?
SGK_T4T_01AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSEntityDescription *entityDiscription = [NSEntityDescription entityForName:#"Sessions" inManagedObjectContext:context];
//Swim 3 Count
NSFetchRequest *request2 = [[NSFetchRequest alloc] init];
[request2 setEntity:entityDiscription];
[request2 setResultType:NSDictionaryResultType];
NSExpression *keyPathExpression2 = [NSExpression expressionForKeyPath:#"sport"];
NSExpression *swimCountExpression = [NSExpression expressionForFunction:#"count:" arguments:[NSArray arrayWithObject:keyPathExpression2]];
NSExpressionDescription *expressionDescription2 = [[NSExpressionDescription alloc] init];
[expressionDescription2 setName:#"swimCount"];
[expressionDescription2 setExpression:swimCountExpression];
[expressionDescription2 setExpressionResultType:NSInteger16AttributeType];
[request2 setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription2]];
NSPredicate *pred2 = [NSPredicate predicateWithFormat:#"(date >= %# AND sport like %#)", swimSinceDateAsDate, sportTypeSwim];
[request2 setPredicate:pred2];
NSError *error2;
NSArray *objects2 = [context executeFetchRequest:request2 error:&error2];
if (objects2 == nil) {
NSLog(#"The fetch request returned an array == nil");
} else {
_swimTotalSwimCountLabel.text = [[NSString alloc] initWithFormat:#"%#", [[objects2 objectAtIndex:0] valueForKey:#"swimCount"]];
}
//Swim 4 Fastest 1500m Time Trial
NSFetchRequest *request3 = [[NSFetchRequest alloc] init];
[request3 setEntity:entityDiscription];
[request3 setResultType:NSDictionaryResultType];
NSExpression *keyPathExpression3 = [NSExpression expressionForKeyPath:#"time1"];
NSExpression *swimfastest1500Expression = [NSExpression expressionForFunction:#"min:" arguments:[NSArray arrayWithObject:keyPathExpression3]];
NSExpressionDescription *expressionDescription3 = [[NSExpressionDescription alloc] init];
[expressionDescription3 setName:#"swimFastest1500"];
[expressionDescription3 setExpression:swimfastest1500Expression];
[expressionDescription3 setExpressionResultType:NSInteger16AttributeType];
[request3 setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription3]];
NSString *sessType3 = #"Time Trial - 1500m";
NSPredicate *pred3 = [NSPredicate predicateWithFormat:#"(date >= %# AND sport like %# AND sessiontype like %#)", swimSinceDateAsDate, sportTypeSwim, sessType3];
[request3 setPredicate:pred3];
NSError *error3;
NSArray *objects3 = [context executeFetchRequest:request3 error:&error3];
if (objects3 == nil) {
NSLog(#"The fetch request returned an array == nil");
} else {
NSUInteger durationInSeconds = [[[objects3 objectAtIndex:0] valueForKey:#"swimFastest1500"] integerValue];
NSUInteger durationInMinutes = durationInSeconds / 60;
NSUInteger durationRemainder = durationInSeconds % 60;
_swimTt1500PaceLabel.text = [[NSString alloc] initWithFormat:#"%02i:%02i", durationInMinutes, durationRemainder];
}
Thanks in advance for any assistance, links or direction you may be able to provide...

The predicate stuff doesn't look too bad - but may I ask why you're limiting to all those NSExpressionDescriptions? Did you analyze and find that by limiting to just those properties you found concrete performance improvements? Core Data is usually pretty good at faulting/optimizing your memory, and that might clean up the statements quite a bit.

Related

Boolean values and core data

I have Boolean values in a entity, the values are all show 0.
The values are fetched and no matter what I always get back a TRUE value.
Here is the code, the problem is probably at the last 2 lines of code.
What am I doing wrong?
-(NSInteger)getStatus:(NSString*)nameID;
{
**//Fetch Request**
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"status"
inManagedObjectContext:[self managedObjectContext]];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate
predicateWithFormat:#"id_Name = %#", nameID];
[fetchRequest setPredicate:predicate];
NSError *error = nil;
NSArray *fetchedObjects = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
}
**//Values**
status *ENT_status;
ENT_status = [fetchedObjects objectAtIndex:0];
//Either way, both these lines return YES when it would be NO
//------------------------------------------------------------------
BOOL one = ENT_status.status_one;
NSNumber *two = [NSNumber numberWithBool:ENT_status.status_two];
By default, boolean values are represented as NSNumber objects in the managed objects.
So you can either use it as it is:
NSNumber *one = ENT_status.status_one;
or convert it to a plain (scalar) BOOL:
BOOL one = [ENT_status.status_one boolValue];
(In your code, the pointer ENT_status.status_one is always interpreted as YES.)
Alternatively, you can use the option "Use scalar properties for primitive data types"
when creating the managed object subclass in Xcode.
I know its a bit late and probably no one is using Objective-C anymore but i came across this problem and what worked for me was:
to retrieve a boolean value from object:
BOOL myBool = [[myManagedObject valueForKey:#"myKey"] boolValue];
and to set the new value:
myManagedObject.myKey = #YES;

Filtering many-to-many count expression using existing subquery

In my app, I have a many-to-many relationship between tags and links as follows :
Tags <<-->> Links
I am trying to return a list of the tags that relate to links that have the currently active tags, but are not included in the active tags.
I also want to obtain a count of the number of links that have the 'other' tags, which needs to be limited by the active tags.
Using the below, I have been able to return the 'other' tags and a count of links, but the count returned is of all links for each tag.
I would like to be able to count the links using a similar approach to the one I'm using to build the subquery, but am struggling to get it to work. I have tried using the subquery generated in the count NSExpression, but this errors when the subquery is evaluated.
// Test array of tag names
self.activeTagArray = [#[#"tag1", #"tag2"] mutableCopy];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:[Tag entityName]];
// We want to exclude the tags that are already active
NSPredicate *activeTagsPredicate = [NSPredicate predicateWithFormat:#"NOT ANY name IN %#", self.activeTagArray];
// Build subquery string to identify links that have all of the active tags in their tag set
NSString __block *subquery = #"SUBQUERY(links, $link, ";
[self.activeTagArray enumerateObjectsUsingBlock:^(id tagName, NSUInteger index, BOOL *stop) {
if (index == self.activeTagArray.count - 1) {
subquery = [subquery stringByAppendingString:[NSString stringWithFormat:#"SUBQUERY($link.tags, $tag, $tag.name = '%#') != NULL", tagName]];
} else {
subquery = [subquery stringByAppendingString:[NSString stringWithFormat:#"SUBQUERY($link.tags, $tag, $tag.name = '%#') != NULL AND ", tagName]];
}
}];
subquery = [subquery stringByAppendingString:#") != NULL"];
NSLog(#"Subquery : %#", subquery);
NSPredicate *noTagsPredicate = [NSPredicate predicateWithFormat:subquery];
// Create a predicate array
NSArray *predicateArray = #[noTagsPredicate, activeTagsPredicate, userPredicate];
NSPredicate *compoundPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:predicateArray];
fetchRequest.predicate = compoundPredicate;
fetchRequest.relationshipKeyPathsForPrefetching = #[#"links"];
// Set up the count expression
NSExpression *countExpression = [NSExpression expressionForFunction: #"count:" arguments:#[[NSExpression expressionForKeyPath: #"links.href"]]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
expressionDescription.name = #"counter";
expressionDescription.expression = countExpression;
expressionDescription.expressionResultType = NSInteger32AttributeType;
fetchRequest.propertiesToFetch = #[#"name", expressionDescription];
// Sort by the tag name
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES];
fetchRequest.sortDescriptors = #[sortDescriptor];
fetchRequest.resultType = NSDictionaryResultType;
NSError *error = nil;
NSArray *resultsArray = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error) {
NSLog(#"Error : %#", [error localizedDescription]);
}
NSMutableArray *allTags = [[NSMutableArray alloc] init];
for (NSDictionary *tagDict in resultsArray) {
NSLog(#"Tag name : %#, Link Count : %#", tagDict[#"name"], tagDict[#"counter"]);
[allTags addObject:tagDict[#"name"]];
}
[allTags addObjectsFromArray:self.activeTagArray];
Any help with this would be greatly appreciated!
If I understand your question correctly, the following predicate fetches your "other tags",
i.e. all tags that are related to a link which is related to all of the given "active tags":
NSArray *activeTags = #[#"tag1", #"tag2"];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Tag"];
NSPredicate *p1 = [NSPredicate predicateWithFormat:#"NOT name in %#", activeTags];
NSPredicate *p2 = [NSPredicate predicateWithFormat:#"SUBQUERY(links, $l, SUBQUERY($l.tags, $t, $t.name IN %#).#count = %d).#count > 0",
activeTags, [activeTags count]];
NSPredicate *predicate = [NSCompoundPredicate andPredicateWithSubpredicates:#[p1, p2]];
[request setPredicate:predicate];
And now the "trick": The left hand side of the predicate p2 is
SUBQUERY(links, $l, SUBQUERY($l.tags, $t, $t.name IN %#).#count = %d).#count
and that is exactly the count of links that should be included in the result,
so we can create the expression description from that predicate:
NSExpression *countExpression = [(NSComparisonPredicate *)p2 leftExpression];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
expressionDescription.name = #"counter";
expressionDescription.expression = countExpression;
expressionDescription.expressionResultType = NSInteger32AttributeType;
[request setResultType:NSDictionaryResultType];
[request setPropertiesToFetch:#[#"name", expressionDescription]];
The resulting SQLite query is quite complex. It might be sensible to fetch the links first:
NSFetchRequest *linkRequest = [NSFetchRequest fetchRequestWithEntityName:#"Link"];
NSPredicate *linkPredicate = [NSPredicate predicateWithFormat:#"SUBQUERY(tags, $t, $t.name IN %#).#count = %d",
activeTags, [activeTags count]];
[linkRequest setPredicate:linkPredicate];
NSArray *activeLinks = [context executeFetchRequest:linkRequest error:&error];
and fetch the tags in a separate step, which can be done with above code where only the
predicate p2 is replace by the simpler subquery
NSPredicate *p2 = [NSPredicate predicateWithFormat:#"SUBQUERY(links, $l, $l IN %#).#count > 0", activeLinks];

Real World Peformance of SQL vs Core Data: Need Perspective and Perhaps Specific Fixes

I have a word game app that accesses a list of words and clues, which is about 100K entries. The data base is only accessed, never changed. I built the app using SQL methods and it performs pretty well on iOS 6 but the time to get a new clue from the data base is extremely slow on iOS 5:
iOS 5, getting one record from the 100K using SQL, takes about 12
seconds.
iOS 6, getting one record from the 100K using SQL, takes
about 700-1000 milliseconds.
Both of these are on 32 GB iPod Touch.
Given this performance, I made a version using Core Data. My approach gets a random data base entry by first counting the records that fit the query, then chosing one at random. Code follows. Everything I read suggested that Core Data would be faster:
iOS 5, counting records takes around 4 seconds, and retrieving one of
those records takes about 50 - 1500 millisecs. A total time of about 5 seconds.
iOS 6, counting records takes a bit over 2 seconds, and retrieving
one of those records takes about 300-500 millisecs. A total of about 3 seconds.
So Core Data is faster on iOS 5 but slower on iOS 6, compared to SQL. Either way the performance is too slow as far as I am concerned. I know the overhead comes from the methods given below (for the Core Data version). So, two questions:
Any general advice about this issue with an eye to understanding it and improving performance?
Specifically, what about the Core Data code appended below: have I done something foolish that slows it down? Or something else I should add to speed it up? This is my first attempt at Core Data.
Thanks.
- (NSArray *) randomClue {
NSManagedObjectContext* context = [self managedObjectContext];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"A"];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"WL28"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [self createSearchQuery];
[fetchRequest setPredicate:predicate];
NSError *error;
NSDate *date1 = [NSDate date];
NSString *timeString1 = [formatter stringFromDate:date1];
int resCount = [context countForFetchRequest:fetchRequest
error:&error];
NSDate *date2 = [NSDate date];
NSString *timeString2 = [formatter stringFromDate:date2];
int t1 = [timeString1 intValue];
int t2 = [timeString2 intValue];
int d1 = t2-t1;
NSLog(#"randomClue:");
NSLog(#" Time to count array entries: %i", d1);
int ranNum = arc4random_uniform(resCount-1);
int ranNum2 = ranNum + 1;
// Now we fetch just one answer object, not a whole database or even a piece of it!
[fetchRequest setReturnsObjectsAsFaults:YES];
[fetchRequest setPropertiesToFetch:nil];
[fetchRequest setFetchLimit:1];
[fetchRequest setFetchOffset:ranNum2];
NSDate *date3 = [NSDate date];
NSString *timeString3 = [formatter stringFromDate:date3];
self.wl28 = [context executeFetchRequest:fetchRequest error:&error];
NSDate *date4 = [NSDate date];
NSString *timeString4 = [formatter stringFromDate:date4];
int t3 = [timeString3 intValue];
int t4 = [timeString4 intValue];
int d2 = t4-t3;
NSLog(#" Time to retrieve one entry: %i", d2);
return self.wl28;
}
EDIT: createSearchQuery added below
- (NSPredicate *)createSearchQuery {
NSMutableArray *pD = [[GameData gameData].curData valueForKey:#"persData"];
NSNumber *currMin = [pD objectAtIndex:0];
NSNumber *currMax = [pD objectAtIndex:1];
NSNumber *dicNo = [pD objectAtIndex:2];
NSString *dict = nil;
if ([dicNo intValue] == 0) dict = #"TWL";
if ([dicNo intValue] == 1) dict = #"LWL";
NSPredicate *dictPred = [NSPredicate predicateWithFormat:#"dict == %#", dict];
NSPredicate *lowNoCPred = [NSPredicate predicateWithFormat:#"noC >= %#", currMin];
NSPredicate *highNoCPred = [NSPredicate predicateWithFormat:#"noC <= %#", currMax];
NSPredicate *query = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray
arrayWithObjects:dictPred, lowNoCPred, highNoCPred,nil]];
NSLog(#"%#", query);
return query;
}
You can try adding index to your noC and/or dict attributes inside your entity. It might speed up your query time.

NSPredicate always gives back the same data

I am trying to work with NSPredicates. But it always give me back the same array. Here you can see my predicate.
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"whichAlbum.album_id == %d", AlbumId];
[request setEntity:[NSEntityDescription entityForName:#"Picture" inManagedObjectContext:self.genkDatabase.managedObjectContext]];
[request setPredicate:predicate];
Also when I try it hardcoded. It gives back the same array.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"whichAlbum.album_id == 5"];
My database model is:
And here you can see how I put data in my database for entity Picture.
+ (Picture *)pictureWithGenkInfo:(NSDictionary *)genkInfo
inManagedObjectContext:(NSManagedObjectContext *)context
withAlbumId:(int)albumId
withPictureId:(int)pictureId;
{
Picture *picture = nil;
picture = [NSEntityDescription insertNewObjectForEntityForName:#"Picture"
inManagedObjectContext:context];
picture.url = [genkInfo objectForKey:PICTURES_URL];
picture.pic_album_id = [NSNumber numberWithInt:albumId];
picture.picture_id = [NSNumber numberWithInt:pictureId];
return picture;
}
Anybody can help me ?
Kind regards
EDIT
for (NSDictionary *genkInfo in albums ) {
albumId++;
Album *album = [Album albumWithGenkInfo:genkInfo inManagedObjectContext:document.managedObjectContext withAlbumId:albumId];
for (NSDictionary *genkInfo2 in pictures ) {
pictureId++;
Picture *pic = [Picture pictureWithGenkInfo:genkInfo2 inManagedObjectContext:document.managedObjectContext withAlbumId:albumId withPictureId:pictureId];
[album addPicturesObject:pic]; // this method should be automatically generated
}
pictureId = 0;
// table will automatically update due to NSFetchedResultsController's observing of the NSMOC
}
Better, assuming the value for AlbumId is some kind of number primitive:
old style:
[NSPredicate predicateWithFormat:#"whichAlbum == %#", [NSNumber numberWithInt:AlbumId]];
modern style: (xcode 4.5)
[NSPredicate predicateWithFormat:#"whichAlbum == %#", #(albumId)];
As it looks like predicateWithFormat: might only generate proper predicates with %# and #K in its format strings.
Best: Assuming you have access to the Album managed object you are trying to match try this:
[NSPredicate predicateWithFormat:#"whichAlbum == %#", album];
Match to the object, not one of its properties
just remove whichAlbum and try:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"album_id == %d", AlbumId];

Core Data: Count across relationships

I have three models:
+----------+ +--------------+ +------+
| Occasion | <--->> | GiftOccasion | <--->> | Gift |
+----------+ +--------------+ +------+
I've like to get the name of the occasion, and a count of all the gifts that meet certain criteria. Currently I'm getting the distinct list of occasions sorted by year and name:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request retain];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Occasion" inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];
[request setResultType:NSDictionaryResultType];
[request setPropertiesToFetch:[NSArray arrayWithObjects:#"name", #"year", nil]];
[request setReturnsDistinctResults:YES];
NSSortDescriptor *yearDescriptor = [[[NSSortDescriptor alloc] initWithKey:#"year" ascending:NO selector: #selector(caseInsensitiveCompare:)] autorelease];
NSSortDescriptor *nameDescriptor = [[[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES selector: #selector(caseInsensitiveCompare:)] autorelease];
NSArray *descriptors = [NSArray arrayWithObjects:yearDescriptor, nameDescriptor, nil];
[request setSortDescriptors:descriptors];
NSError *error = nil;
NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:&error];
... but I'm totally stumped as to how to get the count of Gift. Is there a straightforward way, or do I need to traverse each GiftOccasion and related Gifts and add it all together?
ETA: Note that I'm generating distinct results, here, not getting the model directly. I assume I'd have to run another request to get this information, but if it's possible not to, that would be better.
Thanks!
Get number of objects in a Core Data relationship
This isn't a good answer, and I'm not going to select it as the "right" answer, but what I ended up doing was a ridiculous set of nested NSEnumerators that go through each relationship and eventually do a count += 1;, something like this:
NSUInteger giftCount = 0;
NSEnumerator *oEnum = [resultsList objectEnumerator];
Occasion *occasion = nil;
while (occasion = [oEnum nextObject]) {
NSEnumerator *goEnum = [occasion.giftOccasions objectEnumerator];
GiftOccasion *go;
while (go = [goEnum nextObject]) {
if (go.gifts) {
NSEnumerator *giftEnum = [go.gifts objectEnumerator];
Gift *gift = nil;
while (gift = [giftEnum nextObject]) {
NSLog(#"Gift: %# %#", gift.name, gift.purchased);
if ([gift.purchased boolValue] == NO) {
giftCount += 1;
}
}
}
}
}
Embarrassing, but it works. :-)
If there is a better, more efficient way to do this, I'd love to hear it. It's one of the things I dislike about Core Data as an ORM vs., say, ActiveRecord.