6

For the life of me I can not seem to get this to work.

Assume our entity is an managed object with a status field and an order field.

How would I go about getting all orderedEntries having more than one order that are the same?

Please no answers telling me to just do a subquery with @count in the main predicate, since I know of that solution, the point of this post is to understand how to use the having predicate in core data, which would probably be faster than a subquery anyways. (unless you explain why I can not use a having clause)

The following code would return an array of dictionaries with the number of orders per order number. What I want is to be able to add a having clause to restrict my request to only return the dictionaries representing objects of those orders that have a count greater than 1.

Here is the code so far and my attempts at a having predicate:

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"OrderedEntry"
                                          inManagedObjectContext:context];
[fetchRequest setEntity:entity];
[fetchRequest setResultType:NSDictionaryResultType];

NSPredicate *predicate = [NSPredicate predicateWithFormat:
                 @"(status == %@)",[NSNumber numberWithInt:EntryStatusAlive]];


[fetchRequest setPredicate:predicate];


NSExpression *keyPathExpression = [NSExpression expressionForKeyPath: @"order"]; // Does not really matter
NSExpression *maxExpression = [NSExpression expressionForFunction: @"count:"
                                                        arguments: [NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName: @"orderCount"];
[expressionDescription setExpression: maxExpression]; 
[expressionDescription setExpressionResultType: NSInteger32AttributeType];

[fetchRequest setPropertiesToFetch: [NSArray arrayWithObjects:expressionDescription,@"order",nil]];


[fetchRequest setPropertiesToGroupBy:[NSArray arrayWithObjects:@"order",nil]];
//[fetchRequest setHavingPredicate:[NSPredicate predicateWithFormat:@"@count > 1"]];
//[fetchRequest setHavingPredicate:[NSComparisonPredicate predicateWithLeftExpression:maxExpression rightExpression:[NSExpression expressionForConstantValue:[NSNumber numberWithInteger:1]] modifier:NSDirectPredicateModifier type:NSGreaterThanPredicateOperatorType options:NSCaseInsensitivePredicateOption]];

NSError *error;

NSArray * array = [context executeFetchRequest:fetchRequest error:&error];
thewormsterror
  • 1,608
  • 14
  • 27
  • 1
    Did you ever get an answer on this? I have found many useless references to 'setHavingPredicate' that provide no indication of what the format of this predicate needs to be. No working examples, and nothing that I have tried has worked... – Christopher King Jan 17 '13 at 19:25
  • 2
    @ChristopherKing Nope not at all, I basically gave up, there is NO information on the web and the apple web forums are not more helpful. I really believe no one uses this, as it's very easy to construct the same functionality of the Having predicate just by doing a normal fetch and then shifting through the data with a filter or manually with a loop. We want to know because there's always the 'right way' to do something, especially if the api is provided. – thewormsterror Jan 21 '13 at 15:08

3 Answers3

1

I ended up going with this for anyone interested

-(BOOL)ordersAreSaneOnDay:(NSNumber*)dayNumber forUser:(User*)user inContext:(NSManagedObjectContext*)context {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"BasicEntry"
                                          inManagedObjectContext:context];
[fetchRequest setEntity:entity];
[fetchRequest setResultType:NSDictionaryResultType];

NSPredicate *predicate = [NSPredicate predicateWithFormat:
                 @"(status == %@) && ((type != %@) && (type != %@) && (dayNumber == %@))  && ((user == NIL) || (user == %@))",[NSNumber numberWithInt:EntryStatusAlive],[NSNumber numberWithInt:EntryTypeTask],[NSNumber numberWithInt:EntryTypeCompletedTask],dayNumber,user];


[fetchRequest setPredicate:predicate];


NSExpression *keyPathExpression = [NSExpression expressionForKeyPath: @"order"]; // Does not really matter
NSExpression *maxExpression = [NSExpression expressionForFunction: @"count:"
                                                        arguments: [NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName: @"orderCount"];
[expressionDescription setExpression: maxExpression]; 
[expressionDescription setExpressionResultType: NSInteger32AttributeType];

[fetchRequest setPropertiesToFetch: [NSArray arrayWithObjects:expressionDescription,@"order",nil]];
[expressionDescription release];

[fetchRequest setPropertiesToGroupBy:[NSArray arrayWithObjects:@"order",nil]];
//[fetchRequest setHavingPredicate:[NSPredicate predicateWithFormat:@"self.order.@count > 1"]];
//[fetchRequest setHavingPredicate:[NSComparisonPredicate predicateWithLeftExpression:maxExpression rightExpression:[NSExpression expressionForConstantValue:[NSNumber numberWithInteger:1]] modifier:NSDirectPredicateModifier type:NSGreaterThanPredicateOperatorType options:NSCaseInsensitivePredicateOption]];

NSError *error;

NSArray * array = [context executeFetchRequest:fetchRequest error:&error];
array = [array filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"orderCount > 1"]];
//NSLog(@"it worked %@",array);
[fetchRequest release];

if ([array count]) return FALSE;
return TRUE;

}

thewormsterror
  • 1,608
  • 14
  • 27
1

I've got this working using the following:

[fetchRequest setHavingPredicate:[NSPredicate predicateWithFormat:@"$orderCount > 1"]];

Use the name of your expressionDecription as variable $orderCount. Alternatively you can use

NSExpression *countExpression = [NSExpression expressionForVariable:@"orderCount"];
[fetchRequest setHavingPredicate:[NSPredicate predicateWithFormat:@"%@ > 1", countExpression]];
Eerko
  • 136
  • 2
  • 6
0

Firstly, whenever I try something analogous to what you're doing, I get an error that you can't pass a to-many relationship to setPropertiesToFetch:. The NSFetchRequest documentation backs this up: "The property descriptions may represent attributes, to-one relationships, or expressions." So that's problem #1.

Problem #2 is that it appears that you can't group by a to-many relationship either (this isn't made clear in the documentation, but you get the same error and it also makes sense).

Remove "order" from the properties to fetch. Group by an attribute. Modify your main predicate to only include those attributes you group by (or just remove it). Specify "order" in your having predicate.

[fetchRequest setPropertiesToGroupBy: @[ @"???" ]];
[fetchRequest setHavingPredicate: [NSPredicate predicateWithFormat: @"self.order.@count > 1"]];

Now you'll see the request will work, but the results probably weren't what you were expecting:

 - NSArray
     - NSDictionary { status = @"alive", orderCount = "4" }
     - NSDictionary { status = @"alive", orderCount = "9" }
     - NSDictionary { status = @"alive", orderCount = "2" }
     - etc...

NSDictionaryResultType doesn't actually give you anything to identify those objects by - it just gives you the values.

So your next step is to get back IDs for those OrderedEntry objects. The trick is to include an expression which will give you back the NSManagedObjectID as a key in each dictionary.

I don't know if this will actually give you improved performance at all (over just AND-ing it in to the main predicate). In my experience, one of the best things you can do to improve fetching performance is to create singleton NSPredicates and use substitution variables to set up each fetch. Predicate parsing can be expensive.

Dictionary result types can be a pain. Usually just constructing a good predicate is better. I tend only to use them for expressions (i.e. performing statistic-type calculations on the graph which return a value). If you look at all the restrictions around properties to fetch and group by and the predicate restrictions, this seems to be what Apple intend it for. I'm not even sure it's possible to do what you want to do by grouping and using a having predicate - for example, what are you going to group by? If by status (which you need to group by to include in your predicate), won't that collapse all the OrderEntries and you won't get the separate objectIDs and orderCounts? It's best to stick to the main predicate for this, not grouping and having predicates.

Community
  • 1
  • 1
karwag
  • 2,641
  • 1
  • 15
  • 12
  • I don't understand what you are saying, setPropertiesToFetch: what am I doing wrong here? I have an attribute "order" and expressionDescription which is an expression. – thewormsterror Jan 22 '13 at 14:56
  • for problem number 2 my order is an attribute, not a relationship, so I can group by it from my understanding. – thewormsterror Jan 22 '13 at 14:58
  • How do you expect to count an attribute, then? order has to be a to-many relationship for your example above to make any sense at all. – karwag Jan 22 '13 at 22:17
  • 1
    you can count the occurrences of objects having an attribute at a certain value, in my example I am trying to know how many times I have each order, for example order = 1 -> orderCount = 2, order = 3 -> orderCount = 1. – thewormsterror Jan 23 '13 at 12:47
  • What I am trying to do with the having predicate is only return order numbers with a count higher than 1, I already can get the orderCount. This is easy in sql with a having predicate, but I don't know the syntax here. – thewormsterror Jan 23 '13 at 12:48
  • I'm not sure if CoreData will do that. Just try and avoid treating it like a SQL frontend - it's not designed for that and it won't be easy (as you're seeing). Having predicates do work though - I tested one of the format in my answer and it seemed to do the job. – karwag Jan 24 '13 at 01:13
  • In your example where is orderCount coming from? – thewormsterror Jan 24 '13 at 02:25