views:

65

answers:

1

In short, I want to associate arbitrary key/value pairs with the objects of a Core Data entity, on an iPad app.

My current solution is to have a to-many relationship with another entity that represents a single pair. In my application, I have:

Entry <--->> ExtraAttribute

where ExtraAttribute has properties key and value, and the key is unique to the ExtraAttribute's Entry.

Although the code to deal with this is slightly complicated, it is acceptable. The real problem comes with sorting.

I need to sort those Entries that have a given ExtraAttribute by that attribute. Using the SQL store, it is apparently impossible for Core Data itself to sort the Entries by the value of the associated ExtraAttribute with a given key. (Frustrating, since this is possible with the other stores, and trivial in SQL itself.)

The only technique I can find is to sort the entries myself, then write a displayOrder attribute back to the store, and have Core Data sort by the displayOrder. I do that with the following class method on Entry. (This uses a some methods and global functions not shown, but hopefully you can get the gist. If not, ask and I will clarify.)

NSInteger entryComparator(id entry1, id entry2, void *key) {
    NSString *v1 = [[entry1 valueForPropertyName:key] description];
    NSString *v2 = [[entry2 valueForPropertyName:key] description];
    return [v1 localizedCompare:v2];
}

@implementation Entry

...

// Unified builtin property and extraAttribute accessor;
// expects human-readable name (since that's all ExtraAttributes have).
- (id)valueForPropertyName:(NSString *)name {
    if([[Entry humanReadablePropertyNames] containsObject:name])  {
        return [self valueForKey:
            [Entry propertyKeyForHumanReadableName:name]];
    } else {
        NSPredicate *p = [NSPredicate predicateWithFormat:
            @"key = %@", name];
        return [[[self.extraAttributes filteredSetUsingPredicate:p]
                                            anyObject] value];
    }
}

+ (void)sortByPropertyName:(NSString *)name
        inManagedObjectContext:(NSManagedObjectContext *)moc {
    BOOL ascending = [Entry propertyIsNaturallyAscending:name];
    [Entry sortWithFunction:entryComparator
        context:name ascending:ascending moc:moc];
    [[NSUserDefaults standardUserDefaults]
        setObject:name
        forKey:@"entrySortPropertyName"];
}

// Private method.
+ (void)sortWithFunction:(NSInteger (*)(id, id, void *))sortFunction
        context:(void *)context
        ascending:(BOOL)ascending
        moc:(NSManagedObjectContext *)moc {

    NSEntityDescription *entityDescription = [NSEntityDescription
        entityForName:@"Entry" inManagedObjectContext:moc];
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    [request setEntity:entityDescription];
    NSError *error;
    NSArray *allEntries = [moc executeFetchRequest:request error:&error];
    [request release];
    if (allEntries == nil) {
        showFatalErrorAlert(error);
    }

    NSArray *sortedEntries = [allEntries
        sortedArrayUsingFunction:sortFunction context:context];

    int i, di;
    if(ascending) {
        i = 0; di = 1;
    } else {
        i = [sortedEntries count]; di = -1;
    }
    for(Entry *e in sortedEntries) {
        e.displayOrder = [NSNumber numberWithInteger:i];
        i += di;
    }
    saveMOC(moc);
}

@end

This has two major problems:

  1. It's slow, even with small data sets.
  2. It can take an arbitrarily large amount of memory and hence crash with large data sets.

I'm open to any suggestions that are easier than ripping out Core Data and using SQL directly. Thanks so much.

EDIT Thank you for your answers. Hopefully this will clarify the question.

Here is a typical data set: There are n Entry objects, and each one has a distinct set of key/value pairs associated with it. Here I am listing the key/value pairs under each entry:

Entry 1:
    Foo => Hello world
    Bar => Lorem ipsum

Entry 2:
    Bar => La dee da
    Baz => Goodbye cruel world

Here I want to sort the entries by any of the keys "Foo", "Bar", or "Baz". If a given entry doesn't have a value for the key, it should sort like an empty string.

The SQLite store cannot sort by an unknown key using -valueForUndefinedKey:; attempting to do so results in an NSInvalidArgumentException, reason keypath Foo not found in entity <NSSQLEntity Entry id=2>.

As noted in the documentation, only a fixed set of selectors will work with sort descriptors using the SQL store.

EDIT 2

Suppose there are three instances E1, E2, and E3 of my entity, and the user attaches the custom properties 'Name' and 'Year' to each of these instances. Then we might have:

E1    Bob      2010
E2    Alice    2009
E3    Charles  2007

But we wish to present these instances to the user, sorted by any of these custom properties. For example, the user might sort by Name:

E2    Alice    2009
E1    Bob      2010
E3    Charles  2007

or by Date:

E3    Charles  2007
E2    Alice    2009
E1    Bob      2010

and so on.

+2  A: 

First question is, why do you need to store the sort in the database? If you are alway sorting in the key property, just use a sort descriptor whenever you need to access them in a sorted order.

Second question, why are you writing your own sort routine?

This design seems rather complicated. I understand the need for arbitratary storage of key value pairs, I designed a similar system in my book. However I am unclear as to the need for sorting those values nor the need for a custom sort routine such as this one.

If you could explain the need behind the sorting I could probably suggest a better strategy.

Also, I would highly recommend looking into the two methods -valueForUndefinedKey: and -setValue: forUndefinedKey: as a cleaner solution to your issue. That would allow you to write code like:

[myObject valueForKey:@"anythingInTheWorld"];
[myObject setValue:someValue forKey:@"anythingInTheWorld"];

and follow proper Key-Value Coding rules.

Update

The -valueForUndefinedKey: design is only for use in code, it is not for use accessing the store. I am still a little unclear with your goals.

Given the following model:

Entity <-->> Property

In this design, Property has two attributes:

Key
Value

From here you can access any property on Entity via -valueForUndefinedKey: because under the covers, Entity will go out and fetch the associated Property for that key. Thus you get dynamic values on your Entity.

Now the question of sorting. With this design, you can sort directly on SQLite because you are really sorting on the Property entity. Although I am still unclear as to the final goal of the sorting. What value does it have? How will it be used?

Update: Design reconsidered

The last design I proposed was wrong. On deeper reflection, it is simpler than I proposed. Your goal can be accomplished with the original Entity <-->> Property design. However there is a bit more work to be done in the -setValue: forKey: method. The logic is as follows:

  1. External code called -setValue: forKey: on an Entity.
  2. The -setValue: forKey: method attempts to retrieve the Property.
  3. If the Property exists then the value is updated.
  4. If the Property does not exist then a Property is created for each Entity with a default value set (assumed to be an empty string).

The only performance hit is when a new key is introduced. Other than that it should work without any performance penalties.

Marcus S. Zarra
I was hoping you'd show up.I tried the -valueForUndefinedKey: approach. However, the SQLite store cannot sort by keys that are not given in the conventional way in the data model.I will update the question with more details as to my intent and the approaches I have tried.
David M.
Thank you for responding again.It's basic application functionality. I am writing an app in which the user can attach arbitrary properties to objects. The user then must be able to see a list of the objects sorted by a given property value.For example, a user might attach the property 'Name' to a subset of the objects; they then would wish to see the objects alphabetized by name. (The actual property names would be terms of art, and unpredictable by me.) I do not see how to achieve this by sorting the Property objects themselves.
David M.
Thank you for taking so much time to answer this. Unfortunately, I don't grok your diagram without some commentary. As to your question, I need to sort on the value of any user-defined property, as well as some of the built-in properties of my entity (but that second part is easy and can be implemented separately). I have appended an example to the original question.
David M.
Design updated.
Marcus S. Zarra
I'm deep in some other code right now, but I'll try this and get back to you next week. Thanks again for all your help.I'll try what you suggested, but I expect to get an exception from the SQL store, saying it can't sort on keys that don't "really exist". (I forget the exact wording.) Ciao for now.
David M.
SO will let me know when you reply, if you follow my latest update it will work because the keys will exist. You create them for all entities at once.
Marcus S. Zarra