Core Data Optimize Fetch Request - core-data

This Fetch Request takes almost 10 seconds to execute on iPhone.
The goal is to fetch 1 random thumbnail from each category.
setting fetchlimit = 1 most of the times return the same thumbnail so I have to fetch all photos from each category.
Any ideas ?
[categoriesArray enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Photos"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"place.subcategory.category == %#", object];
[request setPredicate:predicate];
NSError *error;
NSArray *photosManagedObjectsArray = [managedObjectContext executeFetchRequest:request error:&error];
NSUInteger randomIndex = arc4random() % [photosManagedObjectsArray count];
NSManagedObject *photoObject = [photosManagedObjectsArray objectAtIndex:randomIndex];
UIImage *photoImage = [UIImage imageWithData:[photoObject valueForKey:#"thumbnail"]];
UIImage *resizedImage = [photoImage imageCroppedToFitSize:CGSizeMake(IMAGE_WIDTH, IMAGE_HEIGHT)];
[imagesArray addObject:resizedImage];
[objectsArray addObject:photoObject];
}];
many thanks!
Bill.

You don't need to pull all your objects into an array. Just change
NSArray *photosManagedObjectsArray = [managedObjectContext executeFetchRequest:request error:&error];`
NSUInteger randomIndex = arc4random() % [photosManagedObjectsArray count];
to this
NSUInteger count = [managedObjectContext countForFetchRequest:request error:&error];
NSUInteger randomIndex = arc4random() % count;
Now use fetchOffset to grab just the single object you need.
[request setFetchOffset:randomIndex];
[request setFetchLimit:1];

Related

Core Data - NSPredicate for objects with identical column and also meet another condition

I am looking for a predicate to fetch all managed objects of type Entity whose values are duplicated in a property sessionId, where all groups' ("groups", meaning managed objects whose sessionId's are equal) contents' flags in a property processed is set to YES. This can be done (slowly), but I am looking for an efficient one liner for this. Thanks
This is the slow way:
NSFetchRequest *request = [NSEntityDescription entityForName:#"Entity"
inManagedObjectContext:context];
NSArray *all = [context executeFetchRequest:request error:nil];
NSArray *sessionIds = [all valueForKeyPath:#"#distinctUnionOfObjects.sessionId"];
NSMutableArray *objects = [NSMutableArray array];
for (NSString *sessionId in sessionIds) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"sessionId == %#", sessionId];
NSArray *inSession = [all filteredArrayUsingPredicate:predicate];
for(id obj in inSession) {
if(![obj valueForKey:#"processed"]) continue;
}
[objects arrayByAddingObjectsFromArray:processed];
}
NSLog(#"%#", objects);
Since booleans are stored as 0s and 1s, a group where all rows have processed = YES will have average(processed) = 1. Hence you can use NSFetchRequest's propertiesToGroupBy and havingPredicate to get the sessionIds that meet your criteria. A second fetch is then required to get the Entity objects with any of those sessionIds:
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
fetch.resultType = NSDictionaryResultType;
fetch.propertiesToFetch = #[#"sessionId"];
fetch.propertiesToGroupBy = #[#"sessionId"];
fetch.havingPredicate = [NSPredicate predicateWithFormat: #"average:(processed) == 1"];
NSArray *resultsArray = [context executeFetchRequest:fetch error:nil];
NSArray *sessionIdArray = [resultsArray valueForKeyPath:#"sessionId"];
NSFetchRequest *newFetch = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
newFetch.predicate = [NSPredicate predicateWithFormat:#"name IN %#",sessionIdArray];
NSArray *finalResults = [context executeFetchRequest:newFetch error:nil];
NSLog(#"Final results, %#", finalResults);
Sorry it's not a one-liner. And I leave it to you to determine whether it's any quicker than your own code.
EDIT
To do it all in one fetch, use NSFetchRequestExpression in place of the intermediate arrays:
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
fetch.resultType = NSDictionaryResultType;
fetch.propertiesToFetch = #[#"sessionId"];
fetch.propertiesToGroupBy = #[#"sessionId"];
fetch.havingPredicate = [NSPredicate predicateWithFormat: #"average:(processed) == 1"];
NSExpression *fetchExpression = [NSFetchRequestExpression expressionForFetch:[NSExpression expressionForConstantValue:fetch] context:[NSExpression expressionForConstantValue:context] countOnly:false];
NSFetchRequest *newFetch = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
newFetch.predicate = [NSPredicate predicateWithFormat:#"sessionId IN %#",fetchExpression];
NSArray *finalResults = [context executeFetchRequest:newFetch error:nil];
NSLog(#"Final results, %#", finalResults);
Note that on my (admittedly trivial) test setup this actually ran more slowly than the two-fetch solution.
FYI, if you use the SQLDebug build setting to examine the SQL that is generated, it looks something like this:
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZSESSIONID, t0.ZPROCESSED FROM ZENTITY t0 WHERE t0.ZSESSIONID IN (SELECT n1_t0.ZSESSIONID FROM ZENTITY n1_t0 GROUP BY n1_t0.ZSESSIONID HAVING avg( n1_t0.ZPROCESSED) = ? )

core data retrieving data from sets with many to many relationships

I am new to core data and am still trying to get my head around it. I have a problem that I have spent hours on trying to solve. I just can't seem to solve it and was hoping someone could help me.
I can't post images yet so here is a link to my data model:
http://yourpcsolution.net/model.png
Each relationship has an inverse. A Game can have many Periods and a Period can have many Players. A Player can be in more than one Period.
Here is my code for populating DB:
- (void) prepareDB {
Games *game = [NSEntityDescription insertNewObjectForEntityForName:#"Games" inManagedObjectContext:self.managedObjectContext];
game.date = [NSDate date];
game.name = #"Game 1";
Periods *period = [NSEntityDescription insertNewObjectForEntityForName:#"Periods" inManagedObjectContext:self.managedObjectContext];
period.date = [NSDate date];
period.name = #"1st Period";
NSMutableSet *gameSet = [[NSMutableSet alloc] initWithObjects:period, nil];
game.periods = gameSet;
Players *player = [NSEntityDescription insertNewObjectForEntityForName:#"Players" inManagedObjectContext:self.managedObjectContext];
player.name = #"Player1";
NSMutableSet *playerSet = [[NSMutableSet alloc] initWithObjects:player, nil];
period.players = playerSet;
NSError *error;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
}
I think that's the correct way to add data because when looking at the sql db the data is there. My problem is in trying to retrieve the players that belong to each period. Because Periods is a set in Games and Players is a set in Periods in core data. So I can't retrieve players by going: game.periods.players
Here is my code for retrieving a Period and in the xcode log i am getting a fault:
"Relationship 'players' fault on managed object (0x8e72bc0) <Periods: 0x8e72bc0>
(entity: Periods; id: 0x8e727c0 <x-coredata://7F63902B-FCB6-4ACA-BB40-904755D37A4A/Periods/p1>;
data: {\n date = \"2013-07-09 19:35:48 +0000\";\n
games = \"0x8e6d760 <x-coredata://7F63902B-FCB6-4ACA-BB40-904755D37A4A/Games/p1>\";\n
name = \"1st Period\";\n players = \"<relationship fault: 0x9840330 'players'>\";\n})"
My Code for retrieving a Period:
NSString *firstPeriod = #"1st Period";
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name == %#", firstPeriod];
[request setEntity:[NSEntityDescription entityForName:#"Periods" inManagedObjectContext:self.managedObjectContext]];
[request setPredicate:predicate];
NSArray *period = [self.managedObjectContext executeFetchRequest:request error:&error];
I'm not sure how to proceed from here to retrieve the players. I know how to retrieve what Periods belong to each game but I don't know how to retrieve the players that belong to a period. I have been trying for days to figure this out and it's just not working.
Is my data model correct? How should I be retrieving the data? Any help or advice is appreciated.
To fetch all players of a given period, the following should work:
NSString *firstPeriod = #"1st Period";
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Players" inManagedObjectContext:self.managedObjectContext]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANY periodsin.name == %#", firstPeriod];
[request setPredicate:predicate];
NSArray *players = [self.managedObjectContext executeFetchRequest:request error:&error];
"ANY" in the predicate is necessary because each player is connected to multiple Periods.
Remark: The data type of a to-many relationship is NSSet, not NSMutableSet.
ADDED: To fetch all periods for a given player, you can use the following fetch request:
Players *givenPlayer = ... ; // the given player
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Periods" inManagedObjectContext:self.managedObjectContext]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANY players == %#", givenPlayer];
[request setPredicate:predicate];
NSArray *periods = [self.managedObjectContext executeFetchRequest:request error:&error];
Alternatively, you can access the relationship directly:
Players *givenPlayer = ... ; // the given player
NSArray *periods = [givenPlayer.periodsin allObjects]; // "allObjects" converts NSSet to NSArray

NSPredicate with 20 last records

I'm working with core data and I try to get last 20 records using setFecthLimit but in these last records I want to get count of unreads, I use this
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Post" inManagedObjectContext:_managedObjectContext];
[request setEntity:entity];
[request setFetchLimit:20];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"isRead = NO OR isRead = NIL"];
[request setPredicate:predicate];
NSError *error = nil;
NSMutableArray *records = [[_managedObjectContext
executeFetchRequest:request
error:&error] mutableCopy];
but it always return me 20, can anyone help me please?
A simple solution is to remove the predicate. Grab 20 objects and then filter them based on the predicate.
For example:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Post" inManagedObjectContext:_managedObjectContext];
[request setEntity:entity];
[request setFetchLimit:20];
NSError *error = nil;
NSArray *records = [_managedObjectContext executeFetchRequest:request
error:&error];
// do some error checking here...
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"isRead = NO OR isRead = nil"];
// [NSPredicate predicateWithFormat:#"isRead = %#", [NSNumber numberWithBool:NO]];
NSArray *filteredRecords = [records filteredArrayUsingPredicate:predicate];
I think you have a typo in your predicate format:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Post"]
request.fetchLimit = 20;
request.predicate = [NSPredicate predicateWithFormat:#"(isRead == NO) OR (isRead == NULL)"];
NSSortDescriptor = [...insert your sort descriptor code here...]
request.sortDEscriptos = #[sd];
And remember to sort, such that you get the last 20, not just a random 20.

(Core Data )Fetch an specific entity with a max property

I have a Entity wich has a toMany relationship to another one. In this second one I have an attribute called "versionNumber" so. I have an object on entity type A, and I want to get the related entity B which has the biggest (max) versionNumber.
I have the following but that returns me a result obtained over all records on entity B, not over the specific entities related to the object of type A.
NSInteger vNumber = 0;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:DPA_VERSION_KEY inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];
// Specify that the request should return dictionaries.
[request setResultType:NSDictionaryResultType];
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:VERSION_NUMBER_KEY];
NSExpression *maxNumberExpression = [NSExpression expressionForFunction:#"max:"
arguments:[NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:#"maxNumber"];
[expressionDescription setExpression:maxNumberExpression];
[expressionDescription setExpressionResultType:NSDecimalAttributeType];
[request setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
// Execute the fetch.
NSError *error = nil;
NSArray *objects = [[self managedObjectContext] executeFetchRequest:request error:&error];
if (objects == nil) {
// Handle the error.
}
else {
if ([objects count] > 0) {
vNumber = [[[objects objectAtIndex:0] valueForKey:#"maxNumber"] integerValue] +1;
}
}
[expressionDescription release];
[request release];
return vNumber;
I have an idea but I hadn't been able to materialize it. I must ask SELF which is my object A to do that fetch over its relationship toVersions (Entity B).
Thanks for the help.
G.
Set a predicate to limit the request to only those B objects who have a relationship with A.
[request setPredicate:[NSPredicate predicateWithFormat:#"myA == %#", myA];

Core Data complex query

I have model that have field date. I need to fetch all count of records for all day of year or if no record for this day presented fetch as 0. Now i fetch all record for year and make it by hands. Can I do it using core data queries?
Typically in Core Data it is much faster to do all the fetching you will need to do up front rather than breaking it up into multiple fetches.
I tried to count records by day of the year in two ways:
Fetch all the records for a year. Find distinct dates for those records. Loop over the dates and count the records.
Loop over each day in the year. Perform a count-only fetch for the records with that date.
With a small database (48 records), the first approach was about 90 times faster. I imagine the performance of the first approach would get worse as more records are added and the second approach would stay about the same.
Here is the code:
- (void)doFullLoadTestWithYear:(int)year {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Record" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSCalendar *gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
NSDateComponents *comps = [[[NSDateComponents alloc] init] autorelease];
[comps setDay:1];
[comps setMonth:1];
[comps setYear:year];
NSDate *start = [gregorian dateFromComponents:comps];
[comps setDay:31];
[comps setMonth:12];
NSDate *end = [gregorian dateFromComponents:comps];
NSPredicate *yearPred = [NSPredicate predicateWithFormat:#"date >= %# && date <= %#",start,end];
[fetchRequest setPredicate:yearPred];
NSError *error = nil;
NSArray *array = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
[fetchRequest release];
if (error) {
NSLog(#"Error during fetch: %#",[error localizedDescription]);
abort();
}
int dateCount = 0, recordCount = 0;
NSArray *dates = [array valueForKeyPath:#"#distinctUnionOfObjects.date"];
for (NSDate *date in dates) {
NSPredicate *pred = [NSPredicate predicateWithFormat:#"date == %#",date];
NSArray *records = [array filteredArrayUsingPredicate:pred];
dateCount++;
recordCount += [records count];
NSLog(#"%d record(s) with date %#",[records count],date);
}
NSLog(#"Record count for year is %d",recordCount);
NSLog(#"Distinct dates count is %d",dateCount);
}
- (void)doSeparateTestWithYear:(int)year {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Record" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSCalendar *gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
NSDateComponents *comps = [[[NSDateComponents alloc] init] autorelease];
[comps setDay:1];
[comps setMonth:1];
[comps setYear:year];
NSDate *start = [gregorian dateFromComponents:comps];
[comps setYear:year+1];
NSDate *end = [gregorian dateFromComponents:comps];
NSDateComponents *oneDay = [[[NSDateComponents alloc] init] autorelease];
[oneDay setDay:1];
int dateCount = 0, recordCount = 0;
NSDate *date = start;
while (![date isEqual:end]) {
NSPredicate *dayPred = [NSPredicate predicateWithFormat:#"date == %#",date];
[fetchRequest setPredicate:dayPred];
NSError *error = nil;
int count = [self.managedObjectContext countForFetchRequest:fetchRequest error:&error];
if (error) {
NSLog(#"Error during fetch: %#",[error localizedDescription]);
abort();
}
if (count > 0) {
NSLog(#"%d record(s) with date %#",count,date);
dateCount++;
recordCount += count;
}
date = [gregorian dateByAddingComponents:oneDay toDate:date options:0];
}
NSLog(#"Record count for year is %d",recordCount);
NSLog(#"Distinct dates count is %d",dateCount);
[fetchRequest release];
}

Resources