views:

28

answers:

2

I am writing an app that connects to a database to fetch data. Since the fetching is expensive and the data is generally unchanging, I'm using CoreData to cache the results so that I can do fast, local queries.

From the database, for each type, there is a string property that is guaranteed to be unique. In fact, there is a URI scheme for the database which is a unique address for each item.

The URL scheme is very basic along the lines of:

ngaobject://<server_license_id>/<type>/<identifier>

I'd like to be able to use this in CoreData as well. I've made a method to fetch a single item from the CoreData store:

-(NSFetchRequest*)fetchRequestForType:(NSString*)typeName identifier:(NSString*)identifier
{
    NSFetchRequest * fetchRequest = [self fetchRequestForType:typeName];

    [fetchRequest setFetchLimit:1];

    NSString * identifierProperty = [self identifierPropertyNameForObjectType:typeName];
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"%K == %@", identifierProperty, identifier];
    [fetchRequest setPredicate:predicate];

    return fetchRequest;
}

-(NGAObject*)objectWithType:(NSString*)typeName
identifier:(NSString*)identifier
    {
        // First try to retrieve it from the cache

    NSAssert1( (identifier != nil), @"Request to create nil-name object of type %@", typeName );

    NSFetchRequest * fetchRequest = [self fetchRequestForType:typeName identifier:identifier];

    if ( !fetchRequest )
        return nil;

    NSError * error = nil;
    NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error];

    if ( !fetchResults )
    {
        NSLog(@"%@", error);
        [NSApp presentError:error];
        return nil;
    }

    if ( [fetchResults count] )
        return [fetchResults objectAtIndex:0];

    return nil;
}

When I retrieve an item from the server, I want to first get a reference to it in the cache and if it's there, update it. If it's not, create a new one.

Since I'm getting back thousands of objects from the server, performing a fetch for a single object for which I know a unique ID brings my machine to a crawl.

Instead, what I'm doing is pre-loading all the objects of a type, then creating a dictionary of identifiers->object, then processing the thousands of objects for that type by running it through the dictionary. This works fine, but is awkward.

Could I not write a method that takes the type/identifier combo and get a single object from CoreData without having to execute a lengthy fetch request?

It seems there is a solution if I can get CoreData to use my own URI specification. I could then call -(NSManagedObjectID*)managedObjectIDForURIRepresentation:(NSURL*)url on the persistent store coordinator.

So, the question is, how can I get CoreData to use my URI scheme? How can I make CoreData use my own unique identifiers?

A: 

Are all the fields which you are using for unique-ID lookup marked as "Indexed" in the CoreData designer? If that has been done then the CoreData fetches shouldn't be lengthy ...

glenc
No, it is not. That's a good suggestion and I'll give that a try. Just for the sake of the question though, is there a way to accomplish what I've laid out?
Michael Bishop
Not sure, the other question here seems to suggest not. Generally speaking, if you find yourself trying to hack something in the innards of how CoreData works, then you are probably doing something wrong. In this case, I'm pretty sure the best solution is just to use table indexing which would be the suggested CoreData way to do this (actually this would apply to any RDBMS)
glenc
+1  A: 

You can't make Core Data use a custom URI scheme. The URI scheme is hardcoded into Core Data such that the URI can be decoded to locate particular data in a particular store in a particular apps on a particular piece of hardware. If the URI was customizable, that system would break down.

Fetching object singularly is what is killing you. Instead you need to batch fetch all objects whose customID matches those provided by the server. The easiest way to that is to use the IN predicate operator:

NSArray *customIDs=//... array of customIDs provided by server
NSPredicate *p;
p=[NSPredicate predicateWithFormat: @"customIdAtrribute IN %@", customIDs];

This will return all existing objects that you can ignore.

Alternatively, you could

  1. Do a fetch on just the customID property by setting the fetch's propertiesToFetch to the customID attribute.
  2. Set the fetch result type to dictionary.
  3. Use the above predicate.
  4. You will get an array of one key dictionaries returned with the customID as each value.
  5. Convert the dictionary to an array of values e.g cachedIDs
  6. Convert customIDs above to a mutable array.
  7. Filter the customIDs array using the predicate, @"NOT (SELF IN %@)", cachedIDs"
  8. The filtered customIDs array will now only contain the customID values NOT cached in Core Data.
  9. You can create managed objects for only the new ids.

(This is how you use a filter predicate if you are unfamilar with it.)

NSMutableArray *f=[NSMutableArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5",@"6",nil];
NSArray *g=[NSArray arrayWithObjects:@"5",@"6",nil];
[f filterUsingPredicate:[NSPredicate predicateWithFormat:@"NOT (SELF IN %@)",g]];
NSLog(@"f=%@",f);

...which outputs:

f=(
    1,
    2,
    3,
    4
)
TechZen