NSSortDescriptor to sort by number of items in Core Data To-Many Relationships
Asked Answered
D

1

8

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://mcmap.net/q/880400/-core-data-sorting-by-count-in-a-to-many-relationship).

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.

Deflocculate answered 21/3, 2013 at 14:52 Comment(3)
You added this to the NSManagedObjectModel? It's hard to see how that could even compile, let alone work. The NSManagedObjectModel does not have any relationships to other classes.Amphictyon
Sorry, my bad, I meant NSManagedObject subclass also the second time around. I've corrected the mistake.Deflocculate
Not a solution to your exact problem but another view on it: How about fetching the children and counting the number of distinct parents? Maybe this post helps you.Selector
E
7

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.

Ellita answered 20/9, 2013 at 16:29 Comment(4)
Why not implement this on a NSManagedObject subclass (i.e. the model layer)?Egest
Implement what? 1 or 2?Ellita
I'm wondering why you wrote the following for 1: In your controller layer (NOT IN YOUR MODEL LAYER) increment cachedCountOfChildren by 1 every time you....Egest
Because in a perfect world your NSManagedObjects should be dead simple. This is because of many reasons (performance, complexity, ...). It is very hard to do smart things in your subclass correctly. For example you get a KVO notification for "children" when an object turns into a fault because all in-memory properties are set to nil. Not every property change is a change caused by you or the user. The context can turn objects into faults and you would change the count of children.Ellita

© 2022 - 2024 — McMap. All rights reserved.