Как автоматически настроить отношения Core Data при использовании вложенных контекстов

Я изо всех сил пытаюсь найти достойное решение проблемы, возникающей при использовании вложенных контекстов управляемых объектов в основных данных. Возьмите модель с двумя именами: "Лицо и имя", где каждый человек имеет отношения "один к одному" с именем "Имя", а "Имя" не является обязательным. Раньше в методе Person -awakeFromInsert я автоматически создавал бы объект Name для нового Person:

- (void)awakeFromInsert
{
    [super awakeFromInsert];

    NSManagedObjectContext *context = [self managedObjectContext];
    self.name = [NSEntityDescription insertNewObjectForEntityForName:@"Name" inManagedObjectContext:context];
}

Это отлично работает в одном, не вложенном контексте управляемых объектов. Однако, если контекст имеет родительский контекст, когда дочерний контекст сохраняется, в родительском контексте создается новый объект Person, а -awakeFromInsert вызывается снова на этом новом объекте до того, как будут скопированы исходные свойства и отношения Person. Итак, создается другой объект Name, затем "отключается", когда копируется существующее отношение имен. Сохранение не выполняется, потому что проверка отношения плавающего имени now-nil person не выполняется. Эта проблема описывается здесь, а также в других местах.

До сих пор я не смог найти хорошее решение этой проблемы. Ленько создавая отношения в методе геттера, фактически вызывает ту же проблему, потому что получатель вызывается внутренними механизмами Core Data, когда новый Person создается в родительском контексте.

Единственное, что я могу придумать, это отказаться от автоматического создания отношений и всегда создавать отношения явно либо в классе контроллера, который создает Person, либо в методе удобства (например, +[Person insertNewPersonInManagedObjectContext:]), который вызывается только мой код и всегда является методом, который используется для создания нового объекта Person явно. Возможно, это лучшее решение, но я бы предпочел не быть настолько строгим, чтобы только один метод мог использоваться для создания управляемых объектов, когда другие методы создания, которые я не контролирую, и использование которых я не могу легко проверить/исключить, существуют. Во-первых, это будет означать несколько подклассов NSArrayController для настройки способа создания управляемых объектов.

Кто-нибудь, кто столкнулся с этой проблемой, придумал элегантное решение, позволяющее одному NSManagedObject создавать объект отношений автоматически при создании/вставке?

Ответ 1

В итоге я решил использовать метод удобства. Все подклассы NSManagedObject в моем приложении имеют метод +insertInManagedObjectContext:. Создание экземпляров этих объектов (в моем собственном коде) всегда выполняется с использованием этого метода. Внутри этого метода я делаю это:

+ (instancetype)insertInManagedObjectContext:(NSManagedObjectContext *)moc
{
    MyManagedObject *result = [NSEntityDescription insertNewObjectForEntityForName:@"MyEntityName" inManagedObjectContext:moc]
    [result awakeFromCreation];
    return result;
}

- (void)awakeFromCreation
{
    // Do here what used to be done in -awakeFromInsert.
    // Set up default relationships, etc.
}

Что касается проблемы NSArrayController, решение этой проблемы неплохое. Я просто создал подкласс NSArrayController, overrode -newObject и использовал этот подкласс для всех соответствующих NSArrayControllers в моем приложении:

@implementation ORSManagedObjectsArrayController

- (id)newObject
{
    NSManagedObjectContext *moc = [self managedObjectContext];
    NSEntityDescription *entity = [NSEntityDescription entityForName:[self entityName]
                                              inManagedObjectContext:moc];
    if (!entity) return nil;

    Class class = NSClassFromString([entity managedObjectClassName]);
    return [class insertInManagedObjectContext:moc];
}

@end

Ответ 2

Первая мысль, которая приходит на ум, заключается в том, что, хотя отношение Name person не является необязательным, вы не сказали, что отношение person Name также не является необязательным. Можно ли создать person без Name, выставить свой код, а затем создать Name позже, когда вам это действительно нужно?

Если нет, один простой способ - просто проверить, находитесь ли вы в корневом контексте перед созданием Name:

- (void)awakeFromInsert
{
    [super awakeFromInsert];

    NSManagedObjectContext *context = [self managedObjectContext];
    if ([context parentContext] != nil) {
        self.name = [NSEntityDescription insertNewObjectForEntityForName:@"Name" inManagedObjectContext:context];
    }
}

Но это работает только в том случае, если вы всегда создаете новые экземпляры в дочернем контексте и никогда не устанавливаете контексты более чем на один уровень.

Вместо этого я создаю метод, описывающий insertNewPersonInManagedObjectContext:. Затем добавьте к нему что-то вроде следующего, чтобы обрабатывать любые случаи, когда экземпляры создаются для вас (например, контроллеры массивов):

- (void)willSave
{
    if ([self name] == nil) {
        NSManagedObjectContext *context = [self managedObjectContext];
        Name *name = [NSEntityDescription insertNewObjectForEntityForName:@"Name" inManagedObjectContext:context];
        [self setName:name];
    }
}

... и, конечно же, не беспокоиться о пользовательском awakeFromInsert...