Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core Data pattern: how to efficiently update local info with changes from network?

I have some inefficiency in my app that I'd like to understand and fix.

My algorithm is:

fetch object collection from network
for each object:
  if (corresponding locally stored object not found): -- A
    create object
    if (a nested related object locally not found): -- B
      create a related object

I am doing the checking on lines A and B by creating a predicate query with the relevant object's key that's part of my schema. I see that both A (always) and B (if execution branched into that part) generate a SQL select like:

2010-02-05 01:57:51.092 app[393:207] CoreData: sql: SELECT <a bunch of fields> FROM ZTABLE1 t0 WHERE  t0.ZID = ? 
2010-02-05 01:57:51.097 app[393:207] CoreData: annotation: sql connection fetch time: 0.0046s
2010-02-05 01:57:51.100 app[393:207] CoreData: annotation: total fetch execution time: 0.0074s for 0 rows.
2010-02-05 01:57:51.125 app[393:207] CoreData: sql: SELECT <a bunch of fields> FROM ZTABLE2 t0 WHERE  t0.ZID = ? 
2010-02-05 01:57:51.129 app[393:207] CoreData: annotation: sql connection fetch time: 0.0040s
2010-02-05 01:57:51.132 app[393:207] CoreData: annotation: total fetch execution time: 0.0071s for 0 rows.

0.0071s for a query is fine on a 3GS device, but if you add 100 of these up, you just got a 700ms blocker.

In my code, I'm using a helper to do these fetches:

- (MyObject *) myObjectById:(NSNumber *)myObjectId {
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    [fetchRequest setEntity:[self objectEntity]]; // my entity cache    
    [fetchRequest setPredicate:[self objectPredicateById:objectId]]; // predicate cache    
    NSError *error = nil;
    NSArray *fetchedObjects = [moc executeFetchRequest:fetchRequest error:&error];
    if ([fetchedObjects count] == 1) {
        [fetchRequest release];
        return [fetchedObjects objectAtIndex:0];
    }
    [fetchRequest release];
    return nil;
}

MyObject *obj = [self myObjectById];
if (!obj) {
   // [NSEntityDescription insertNewObjectForEntityForName: ... etc
}

I feel this is wrong and I should do the check some other way. It should only hit the database once and should come from memory thereafter, right? (The SQL gets executed even for objects that I know for sure exist locally and should have been loaded to memory with previous queries.) But, if I only have myObjectId from an external source, this is the best I could think of.

So, perhaps the question is: if I have myObjectId (a Core Data int64 property on MyObject), how should I correctly check if the relevant local object exists in CD store or not? Preload the whole set of possible matches and then predicate a local array?

(One possible solution is moving this to a background thread. This would be fine, except that when I get the changes from the thread and do [moc mergeChangesFromContextDidSaveNotification:aNotification]; (getting changed objects from background thread by way of notification), this still blocks.)

like image 685
Jaanus Avatar asked Feb 05 '10 17:02

Jaanus


People also ask

What is relationship in Core Data?

Inverse relationships enable Core Data to propagate change in both directions when an instance of either the source or destination type changes. Every relationship must have an inverse. When creating relationships in the Graph editor, you add inverse relationships between entities in a single step.

What is fetched property in Core Data?

Fetched Properties in Core Data are properties that return an array value from a predicate. A fetched property predicate is a Core Data query that evaluates to an array of results.


2 Answers

You could probably take a lesson from email clients.

They work by first querying the server for a list of message IDs. Once the client has that list, it then compares against it's local data store to see if anything is different.

If there is a difference it takes one of a couple of actions. 1. If it exists on client, but not server AND we are IMAP, then delete locally. 2. If it exists on server, but not client, then download the rest of the message.

In your case, first query for all of the ids. Then send a follow up query to grab all of the data for the ones you don't already have.

If you have a situation where the record might exist locally, but might have been updated since the last download on the server, then your query should include the last updated date.

like image 30
NotMe Avatar answered Sep 30 '22 10:09

NotMe


Read "Implementing Find-or-Create Efficiently" in Core Data Programming Guide.

Basically you need to create an array of IDs or properties like names, or anything you have from the managed object entity.

Then you need to create a predicate that will filter the managed objects using this array.

[fetchRequest setPredicate:[NSPredicate predicateWithFormat: @"(objectID IN %@)", objectIDs]];

Of course "objectIDs" could be anything that you can use to identify. It doesn't have to be the NSManagedObjectID.

Then you could do one fetch request, and iterate the resulting fetched objects, to find duplicates. Add a new one if it doesn't exist.

like image 101
Jesse Armand Avatar answered Sep 30 '22 12:09

Jesse Armand