Основные данные iOS: подход к настройке отношений для полиморфной ассоциации Rails - PullRequest
4 голосов
/ 19 апреля 2011

У меня есть приложение для iOS с моделью Core Data, которая имитирует мою внутреннюю модель данных Rails. В моей серверной модели Rails я использую полиморфные ассоциации для нескольких сущностей. Моя модель Rails выглядит примерно так:

Airport < ActiveRecord::Base
has_many :reviews, :as => :reviewable

Restaurant < ActiveRecord::Base
has_many :reviews, :as => :reviewable

Review < ActiveRecord::Base
belongs_to :reviewable, :polymorphic => :true

В моей модели базовых данных у меня есть три отдельных объекта, MyAirport, MyRestaurant и MyReview, с соответствующими свойствами, как указано ниже:

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

Мой вопрос относится к классу MyReview в Core Data. Каков наилучший способ установить правильное отношение базовых данных (например, аэропорт или ресторан) на основе значений в reviewableId и reviewableType?

Я пробовал следующее, и все кажутся немного нечистыми по той или иной причине. Вообще говоря, мне нужно убедиться, что у меня есть ОБА и ревизионный доступ, и доступный для просмотра вид, прежде чем устанавливать правильную связь, что, по-видимому, является основной проблемой, связанной с предполагаемой чистотой. При гидратации этих объектов модели из JSON я на самом деле не контролирую порядок заполнения reviewableId и reviewableType для нового объекта MyReview.

1) Пользовательские сеттеры для одного или обоих reviewableId и reviewableType - я получаю логику в каждом методе сеттера, который проверяет значение другого свойства, чтобы убедиться, что оно заполнено, прежде чем я смог установить отношения аэропорт или ресторан в модели MyReview объект. Вообще говоря, это работает, но это не так, но в каждом сеттере есть какой-то уродливый дубликат кода.

- (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 intValue] != 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 для reviewableId и reviewableType - в awakeFromInsert и awakeFromFetch я регистрирую объект модели для наблюдения за его собственными свойствами reviewableId и reviewableType через KVO. Это действительно ужасно, когда объект наблюдает сам, но, по крайней мере, у меня есть один метод, который обрабатывает заполнение ассоциации, что выглядит как улучшение по сравнению с # 1. Обратите внимание, что здесь я заканчиваю некоторыми сбоями с помощью метода removeObserver: forKeyPath: в методе dealloc (например, сбой указывает, что я удалил наблюдателя, которого не было ??), так что на данный момент это далеко от проверенного кода.

- (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) Переопределите willSave, используя наш метод updatePolymorphicAssociations сверху. Кажется, это работает, но откладывает все изменения в ассоциации до тех пор, пока мы не сохраним объект, что часто не происходит в то время, когда мы вносим начальные изменения в reviewableId и reviewableType. Кажется также, что здесь есть некоторые ограничения, связанные со вставкой нескольких новых объектов MyReview через фоновый поток, предположительно связанные с willSave, которые фактически вызывают изменение объекта, который мы только что сохранили, и, таким образом, снова запускаем willSave. Даже с проверкой на ноль в updatePolymorphicAssociations я все еще могу завершить работу приложения с помощью этого подхода.

- (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) Переопределить didSave вместо willSave. Это решает проблемы сохранения с большим импортом в фоновом режиме, который мы получаем с # 3, но затем мы получаем несохраненные изменения, что ужасно.

Итак, у меня, похоже, есть несколько вариантов, но ни один из них не подходит для решения проблемы, которая кажется обычной в приложениях для iOS, с использованием Core Data, поддерживаемой облачной БД Rails. Я чрезмерно анализирую это? Или есть лучший способ?

Обновление: обратите внимание, что цель состоит в том, чтобы установить только одно из отношений в MyReview, в зависимости от того, является ли типableableType "Аэропорт" или "Ресторан". Такая установка отношений сама по себе довольно некрасива, но именно поэтому я задаю этот вопрос. :)

1 Ответ

2 голосов
/ 20 апреля 2011

Вы, похоже, совершаете кардинальную ошибку, считая базовые данные объектной оболочкой реляционной базы данных.Это не так.ActiveRecord - это объектная оболочка реляционной базы данных, поэтому она должна использовать такие методы, как полиморфная ассоциация.Вы не можете втиснуть Core Data в парадигму реляционной оболочки и ожидать элегантного дизайна.

Из того, что я могу разгадать из вашей модели данных, на самом деле вам не нужны атрибуты reviewablId и reviewableType для фактического создания самого графика.Вместо этого вы должны заменить их наследованием сущностей и связями.Начнем с модели данных, которая выглядит так.

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
}

Это заменяет полиморфную ассоциацию объектными отношениями и делает различие между двумя типами просматриваемых мест, создавая две отдельные сущности.Поскольку сущность Review имеет отношение, определенное с абстрактной сущностью Reviewable, она также может образовывать связь с любой из Reviewable сущностей.Вы можете извлечь с помощью reviewableID для объекта Reviewable, и он вернет объект Airport или Restaurant с этим идентификатором, и вы можете получить либо отношение place нового объекта Review.

Для обработки объектов JSON вы должны (в 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

Я думаю, что это даст вам целостный граф объектов без необходимости прыгать через обручи.В любом случае, большая часть проверки, которую вы рассматриваете в родительском посте, может быть заменена субтитрами и надлежащими отношениями.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...