1

I have a large list of Users in an NSDictionary that has a structure like this:

        97 =             {
            birthday = "";
            gender = Unspecified;
            image =                 {
                status = Prepared;
                type = Image;
            };
            "name_display" = "Facebook User";
            "name_first" = Facebook;
            "name_last" = User;
            type = Contact;
            "user_id" = 97;
        };
        98 =             {
            birthday = "";
            gender = Unspecified;
            image =                 {
                status = Prepared;
                type = Image;
            };
            "name_display" = "Facebook User";
            "name_first" = Facebook;
            "name_last" = User;
            type = Contact;
            "user_id" = 98
         }

I want to input this data into Core Data. First I must check if the user already exists in core data. If so, update that user. Otherwise create a new user. The way I am doing it works, but it is extremely slow. Here's my code:

NSDictionary *users = [responseData objectForKey:@"users"];
if (users) {
    for (id userKey in [users allKeys]) {
        NSDictionary *contactDictionary = [users objectForKey:userKey];
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"userID == %@", userKey];
        NSUInteger count = [CoreDataHelper countForEntity:@"Contact" withPredicate:predicate andContext:[FSAppDelegate managedObjectContext]];
        if (count > 0) {
            NSMutableArray *existingContactArray = [CoreDataHelper searchObjectsForEntity:@"Contact" withPredicate:predicate andSortKey:nil andSortAscending:NO andContext:[FSAppDelegate managedObjectContext]];
            Contact *existingContact = [existingContactArray objectAtIndex:0];
            [CoreDataHelper updateContactWithDictionary:contactDictionary forContactObject:existingContact];
        }
        else {
            Contact *newContact = (Contact*)[NSEntityDescription insertNewObjectForEntityForName:@"Contact" inManagedObjectContext:[FSAppDelegate managedObjectContext]];
            [CoreDataHelper updateContactWithDictionary:contactDictionary forContactObject:newContact];
        }
    }

    NSError *error;
    if (![[FSAppDelegate managedObjectContext] save:&error]) {
        // Handle the error.
        NSLog(@"error saving to db - fsrequest class.");
    }
}

And here is my method to update the contact

+(BOOL)updateContactWithDictionary:(NSDictionary*)changedContact forContactObject:(Contact*)contact {
NSString *bday = [changedContact valueForKey:@"birthday"];
NSString *gender = [changedContact valueForKey:@"gender"];
NSString *nameDisplay = [changedContact valueForKey:@"name_display"];
NSString *nameFirst = [changedContact valueForKey:@"name_first"];
NSString *nameLast = [changedContact valueForKey:@"name_last"];
NSString *type = [changedContact valueForKey:@"type"];
NSString *userID = [NSString stringWithFormat:@"%@",[changedContact valueForKey:@"user_id"]];
NSString *imageStatus = [[changedContact objectForKey:@"image"]objectForKey:@"status"];
NSString *imageType = [[changedContact objectForKey:@"image"]objectForKey:@"type"];
NSString *imageURL = [[changedContact objectForKey:@"image"]objectForKey:@"url"];
NSString *imageThumb = [[changedContact objectForKey:@"image"]objectForKey:@"url_thumb"];
NSString *locationName = [[changedContact objectForKey:@"location"]objectForKey:@"name"];

[contact setBirthday:bday];
[contact setGender:gender];
[contact setNameDisplay:nameDisplay];
[contact setNameFirst:nameFirst];
[contact setNameLast:nameLast];
[contact setType:type];
[contact setUserID:userID];
[contact setImageStatus:imageStatus];
[contact setImageType:imageType];
if (imageURL && !((NSNull *)imageURL == [NSNull null])) {
    [contact setImageURL:imageURL];
}
if (imageThumb && !((NSNull *)imageThumb == [NSNull null])) {
    [contact setImageThumbURL:imageThumb];
}
if (locationName && !((NSNull *)locationName == [NSNull null])) {
    [contact setLocationName:locationName];
}
return YES;
}

Can someone give me an example of how I would do this in a much faster way? Some people have mentioned some ideas, but I need to see it to understand. Thanks!

nicholjs
  • 1,762
  • 18
  • 30
  • Is your userID property marked as indexed in your data model? – paulbailey Jan 24 '12 at 10:45
  • no, what do you mean by that? – nicholjs Jan 24 '12 at 16:01
  • In the Data Model inspector, you can mark an attribute of an entity as indexed (in the same section as where you mark an attribute as transient or optional), which means that an index will be created in the underlying SQLite DB. That should improve your performance, since your fetches are based on that attribute. – paulbailey Jan 24 '12 at 17:22
  • Here's a question with a basic explanation: http://stackoverflow.com/questions/7935348/what-does-the-indexed-property-of-a-coredata-attribute-do – paulbailey Jan 24 '12 at 17:24

2 Answers2

2

First of all I'd move save: outside the loop. Replace:

 // save core data
        NSError *error;
        if (![[FSAppDelegate managedObjectContext] save:&error]) {
            // Handle the error.
            NSLog(@"error saving to db - fsrequest class.");
        }
    }
}

with

    }
    // save core data
    NSError *error;
    if (![[FSAppDelegate managedObjectContext] save:&error]) {
    // Handle the error.
    NSLog(@"error saving to db - fsrequest class.");
  }
}

Also, do you have some default values for imageURL, imageThumb and locationName defined in Core Data model? If no, why do you check for nulls (twice)?

Bonus:

It may be a good idea to eliminate countForEntity:withPredicate:andContext: call, like this:

NSMutableArray *existingContactArray = [CoreDataHelper searchObjectsForEntity:@"Contact" withPredicate:predicate andSortKey:nil andSortAscending:NO andContext:[FSAppDelegate managedObjectContext]];

if ([existingContactArray count] > 0)
{
    Contact *existingContact = [existingContactArray objectAtIndex:0];

    [CoreDataHelper updateContactWithDictionary:contactDictionary forContactObject:existingContact];
}
zrslv
  • 1,748
  • 1
  • 14
  • 25
  • i edited my answer again to show how i am using fast enumeration. i think it can be much faster. i check the imageURL and imageThumb for null because sometimes the server sends null, but i want to retain whatever i have there instead of it being updated to null. – nicholjs Jan 23 '12 at 16:50
  • your idea to eliminate countForEntity is a good idea, but my method count for entity does the count without actually querying all of the objects. it's faster this way. – nicholjs Jan 23 '12 at 16:56
  • If you are no longer saving after each inserted record and still experience performance problems, then I don't see how you can make it considerably faster. Make sure you are not doing anything unnecessary in the fetch routine (searchObjectsForEntity). You can also try batching save: calls (see [this answer](http://stackoverflow.com/a/4146350/793911)). – zrslv Jan 23 '12 at 17:02
1

You need to understand that a fetch request is expensive (it needs to run SQL and do I/O). I'm not sure where your CoreDataHelper comes from, but it will do some sort of NSFetchRequest which is expensive. You can construct a single NSFetchRequest which will tell you which objects already exists. That will reduce the cost from O(N) to O(1).

[request setPredicate:[NSPredicate predicateWithFormat:@"userID IN %@", allKeys]];

And, as noted above, move the saves out of the loop. But if you're updating adding a many objects, you might want to save every now and then.

Daniel Eggert
  • 6,665
  • 2
  • 25
  • 41