iOS FetchRequest on aggregate functions: How to in

2019-07-07 04:31发布

I've finally at least narrowed down this problem. I'm computing some aggregate functions (as in this example the sum) of expenditures. If I change some expenditures, this aggregate fetch doesn't refresh immediately but only after a while (probably after the changes have been saved to the database). I've found this part in the doc:

- (void)setIncludesPendingChanges:(BOOL)yesNo

As per the documentation

A value of YES is not supported in conjunction with the result type NSDictionaryResultType, including calculation of aggregate results (such as max and min). For dictionaries, the array returned from the fetch reflects the current state in the persistent store, and does not take into account any pending changes, insertions, or deletions in the context. If you need to take pending changes into account for some simple aggregations like max and min, you can instead use a normal fetch request, sorted on the attribute you want, with a fetch limit of 1.

Ok how can I still include pending changes? I'm using a NSFetchedResultsController to display my data. And here is my aggregate function:

- (NSNumber *)getExpendituresAmountForCostPeriod:(CostPeriod)costPeriod
{
    NSLog(@"getExpenditures_Start");
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Expenditures"];
    [fetchRequest setResultType:NSDictionaryResultType];

    NSDate *startDate = [NSDate startDateForCostPeriod:[self getBiggestCostPeriod]];
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"forSpendingCategory = %@ AND date >= %@", self, startDate];        

    //Define what we want
    NSExpression *keyPathExpression = [NSExpression expressionForKeyPath: @"amount"];
    NSExpression *sumExpression = [NSExpression expressionForFunction: @"sum:"
                                                            arguments: [NSArray arrayWithObject:keyPathExpression]];

    //Defining the result type (name etc.)
    NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
    [expressionDescription setName: @"totalExpenditures"];
    [expressionDescription setExpression: sumExpression];
    [expressionDescription setExpressionResultType: NSDoubleAttributeType];

    // Set the request's properties to fetch just the property represented by the expressions.
    [fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
    NSLog(@"%@", self.managedObjectContext);

    // Execute the fetch.
    NSError *error = nil;
    NSArray *objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
    if (objects == nil) {
        return [NSNumber numberWithDouble:0];
    } else {
        if ([objects count] > 0) {
            return [[objects objectAtIndex:0] valueForKey:@"totalExpenditures"];
        } else {
            return [NSNumber numberWithDouble:0];
        }
    }
}

EDIT: *Is a loop through the NSSet possible and fast enough?*

- (NSNumber *)getExpendituresAmountForCostPeriod:(CostPeriod)costPeriod
{
    NSDate *startDate = [NSDate startDateForCostPeriod:[self getBiggestCostPeriod]];
    double total = 0;

    for(Expenditures *expenditure in self.hasExpenditures){
        if(expenditure.date >= startDate){
            total = total + [expenditure.amount doubleValue];
        }
    }

    return [NSNumber numberWithDouble:total];
}

EDIT FINALLY WITH ANSWER Thx to all of you I've finally found the problem in the loop. This works very fast and nice:

- (NSNumber *)getExpendituresAmountForCostPeriod:(CostPeriod)costPeriod
{
    NSDate *startDate = [NSDate startDateForCostPeriod:[self getBiggestCostPeriod]];
    double total = 0;

    for(Expenditures *expenditure in self.hasExpenditures){
        if([expenditure.date compare: startDate] == NSOrderedDescending){
            total = total + [expenditure.amount doubleValue];
        }
    }

    return [NSNumber numberWithDouble:total];
}

Called from controllerDidChangeContent.

That's enough for today.. :-)

2条回答
一纸荒年 Trace。
2楼-- · 2019-07-07 04:52

I am not at all sure that using the predicate to filter out a subset is any faster from the primitive loop suggested beforehand.

It is a more concise and beautiful code, but by no means faster. Here are a few reasons (overheads) I can see immediately.

  1. Creating and compiling a predicate from a text format takes both time and memory (several allocations)
  2. Filtering the self.hasExpenditures using predicate again allocates and initiates a new NSSet (shortSet) and populates it with (retained) references to the matching expenditures (withn the date range). For that it must scan the self.expenditures one by one in a loop.
  3. Then the last total calculations, again loops over the subset summing over amount, and allocating the final NSNumber object.

In the loop version --- there is no new allocation, no retaining nor releasing of anything, and just one pass over the self.expenditures set.

All in all, my point is, the second implementation will NEED to do AT LEAST the contents of that loop anyway, plus some more overheads.

And a last point: for id in collection can run concurrently using GCD on several items, hence it is quite fast.

I think you should at least try to match these alternatives via extensive performance test.

查看更多
倾城 Initia
3楼-- · 2019-07-07 04:53

Your solution is OK, but you can still speed things up and produce shorter code by first shortening the set and then avoiding the loop by taking advantage of KVC:

NSSet *shortSet = [self.hasExpenditures filteredSetUsingPredicate:
  [NSPredicate predicateWithFormat:@"date > %@", startDate]];
NSNumber *total = [shortSet valueForKeyPath:@"@sum.amount"];
查看更多
登录 后发表回答