an0
an0

Reputation: 17530

Any way to optimize simple NSFetchRequest for single object?

I'm dong data processing in a child moc in a background queue. I need to query the database by ID so that I can differentiate updating-existing-object from creating-new-object. I found most of the time(the total processing time is about 2s for 50 items) is consumed by executeFetchRequest:error:. The NSPredicate is of the simplest form — only to match a single ID attribute(ID attribute is already indexed), and the NSFetchRequest should return one or none(ID is unique). Is there any way to optimize this kind of NSFetchRequest?

Here is my current code:

+ (User *)userWithID:(NSNumber *)ID inManagedObjectContext:(NSManagedObjectContext *)context {
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"User"];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"ID == %@", ID];
    [fetchRequest setPredicate:predicate];
    [fetchRequest setFetchBatchSize:1];

    NSError *error = nil;
    NSArray *users = [context executeFetchRequest:fetchRequest error:&error];
    if (error) {
        abort();
    }

    if ([users count] == 1) {
        return [users objectAtIndex:0];
    } else if ([users count] > 1) {
        // Sanity check.
        …
    } else {
        return nil;
    }
}

Upvotes: 1

Views: 662

Answers (2)

ChrisH
ChrisH

Reputation: 4558

To expand on my comment to the original question, it's not efficient to repeatedly perform fetch requests with Core Data when importing data.

The simplest approach, as @an0 indicated, is to perform one fetch of all the existing objects you will be checking against, and then constructing an NSDictionary containing the objects with the attribute you will be checking as keys. So sticking with the original User and userID example:

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"User"];

NSError *error = nil;

NSArray *users = [context executeFetchRequest:fetchRequest error:&error];

if (error) {
  //handle appropriately
}

NSMutableDictionary *userToIdMap = [NSMutableDictionary dictionary];

for (User *user in users){

  [userToIdMap setObject:user forKey:user.ID];

}

Now in your method that processes new data you can check the userToIdMap dictionary instead of making fetch requests.

A more sophisticated approach, suited to larger data sets, is outlined in the Core Data Programming Guide's Efficently Importing Data. Take a look at the section called 'Implementing Find-Or-Create Efficiently'. The approach suggested by Apple here misses out some code regarding how to walk the arrays you create, and my solution to that problem is in this SO question: Basic array comparison algorithm

Upvotes: 1

an0
an0

Reputation: 17530

As @ChrisH pointed out in comments under the question, doing a fetch for every ID is no good. So I changed my processing flow to this:

  1. Enumerate data the first time to extract IDs.
  2. Do a single fetch to fetch all existing users matching IDs and put them in a dictionary keyed by ID(named as existingUsers).
  3. Enumerate data the second time to do the real processing: in each iteration, either update one existing user found in existingUsers or create a new user, add it into existingUsers if it is new.

The code is almost doubled, but so is the performance. Really good tradeoff!

Upvotes: 1

Related Questions