It's a long standing problem when using Core Data to-many-relationships that it is very hard to sort a fetch request using NSSortDescriptor
on a Parent
entity based on the number of children
are in a one-to-many relationship to a Child
entity. This is especially useful in combination with a NSFetchedResultsController
. Typically initializing the sort descriptor as:
NSSortDescriptor *sortByNumberOfChildren = [[NSSortDescriptor alloc] initWithKey:@"children.@count" ascending:NO];
results in an exception'Keypath containing KVC aggregate where there shouldn't be one; failed to handle children.@count
On iOS 6.1, I discovered a fix by adding the KVO accessor -countOf<Key>
as an attribute to my managed object model as an integer type. I did NOT implement anything for this attribute in my NSManagedObject
subclass, as all the magic seems to happen under the hood. (see https://stackoverflow.com/a/15546371/2042527).
However, this does not work on iOS 6.0. Here I found that adding the following method to your NSManagedObject
subclass resolves the problem:
- (NSUInteger)countOfChildren{
return [self.children count];
}
Adding both does not fix the problem in both SDKs. On the contrary, it breaks the fix.
Does anyone have a clue why this is happening and why there is a difference between both, eventhough there is no mention of changes to Core Data or Foundation between iOS 6.0 and iOS 6.1.
I think that by saying "Keypath containing KVC aggregate where there shouldn't be one; failed to handle children.@count" Core Data wants to tell you that it does not support this kind of sort descriptor. This is very likely because when the backing SQLite store receives your fetch request it has to generate SQL that does what the fetch request describes. The case of "children.@count" is actually more complex under the hood than one might think.
The "fix" with overriding -countOfChildren is not really a fix. Let's assume for a second that this fixes the problem then -countOfChilden would be called on every Parent. When you first access self.children then Core Data needs to execute a SQL query that determines (at least) the primary keys of the children, create NSManagedObjectIDs, NSManagedObjects and return the result. If this worked then you would see very bad performance.
There are several solutions to your problem.
1. Store the child count in a persistent attribute
Simply add a attribute (name: cachedCountOfChildren, type: Integer 64 bit) to your Parent entity. In your controller layer (NOT IN YOUR MODEL LAYER) increment cachedCountOfChildren by 1 every time you assign a child to a parent and decrement cachedCountOfChildren every time you remove a child from a parent. Then you use cachedCountOfChildren in your sort descriptor key. This will have great performance.
2. Use dictionary results
Set the resultType of your NSFetchRequest to NSDictionaryResultType. This will cause -executeFetchRequest:error: to return NSDictionaries instead of NSManagedObjects. A NSFetchRequest with a NSDictionaryResultType can do different things. For example you can use setPropertiesToGroupBy and NSExpression (...). Please look at the WWDC session "Using iCloud with Core Data (2012)" (starting at slide 122) for reference. Basically they show you how to construct a request that will return an array which contains dictionaries that have this structure:
(
{
countOfChildren = 1;
parentName = "hello";
},
{
countOfChildren = 134;
parentName = "dsdsd";
},
{
countOfChildren = 2;
parentName = "sdd";
}
)
As you can see you will get a unsorted result back. But sorting this array by countOfChildren can be done in memory very efficiently. The generated SQL by Core Data will also be very efficient in this case and you will be able to specify exactly which attributes the dictionaries should contain. So the result should also be very memory efficient. This solution has the advantage that you do not have to keep track of the countOfChildren.
You have to decide which solution is best for yourself depending your your situation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With