Core Data: Count across relationships - objective-c

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.

Related

Objective-c. Fetching distinct values of attribute from core data

i have run into a problem that i can not solve. I have a "database" - read core data - where i have attribute that holds a value and level.
Something like that
value -------- level
55 -------------4
33 -------------4
50 -------------5
70 -------------6
44 -------------5
what i want now is a to extract all values from level 5 only and add them together. How can i achieve this ?I did found "fetch distinct values" on apple dev site, but this would apply to extracting all values from one attribute.
Help appreciated, thank you. If i have missed a similar topic then please provide me with a link. Thanks
You can use the following fetch request:
// Fetch request for your entity:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
[request setResultType:NSDictionaryResultType];
// Restrict result to "level == 5":
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"level == %d", 5];
[request setPredicate:predicate];
// Expression description for "#sum.value":
NSExpression *sumExpression = [NSExpression expressionForKeyPath:#"#sum.value"];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:#"sumValue"];
[expressionDescription setExpression:sumExpression];
[expressionDescription setExpressionResultType:NSInteger32AttributeType];
[request setPropertiesToFetch:#[expressionDescription]];
NSArray *result = [context executeFetchRequest:request error:&error];
The result for your data is
(
{
sumValue = 94;
}
)
i.e. an array containing one dictionary with the sum of values with level=5.

Invalid operations to binary expression ('id' and 'id')

I have a array fetched from the core data and trying to do some simple calculation but getting errors of Invalid operations to binary expression
pop_cum[i]= (pop_ln_array[i-1] + pop_ln_array[i]);
//getting error at this point of "Invalid operations to binary expression('id' and 'id')
I know we have to change the type of the array values into int but how?
NSManagedObjectContext *moc = [self managedObjectContext];
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Input_Details" inManagedObjectContext:moc];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
// Set example predicate and sort orderings...
request.propertiesToFetch = #[ #"pop_Ln" ];
request.resultType = NSDictionaryResultType;
//if you change the sort order here, please change it to SDInputDetailsList also
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
initWithKey:#"sewer_No" ascending:YES];
[request setSortDescriptors:#[sortDescriptor]];
NSError *error;
NSArray *pop_ln_array = [moc executeFetchRequest:request error:&error];
int arrayCount=[pop_ln_array count];
//Calculating cumulative population, flow & peak factor
NSLog(#"arrayCount is %d",arrayCount);
if (pop_ln_array == nil)
{
NSLog(#"Cumilative population line array is nil");
}
else
{
NSArray *pop_cum[arrayCount];
for(int i=0;i<[pop_ln_array count];i++)
{
NSLog(#"Cumilative array at %d is %#",i,pop_ln_array[i]);
if (i==0)
{
pop_cum[i]=pop_ln_array[i] ;
}
else
{
pop_cum[i]= (pop_cum[i-1] + pop_ln_array[i]); //getting error at this point of "Invalid operations to binary expression('id' and 'id')
}
}
}
Use as follows
for(int i=0;i<[pop_ln_array count];i++){
NSLog(#"Cumilative array at %d is %#",i,pop_ln_array[i]);
if (i==0){
pop_cum[i]=pop_ln_array[i];
NSLog(#"pop_cum[%d] %#",i,pop_cum[i]);
}
else {
NSLog(#"Pop_cum %#",pop_cum[i-1]);
[pop_ln_array[i]intValue]);
NSInteger first=[pop_ln_array[i-1]intValue], second=[pop_ln_array[i]intValue];
NSInteger sum=first+second;
pop_cum[i]=[NSString stringWithFormat:#"%d",sum];
}
}
You are adding two ids. Which can not be.
Convert it to integerValue or intValue or any numerical and then add.

Joining tables in Core Data

I have been trying to solve the following problem using Core Data without any luck.
My model has two entities:Group and Element. Both with a "name" attribute and a to-many relationship in the form of: "group.elements" and "element.groups" (a Element belonging to several Groups and a Group having several Elements)
I want to establish a "filter" in the form of:
elements that belongs to "group_A" AND "group_B"
In order to show to the user something like:
The elements that match the filter belong to this set of groups in that quantity
As an example, having something like:
Element_1 Group_A, Group_B, Group_C
Element_2 Group_B, Group_C
Element_3 Group_A, Group_B, Group_D
Element_4 Group_A, Group_B, Group_D
Element_5 Group_C, Group_D
The answer should be: Element_1, Element_3 and Element_4 match the filter and the information to be shown would be like:
Group_A has 3 elementsGroup_B has 3 elementsGroup_C has 1 elementGroup_D has 2 elementsThat match the filter
How could I put this in Core Data NSExpression, NSPredicate etc.?
Thanks.
UPDATE
I think I found two ways of solving this.
Option 1
This option establishes an NSArray with the "group's names filter" and returns all the groups with the number of elements they have that match the condition EVEN THOUGH it's zero (no elements match)
There are two entities, "Grp" and "Elem", with a to-many relationship between them.
NSError *error = nil;
// Properties to be fetched
NSPropertyDescription *namePropDesc = [[[[self.moModel entitiesByName] objectForKey:#"Grp"] propertiesByName] objectForKey:#"name"];
// Variable group filter
NSArray *grpFilter = [NSArray arrayWithObjects:#"group_A", #"group_B", nil];
// Expression for counting elements
NSExpressionDescription *countExprDesc = [[NSExpressionDescription alloc] init];
[countExprDesc setExpression:[NSExpression expressionWithFormat:#"SUBQUERY(elems,$elem, SUBQUERY($elem.grps, $grp, $grp.name in %#).#count==%d).#count", grpFilter, grpFilter.count]];
[countExprDesc setExpressionResultType:NSInteger32AttributeType];
[countExprDesc setName:#"elementCount"];
// Create data fetching and set its properties
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Grp"];
[request setResultType:NSDictionaryResultType];
[request setPropertiesToFetch:[NSArray arrayWithObjects:namePropDesc,countExprDesc, nil]];
NSArray *results = [self.moContext executeFetchRequest:request error:&error];
NSLog(#"results = %#"results);
Option 2
This option establishes an NSArray with the "group's names filter" and returns all the groups with the number of elements they have that match the condition WITHOUT THOSE groups that don't have any elements.
In this case, I created three entities, Grp, Elem and RGE. Having RGE as an intermediate entity that keeps the to-many relationships with the two other. This option allows to put some extra information in the group-element association (creation date, etc.) if needed. Grp and Elem don't have a relationship between each other.
Note: In fact, I neened to create a "regular" field (name) in the RGE entity to apply the #count function. If a "to-many relationship field" is used it fails to count properly.
NSError *error = nil;
// Variable group filter
NSArray *grpFilter = [NSArray arrayWithObjects:#"group_A", #"group_B", nil];
// Create variable predicate string from "group's names filter"
NSMutableString *predicateStr = [[NSMutableString alloc] init];
for(int n=0;n<grpFilter.count;n++) {
if(n>0) {
[predicateStr appendString:#" AND "];
}
[predicateStr appendString:#"(ANY elem.rges.grp.name=%#)"];
}
// Filter to be applied
NSPredicate *filterQuery = [NSPredicate predicateWithFormat:predicateStr argumentArray:grpFilter];
// Expression for counting elements
NSExpressionDescription *countExprDesc = [[NSExpressionDescription alloc] init];
[countExprDesc setExpression:[NSExpression expressionWithFormat:#"name.#count"]];
[countExprDesc setExpressionResultType:NSInteger64AttributeType];
[countExprDesc setName:#"count"];
// Expression for grouping JUST by the group's name
NSExpressionDescription *grpNameExprDesc = [[NSExpressionDescription alloc] init];
[grpNameExprDesc setExpression:[NSExpression expressionWithFormat:#"grp.name"]];
[grpNameExprDesc setExpressionResultType:NSStringAttributeType];
[grpNameExprDesc setName:#"grpName"];
// THIS COULD JUST BE an NSPropertyDescription if you want the WHOLE "grp":
NSPropertyDescription *grpPropDesc = [[[[self.moModel entitiesByName] objectForKey:#"RGE"] propertiesByName] objectForKey:#"grp"];
// Create data fetching and set its properties
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"RGE"];
[request setResultType:NSDictionaryResultType];
[request setPropertiesToGroupBy:[NSArray arrayWithObjects: grpNameExprDesc, nil]];
[request setPropertiesToFetch:[NSArray arrayWithObjects:grpNameExprDesc, countExprDesc, nil]];
[request setPredicate:filterQuery];
NSArray *results = [self.moContext executeFetchRequest:request error:&error];
NSLog(#"results = %#",results);
I had a similar problem and I solved it using the "option 2" approach: Grouping three entities with many-to-many relationships
Regards.
Pedro Ventura.
I fetched all the groups, and for the display I did the following:
Group *gr = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSSet *filtered = [NSSet set];
if ([gr.name isEqualToString:#"GroupA"]) {
filtered = [gr.elements filteredSetUsingPredicate:
[NSPredicate predicateWithFormat:#"any groups.name == %#", #"GroupB"]];
}
else if ([gr.name isEqualToString:#"GroupB"]) {
filtered = [gr.elements filteredSetUsingPredicate:
[NSPredicate predicateWithFormat:#"any groups.name == %#", #"GroupA"]];
}
else {
filtered = [gr.elements filteredSetUsingPredicate:
[NSPredicate predicateWithFormat:
#"any groups.name == %# && any groups.name == %#",
#"GroupA", #"GroupB"]];
}
cell.textLabel.text =
[NSString stringWithFormat:#"%# has %d elements.",
gr.name, filtered.count] ;
This gives the correct counts as in your example. However, if there is an excluded group, it would still be listed with 0 elements.

Multiple fetch requests with complex predicates

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.

Number of elements in NSArray where (....)

I am used to working in C# using the linq extensions (list.select(...).where(...) ext), and I was wondering if there was some way of doing the same sort of thing in Objective-C. This would save me from building a number of rather complicated queries using Core Data, which is great for some things, but perhaps not the best for complex queries (or maybe I'm just uninformed).
Is there some kind of equivalent for linq in Objective-C/Core Data?
EDIT: More specifically, I would like to count the number of elements that fit some criteria. Say my model has a field called date. I am trying to select the distinct dates, and then calculate how many of each date there are. In SQL this would be like a group by, and a COUNT aggregate.
Your question goes from very general ("linq equivalent?") to very specific (computing count by date). I'll just answer your specific question.
Unfortunately, NSArray doesn't have a built-in map or select method, but it does offer NSCountedSet, which will compute what you want:
NSCountedSet *dateSet = [NSCountedSet set];
for (id thing in array) {
[dateSet addObject:[thing date]];
}
for (NSDate *date in dateSet) {
NSLog(#"There are %d instances of date %#", [dateSet countForObject:date], date);
}
Change predicate , and "Date" keys with your props
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"child" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:#"Date"]];
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"(start <= %# ) and (completion < 100)",sda ];
[fetchRequest setPredicate:predicate];
int c = [self.managedObjectContext countForFetchRequest:fetchRequest error:nil];
Here was something posted with comes close
filtering NSArray into a new NSArray in objective-c
Anyway AFAIK you don't have some sort of linq in Objective-C but you have. Arrays and Blocks. And Blocks are functions. So you can really filter on anything in there.
Of course, Cora Data has many functions to make complex queries:
In example to get sum of elements, you have two major ways:
first - get your data to NSSet or NSArray and use #sum operator:
//assume that `childs` are NSArray of your child entities and ammount is attribute to sum, and has attributes start (date) and completion (integer)
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"child" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate =[NSPredicate predicateWithFormat:#"(start <= %# ) and (completion < 100)", dzis];
[fetchRequest setPredicate:predicate];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
NSError *error = nil;
if ([afetchedResultsController performFetch:&error]) {
NSArray *childs = afetchedResultsController.fetchedObjects;
NSNumber *sum=[childs valueForKeyPath:"#sum.ammount"];
}
second is using specific fetch for specific value with added NSExpressionDescription with a sum. This way is harder but better for larger db's
suppose if you have an array of your model objects, you could that with the following statement,
NSArray *distintDatesArray = [array valueForKeyPath:#"#distinctUnionOfObjects.date"];
for ( NSDate *date in distintDatesArray)
{
NSLog (#"Date :%# ,count : %d",date,[[array filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"date = %#",date]] count]);
}
This will have same effect as the group by query.