iOS Core Data: Approach for setting relationship for Rails polymorphic association
I have an iOS app with a Core Data model that mimics my Rails back-end data model. In my Rails back-end model, I'm using polymorphic associations for a few entities. My Rails model looks something like this:
Airport < ActiveRecord::Base
has_many :reviews, :as => :reviewable
Restaurant < ActiveRecord::Base
has_many :reviews, :as => :reviewable
Review < ActiveRecord::Base
belongs_to :reviewable, :polymorphic => :true
In my Core Data model, I have three separate entities, MyAirport, MyRestaurant, and MyReview, with relevant properties as indicated below:
MyAirport
@property (nonatomic, retain) NSSet* reviews; //inverse is airport
MyRestaurant
@property (nonatomic, retain) NSSet* reviews; //inverse is restaurant
MyReview
@property (nonatomic, retain) NSNumber* reviewablId;
@property (nonatomic, retain) NSString* reviewableType;
@property (nonatomic, retain) MyAirport* airport; //inverse is reviews
@property (nonatomic, retain) MyRestaurant* restaurant; //inverse is reviews
My question pertains to the MyReview class in Core Data. What is the best way to set the proper Core Data relationship (e.g. either airport or restaurant) based on the values in reviewableId and reviewableType?
I've tried the following, and all seem a bit unclean for one reason or another. Generally speaking, I need to ensure I have BOTH reviewableId and reviewableType available before setting the correct association, which seems to be the main issue related to perceived cleanliness. When hydrating these model objects from JSON, I do not really control the order in which reviewableId and reviewableType are populated on a new MyReview object.
1) Custom setters on one or both reviewableId and reviewableType - I end up with logic in each setter method that checks the value of the other property to ensure it's populated before I can set either the airport or restaurant relationship on the MyReview model object. Generally speaking, this works, but it just doesn't feel right, while also having some ugly duplicate code in each setter.
- (void)setReviewableId:(NSNumber*)reviewableId {
[self willChangeValueForKey:@"reviewableId"];
[self setPrimitiveReviewableId:reviewableId];
[self didChangeValueForKey:@"reviewableId"];
if (reviewableId && [reviewableId intValue] != 0 && self.reviewableType) {
if (self.airport == nil && [self.reviewableType isEqualToString:@"Airport"]) {
MyAirport myAirport = <... lookup MyAirport by airportId = reviewableId ...>
if (myAirport) {
self.airport = myAirport;
}
} else if (self.restaurant == nil && [self.reviewableType isEqualToString:@"Restaurant"]) {
MyRestaurant myRestaurant = <... lookup MyRestaurant by restaurantId = reviewableId ...>
if (myRestaurant) {
self.restaurant = myRestaurant;
}
}
}
}
- (void)setReviewableType:(NSString*)reviewableType {
[self willChangeValueForKey:@"reviewableType"];
[self setPrimitiveReviewableType:reviewableType];
[self didChangeValueForKey:@"reviewableType"];
if (self.reviewableId && [self.reviewableId i开发者_运维技巧ntValue] != 0 && reviewableType) {
if (self.airport == nil && [reviewableType isEqualToString:@"Airport"]) {
MyAirport myAirport = <... lookup MyAirport by airportId = self.reviewableId ...>
if (myAirport) {
self.airport = myAirport;
}
} else if (self.restaurant == nil && [reviewableType isEqualToString:@"Restaurant"]) {
MyRestaurant myRestaurant = <... lookup MyRestaurant by restaurantId = self.reviewableId ...>
if (myRestaurant) {
self.restaurant = myRestaurant;
}
}
}
}
2) KVO on reviewableId and reviewableType - In awakeFromInsert and awakeFromFetch, I register the model object to observer its own reviewableId and reviewableType properties via KVO. This feels really ugly having the object observe itself, but at least I have a single method that handles populating the association, which seems like a bit of an improvement over #1. Note that I do end up with some crashes here with removeObserver:forKeyPath: in the dealloc method (e.g. crash indicates I've removed an observer that didn't exist??), so this is far from proven code at this point.
- (void)awakeFromFetch {
[super awakeFromFetch];
[self addObserver:self forKeyPath:@"reviewableId" options:0 context:nil];
[self addObserver:self forKeyPath:@"reviewableType" options:0 context:nil];
}
- (void)awakeFromInsert {
[super awakeFromInsert];
[self addObserver:self forKeyPath:@"reviewableId" options:0 context:nil];
[self addObserver:self forKeyPath:@"reviewableType" options:0 context:nil];
}
- (void)dealloc {
[self removeObserver:self forKeyPath:@"reviewableId"];
[self removeObserver:self forKeyPath:@"reviewableType"];
[super dealloc];
}
- (void)updatePolymorphicAssociations {
if (self.reviewableId && [self.reviewableId intValue] != 0 && self.reviewableType) {
if (self.airport == nil && [self.reviewableType isEqualToString:@"Airport"]) {
MyAirport myAirport = <... lookup MyAirport by airportId = self.reviewableId ...>
if (myAirport) {
self.airport = myAirport;
}
} else if (self.restaurant == nil && [self.reviewableType isEqualToString:@"Restaurant"]) {
MyRestaurant myRestaurant = <... lookup MyRestaurant by restaurantId = self.reviewableId ...>
if (myRestaurant) {
self.restaurant = myRestaurant;
}
}
}
}
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {
[self updatePolymorphicAssociations];
}
3) Override willSave, using our updatePolymorphicAssociations method from above. This seems to work, but it defers all changes to the association until we go to save the object, which often is not at the time we make the initial change to reviewableId and reviewableType. There also seem to be some limitations here related to inserting a bunch of new MyReview objects via a background thread, presumably related to willSave actually causing a change to the object we were just saving, and thus triggering willSave again. Even with the nil checking in updatePolymorphicAssociations, I still seem to be able to crash the app with this approach.
- (void)updatePolymorphicAssociations {
if (self.reviewableId && [self.reviewableId intValue] != 0 && self.reviewableType) {
if (self.airport == nil && [self.reviewableType isEqualToString:@"Airport"]) {
MyAirport myAirport = <... lookup MyAirport by airportId = self.reviewableId ...>
if (myAirport) {
self.airport = myAirport;
}
} else if (self.restaurant == nil && [self.reviewableType isEqualToString:@"Restaurant"]) {
MyRestaurant myRestaurant = <... lookup MyRestaurant by restaurantId = self.reviewableId ...>
if (myRestaurant) {
self.restaurant = myRestaurant;
}
}
}
}
- (void)willSave {
[self updatePolymorphicAssociations];
}
4) Override didSave, instead of willSave. This solves the save issues with large imports in the background that we get with #3, but then we end up with unsaved changes, which is ugly.
So, I seem to have a bunch of options here, but none just feels right for solving what would seem to be a common problem in iOS apps using Core Data backed by a Rails cloud DB. Am I over-analyzing this? Or is there a better way?
Update: Note that the goal is to set only one of the relationships on MyReview, based on whether the reviewableType is "Airport" or "Restaurant". This relationship setup is, in and of itself, quite ugly, but that's why I'm asking this question. :)
You appear to be making the cardinal mistake of thinking of Core Data as an object wrapper around a relational database. It isn't. ActiveRecord is an object wrapper around a relational database which is why it has to use techniques like polymorphic association. You can't cram Core Data into the relational wrapper paradigm and expect to get an elegant design.
From what I can puzzle out of your data model, you don't actually need the reviewablId
and
reviewableType
attributes to actually create the graph itself. Instead, you would replace them with entity inheritance and relationships. Start with a data model that looks like so.
Review{
//... some review attributes
place<<-(required)->ReviewablePlace.reviews //... logically a review must always have a place that is reviewed
}
ReviewablePlace(abstract){
reviewableID:Number //... in case you have to fetch on this ID number later
reviews<-->>Review.place
}
Airport:Reviewable{
//... some airport specific attributes
}
Restaurant:Reviewable{
//... some restaurant specific attributes
}
This replaces the polymorphic association with object relationships and it differentiates between the two types of reviewable places by making two separate subentities. Since the Review
entity has a relationship defined with the abstract Reviewable
entity it can also form a relationship with any of the Reviewable
subentities. You can fetch by reviewableID on the Reviewable
entity and it will return the Airport
or Restaurant
object with that id and you can pop either into the place
relationship of a new Review
object.
To process you JSON objects you would (in psuedocode):
if Airport
fetch on reviewableID
if fetch returns existing Airport managed object
break //don't want a duplicate so don't create an object
else
create new Airport managed object with reviewableID
break
if Restaurant
fetch on reviewableID
if fetch returns existing Restaurant managed object
break //don't want a duplicate so don't create an object
else
create new Restaurant managed object with reviewableID
break
if Review
fetch on reviewableID
if fetch returns Reviewable object of either subclass
create new Review object and assign fetched Reviewable object to the Review.place relationship
break
else
create new Airport or Resturant object using the JSON provided reviewableID and reviewableType info
create new Review object and assign the newly created Reviewable subclass object to the Review.place relationship
break
I think that will give you an integral object graph without having to jump through hoops. In any case, much of the checking that you are contemplating in the parent post can be replaced with the subentities and the proper relationships.
精彩评论