4

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.. :-)

MichiZH
  • 5,587
  • 12
  • 41
  • 81
  • 1
    I am afraid you can't (because `NSDictionaryResultType` implies `setIncludesPendingChanges:NO`). You have to save the context explicitly after making changes. – Martin R Dec 30 '13 at 16:39
  • Can I save the context just in the viewWillDisappear method of the viewController I made changes and then it should work? Or can I loop through the relationsship NSSet as in my edit? – MichiZH Dec 30 '13 at 16:42
  • 1
    You can save the context whenever you want, the only problem is that saving often makes your app slow. - The advantage of NSExpressionDescription vs an explicit loop is that the NSExpressionDescription is executed on the SQLite level, without fetching all objects into memory (firing the faults). But I have no advice which is better in general, you probably have to do some performance testing. – Martin R Dec 30 '13 at 16:47
  • But if I save in the viewWill or DidDisappear, my view controller still fetches the old values. Is there any possibility to refetch only after the save is complete? maybe in a block? – MichiZH Dec 30 '13 at 16:50
  • I have no idea, but saving is a synchronous process. - Btw, is this question about a fetched results controller or about `executeFetch`? – Martin R Dec 30 '13 at 16:54
  • synchronous means here that first saving is completed and only thereafter the following lines of code executed? Oh it's about a fetch, in the first code snippet I've posted. – MichiZH Dec 30 '13 at 16:56
  • 1
    Perfectly agree with @MartinR. Anyway, if you don't want to save, the loop is the right choice. Well, you should do some measures as Martin suggested. Synchronous means that you need to wait the save ends before call the method you are interested in. So, do a save and then call the update method. – Lorenzo B Dec 30 '13 at 16:58
  • 1
    @MichiZH: Yes. You can set the launch argument `-com.apple.CoreData.SQLDebug 1` (or `3`) to get debug output about all SQLite commands as they are executed. – Martin R Dec 30 '13 at 16:58
  • Thx alot, didn't know that. I'll try that now. My loop would be nice and fast, however the if clause isn't really working. Sometimes all entries are used for a calculation, sometimes only the correct one, sometimes some of them..even though both data types which are compared are dates... – MichiZH Dec 30 '13 at 17:05
  • 1
    there is no operator overloading in Objective-C, use `compare:` method of `NSDate` (to compare dates of that type) – Dan Shelly Dec 30 '13 at 17:41
  • Please put the last edit as an answer and mark it as correct. It's ok to answer to own questions. You should also try @Mundi's one since a very valid one. – Lorenzo B Dec 31 '13 at 02:04

2 Answers2

4

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"];
Mundi
  • 79,884
  • 17
  • 117
  • 140
  • awesome, didn't know that you can predicate a set. Thx a lot mundi – MichiZH Dec 31 '13 at 07:58
  • Still one more question, since I've checked my categories on several managed objects where I always make a fetch for max position, or sum of stuff etc. Since these valueForKeyPath methods are only one-liners and use already stuff which has been fetched, they must be a lot faster than always fetching right? – MichiZH Dec 31 '13 at 09:13
  • And in my "main" view controller (my first fetch, the main category) I'm also using aggregate functions. Can I use them somehow on the fetched results set? – MichiZH Dec 31 '13 at 09:21
  • Yes, you are right on all points: usage, speed etc. All is very well documented in the [Collection Operators section of the Key Value Coding Guide](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/CollectionOperators.html#//apple_ref/doc/uid/20002176-BAJEAIEE). – Mundi Dec 31 '13 at 16:10
1

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.

Motti Shneor
  • 2,095
  • 1
  • 18
  • 24