Querying for all objects of multiple child entity types in Core Data
Given the following contrived example:
I would like to query my data for all objects that are either cats or dogs. I want the result set ordered by name regardless of species, so fetching all cats then fetching all dogs won't do. I want to do this in a single query.
One way to do this would be to add a petType field to Pet, give every record a petType value that identifies the sub-entity it belongs to, then query like so:
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Pet"
inManagedObjectContext:myMOC];
[fetchRequest setEntity:entity];
// petType values: 1 = dog, 2 = cat, 3 = goldfish. Yuk.
NSPredicate *p = [NSPredicate predicateWithFormat:@"petType = 1 OR petType = 2"]
[fetchRequest setPredicate:p];
// etc...
But the mere thought of doing it that way makes me shudder. Is there a better way?
Update: Thanks to all those who've replied - there are some really good, well-thought out solutions here and I appreciate all of them.
To give this some context, the real data model is a little more complex than this (aren't they always), but it's pretty well organised. I've designed more than my fair share of data schemas in my time and I'm happy that the entities and their relationships are well considered. This issue has come about because (to extend the already shaky contrived example) the client originally wanted:
- a view showing a list of all pets
- a view showing a list of goldfish
- a view showing a list of cats
- a view showing a list of dogs
So far, so good. But they also want a view showing a combined list of all cats and dogs "because little girls 开发者_StackOverflow社区like cats and dogs". (Initially it was cats and goldfish, for the same reason.) There isn't really a way to naturally group that subset of the concrete entities; it's really rather arbitrary.
So far, Dave Dribin's "abstract intermediate entity" approach seems like the cleanest solution, although in my case I think it would feel somewhat artificial; really the only way you could truthfully label the intermediate entity would be as "ThingLittleGirlsLike"! :)
As you've found out, you cannot use entity.name
in your fetch predicate for SQLite stores (you can use it on other store types). You can add a petType
, as you suggest, which works well enough, but makes me shudder, too. As an alternative, you could fetch all Pets
, and then filter the fetched results based on entity.name
. But that's a bit inefficient, since you're loading more entities than you need and your doing in-memory filtering that SQLite could be doing on your behalf.
I think the real question is: what are you trying to do? If you really need to fetch Cats
and Dogs
without Goldfish
, then your model should reflect that. You could insert a FourLeggedPet
abstract entity as a parent of Cat
and Dog
. Then, in your fetch request, use FourLeggedPet
as the entity with setIncludesSubentities:YES
.
Your managed objects already have a pet type: they have their entity.name
. If you search for all Pet
entities that have the entity name @"Cat"
or @"Dog"
, that should do it, I think.
Example (typed quick and dirty into Xcode, so poorly designed):
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
NSEntityDescription *catEntity = [NSEntityDescription entityForName: @"Cat" inManagedObjectContext: [self managedObjectContext]];
NSEntityDescription *dogEntity = [NSEntityDescription entityForName: @"Dog" inManagedObjectContext: [self managedObjectContext]];
NSEntityDescription *goldfishEntity = [NSEntityDescription entityForName: @"Goldfish" inManagedObjectContext: [self managedObjectContext]];
id cat = [[NSManagedObject alloc] initWithEntity: catEntity insertIntoManagedObjectContext: [self managedObjectContext]];
[cat setName: @"Mittens"];
id dog = [[NSManagedObject alloc] initWithEntity: dogEntity insertIntoManagedObjectContext: [self managedObjectContext]];
[dog setName: @"Rover"];
id fish = [[NSManagedObject alloc] initWithEntity: goldfishEntity insertIntoManagedObjectContext: [self managedObjectContext]];
[fish setName: @"Goldie"];
[[self managedObjectContext] save: NULL];
[fish release];
[dog release];
[cat release];
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName: @"Pet"];
NSPredicate *pred = [NSPredicate predicateWithBlock: ^(id evaluatedObject, NSDictionary *bindings) {
BOOL result = ![[[evaluatedObject entity] name] isEqualToString: @"Goldfish"];
return result;
}];
[fetch setPredicate: pred];
NSArray *entities = [[self managedObjectContext] executeFetchRequest: fetch error: NULL];
NSLog(@"Entities: %@", entities);
}
This logs the following:
2011-06-30 14:46:57.435 CDSubentityTest[5626:407] Entities: ( " (entity: Cat; id: 0x10025f740 ; data: {\n name = Mittens;\n})", " (entity: Dog; id: 0x1002914e0 ; data: {\n name = Rover;\n})" )
Notice the expected disappearance of the goldfish.
I believe the approach you suggested would work nice. Having a petType
could also allow you to define subtypes of Pet
s (i.e. "mammals", "birds", "reptiles", "fish", etc.). Of course, you could have abstract entities for each of these instead, but it's not clear how you would benefit from that either. It ultimately depends on how your application uses the model. Do you ever need to fetch all Pet
s? Do you plan for the possibility to get only Cat
s and Fish
in a single fetch? Do you care about fetching data you won't immediately use?
Once you define the required classifications based on how you want to display/use the data, adapt the model to those classifications. Having a good looking, generic model, can often be a pain in the butt to use...
Predicates don't recognize entities so you can't construct a predicate to find them. (Update: This is only true of SQL stores.)
If you have your heart set on fetching by pet type then you don't have choice but to provide an attribute that will provide the pet type value and which will let predicates and fetches operate. However, you can make a much cleaner and safer version than the one you proposed.
- Create a
petType
attribute for each entity (it can't be inherited in the entity but can be inherited in custom NSManagedObject subclasses.) - Then set the default value in the data model editor to the name of species e.g. cat, dog, goldfish etc. (If you use an inherited attribute, you inherit the default value as well.)
Override the setter method to do nothing effectively making the attribute readonly. (This can be inherited from a common superclass.)
-(void) setPetType:(NSString *) petType{ return; }
Now finding all dogs and cats just becomes a matter of setting the fetch entity to Pet
and then providing and array of pet type names for the IN
operator.
NSArray *petTypes=[NSArray arrayWithObjects:@"cat",@"dog",nil];
NSPredicate *p=[NSPredicate predicateWithFormat:@"petType IN %@", petTypes];
While this will work, I think that Dave Dribin made the best point. This kind of hack is usually needed when you haven't properly refined your data model. If you need to group various pets by pet type then that grouping probably belongs to another real-world object, condition or event e.g. owner, which in turn should be modeled with relationships to specific pet instances. If you do that, then your grouping happens automatically and you don't have to mess around with all the above.
精彩评论