问题
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:
- It's slow, even with small data sets.
- 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.
回答1:
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:
- External code called
-setValue: forKey:
on anEntity
. - The
-setValue: forKey:
method attempts to retrieve theProperty
. - If the
Property
exists then the value is updated. - If the
Property
does not exist then aProperty
is created for eachEntity
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.
来源:https://stackoverflow.com/questions/3561936/arbitrary-attributes-in-core-data