1
votes

I'm having big problem with creating fetch request in my app that uses Core Data. I have 2 entities, Song and Genre. My models look like this: enter image description here

I need to make query that would return top 5 played artist, using playCount attribute of Song entity. In the end I need to group result by Artist.name attribute.

I.e. resulting dictionary should look something like this:

result_array('my_first_artist' => '3200', 'my_second_artist' => '12');

I know that I can't accomplish resulting array as above in Core Data, but something similar would be great.

I tried with this code:

NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:@"Artist" inManagedObjectContext:context]];
[request setPropertiesToFetch:@[@"name", @"[email protected]"]];
[request setPropertiesToGroupBy:@[@"name"]];
[request setResultType:NSDictionaryResultType];
[request setSortDescriptors:@[[[NSSortDescriptor alloc] initWithKey:@"[email protected]" ascending:NO]]];
[request setFetchLimit:5];

But it always throws exception on @"[email protected]" part. So my question is: How can I sum relationship objects using their attributes?

Any suggestion would be great.

EDIT:

Thanks to "Martin R" part of problem is solved. Using NSExpressionDescription I can make fetch, but NSSortDescriptor does not allow sorting on keys that are not attributes of NSManagedObject entity.

In case someone needs it, this is solution:

NSExpression *aggregateExpression = [NSExpression expressionForFunction:@"sum:"
                                                              arguments:@[[NSExpression expressionForKeyPath:@"songs.playCount"]]];

NSExpressionDescription *aggregateDescription = [[NSExpressionDescription alloc] init];
[aggregateDescription setName:@"count"];
[aggregateDescription setExpression:aggregateExpression];
[aggregateDescription setExpressionResultType:NSInteger32AttributeType];

[request setPropertiesToFetch:@[@"name", aggregateDescription]];
2
From your CoreData model screenshot, it doesn't look like your playCount property exists?Zack Brown
You talk about "Song" and "Genre", but the image shows "Song" and "Artist". - And please show the exact exception message.Martin R
Sorry I uploaded wrong image. In edited image you can see that Song entity has playCount property. This is my excetion message: "Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid to many relationship in setPropertiesToFetch: ([email protected])'"Josip B.
I think you have to use a NSExpressionDescription for the sum expression. I am not sure if sorting by that expression works at all.Martin R

2 Answers

1
votes

Finally I got answer.

I added new property to Genre.h

@property (nonatomic, retain) NSNumber * songsPlayCountSum;

In Genre.m I added

@dynamic songsPlayCountSum;

- (NSNumber *)songsPlayCountSum {

    return [self valueForKeyPath:@"[email protected]"];
}

Also I added new attribute called songsPlayCountSum to my XCDATAMODEL.

And this is finally code that works as I wanted:

NSExpression *aggregateExpression = [NSExpression expressionForFunction:@"sum:"
                                                              arguments:@[[NSExpression expressionForKeyPath:@"songs.playCount"]]];

NSExpressionDescription *aggregateDescription = [[NSExpressionDescription alloc] init];
[aggregateDescription setName:@"count"];
[aggregateDescription setExpression:aggregateExpression];
[aggregateDescription setExpressionResultType:NSInteger32AttributeType];


NSManagedObjectContext *context = [self getManagedObjectContext];

NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:@"Genre" inManagedObjectContext:context]];
[request setPropertiesToFetch:@[@"name", aggregateDescription]];
[request setPropertiesToGroupBy:@[@"name"]];
[request setPredicate:[NSPredicate predicateForNotEmptyAtrribute:@"name"]];
[request setResultType:NSDictionaryResultType];
[request setSortDescriptors:@[[[NSSortDescriptor alloc] initWithKey:@"songsPlayCountSum" ascending:NO]]];
[request setFetchLimit:5];

NSArray *results = [[self getManagedObjectContext] executeFetchRequest:request
                                                                 error:nil];

EDIT:

Using custom getters in NSManagedObject subclass isn't so easy to do, so I was getting wrong results when I wanted to use it. Instead add KVO to your relationship property and when Core Data makes changes to relationship NSSet just update property that you need to use for sorting. In my case it looks like this:

#pragma mark - Memory Management

- (void) dealloc {

    [self removeObserver:self
              forKeyPath:@"songs"];
}

#pragma mark - Initialization

- (id) initWithEntity:(NSEntityDescription *)entity insertIntoManagedObjectContext:(NSManagedObjectContext *)context {

    if (self = [super initWithEntity:entity insertIntoManagedObjectContext:context]) {

        [self addObserver:self
               forKeyPath:@"songs"
                  options:0
                  context:NULL];
    }

    return self;
}

#pragma mark - Observing 

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {

    if ([keyPath isEqualToString:@"songs"]) {

        self.songsPlayCountSum = [self valueForKeyPath:@"[email protected]"];
    }
}
0
votes

Perhaps you could add a transient (see Documentation) attribute songsPlayCount to the Genre (or Artist). Use it to return the playCount of all its songs. You could sort your output using this attribute.