2013-09-27 3 views
4

Я испытываю странную проблему CoreData.
Прежде всего, в моем проекте я использую много фреймворков, поэтому есть много источников проблем, поэтому я решил создать минимальный проект, который повторяет мою проблему. Вы можете клонировать Test project on Github и повторять мой шаг шаг за шагом.
Итак, проблема:
NSManagedObject привязан к нему это NSManagedObjectID, который не позволяет объекту быть удалены из NSManagedObjectContext правильно
Таким образом, шаги для воспроизведения:
В моем AppDelegate, я установки CoreData стек как обычно. AppDelegate имеет свойство managedObjectContext, к которому можно получить доступ, чтобы получить NSManagedObjectContext для основного потока. График объекта приложения состоит из одного объекта Message с body, from, timestamp атрибутов. Приложение имеет только один viewController с единственным методом viewDidLoad. Это выглядит так:
Поведение Strange NSManagedObject

- (void)viewDidLoad 
{ 
    [super viewDidLoad]; 

    NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext; 

    NSEntityDescription *messageEntity = [NSEntityDescription entityForName:NSStringFromClass([Message class]) inManagedObjectContext:context]; 

    // Here we create message object and fill it 
    Message *message = [[Message alloc] initWithEntity:messageEntity insertIntoManagedObjectContext:context]; 

    message.body  = @"Hello world!"; 
    message.from  = @"Petro Korienev"; 

    NSDate *now = [NSDate date]; 

    message.timestamp = now; 

    // Now imagine that we send message to some server. Server processes it, and sends back new timestamp which we should assign to message object. 
    // Because working with managed objects asynchronously is not safe, we save context, than we get it's objectId and refetch object in completion block 

    NSError *error; 
    [context save:&error]; 

    if (error) 
    { 
     NSLog(@"Error saving"); 
     return; 
    } 

    NSManagedObjectID *objectId = message.objectID; 

    // Now simulate server delay 

    double delayInSeconds = 5.0; 
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void) 
    { 
     // Refetch object 
     NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext; 
     Message *message = (Message*)[context objectWithID:objectId]; // here i suppose message to be nil because object is already deleted from context and context is already saved. 

     message.timestamp = [NSDate date]; // However, message is not nil. It's valid object with data fault. App crashes here with "Could not fulfill a fault" 

     NSError *error; 
     [context save:&error]; 

     if (error) 
     { 
      NSLog(@"Error updating"); 
      return; 
     } 

    }); 

    // Accidentaly user deletes message before response from server is returned 

    delayInSeconds = 2.0; 
    popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void) 
    { 
     // Fetch desired managed object 
     NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext; 

     NSPredicate *predicate = [NSPredicate predicateWithFormat:@"timestamp == %@", now]; 
     NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Message class])]; 
     request.predicate = predicate; 

     NSError *error; 
     NSArray *results = [context executeFetchRequest:request error:&error]; 
     if (error) 
     { 
      NSLog(@"Error fetching"); 
      return; 
     } 

     Message *message = [results lastObject]; 

     [context deleteObject:message]; 
     [context save:&error]; 

     if (error) 
     { 
      NSLog(@"Error deleting"); 
      return; 
     } 
    }); 
} 

Ну, я обнаружил сбой приложения, так что я стараюсь принести message другой путь. Я изменил код выборки:

... 
// Now simulate server delay 

double delayInSeconds = 5.0; 
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) 
{ 
    // Refetch object 
    NSManagedObjectContext *context = ((AppDelegate*)[[UIApplication sharedApplication] delegate]).managedObjectContext; 

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"timestamp == %@", now]; 
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([Message class])]; 
    request.predicate = predicate; 

    NSError *error; 
    NSArray *results = [context executeFetchRequest:request error:&error]; 
    if (error) 
    { 
     NSLog(@"Error fetching in update"); 
     return; 
    } 

    Message *message = [results lastObject]; 
    NSLog(@"message %@", message); 

    message.timestamp = [NSDate date]; 

    [context save:&error]; 

    if (error) 
    { 
     NSLog(@"Error updating"); 
     return; 
    } 

}); 
... 

Какой NSLog'ed message (null)
Таким образом, он показывает:
1) Сообщение на самом деле не существует в БД. Он не может быть извлечен.
2) Первая версия кода каким-либо образом удалена. message объект в контексте (вероятно, это означает, что идентификатор объекта был сохранен для вызова блока).
Но почему я мог получить удаленный объект по его идентификатору? Мне нужно знать.
Очевидно, прежде всего я изменил objectId на __weak. Попадали в катастрофу даже до блоков :)
enter image description here

So CoreData построен без ARC? Хм интересно.
Ну, я считал copy NSManagedObjectID. Что я получил?
enter image description here

(lldb) po objectId 
0xc28ed20 <x-coredata://8921D8F8-436C-4CBC-B4AB-118198988D88/Message/p4> 
(lldb) po message.objectID 
0xc28ed20 <x-coredata://8921D8F8-436C-4CBC-B4AB-118198988D88/Message/p4> 

Посмотрите, что случилось? NSCopying-copy выполнен как return self на NSManagedObjectID
Последняя попытка была __unsafe_unretained для objectId. Здесь мы идем:

...  
    __unsafe_unretained NSManagedObjectID *objectId = message.objectID; 
    Class objectIdClass = [objectId class]; 
    // Now simulate server delay 

    double delayInSeconds = 5.0; 
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void) 
    { 

     if (![NSObject safeObject:objectId isMemberOfClass:objectIdClass]) 
     { 
      NSLog(@"Object for update already deleted"); 
      return; 
     } 
...   

safeObject: isMemberOfClass: Реализация:

#ifndef __has_feature 
#define __has_feature(x) 0 
#endif 

#if __has_feature(objc_arc) 
#error ARC must be disabled for this file! use -fno-objc-arc flag for compile this source 
#endif 

#import "NSObject+SafePointer.h" 

@implementation NSObject (SafePointer) 

+ (BOOL)safeObject:(id)object isMemberOfClass:(__unsafe_unretained Class)aClass 
{ 
#pragma clang diagnostic push 
#pragma clang diagnostic ignored "-Wdeprecated-objc-isa-usage" 
    return ((NSUInteger*)object->isa == (NSUInteger*)aClass); 
#pragma clang diagnostic pop 
} 

@end 

Краткое объяснение - мы используем __unsafe_unretained переменную, поэтому во время вызова блока он может быть освобожден, так что мы должны проверить является ли это действительным объектом. Поэтому мы сохраняем его class перед блоком (он не сохраняется, он назначается) и проверяет его в блоке с помощью safePointer:isMemberOfClass:
Так что пока объект переназначения с помощью управляемого объектаObjectId равен UNTRUSTED шаблон для меня.
Есть ли у кого-нибудь предложения, как я должен делать в этой ситуации? Использовать __unsafe_unretained и проверить?Однако этот managedObjectId также может быть сохранен другим кодом, поэтому он приведет к сбою в работе could not fulfill. Или для извлечения объекта каждый раз предикатом? (и что делать, если объект уникально определен 3-4 атрибутами? Сохраните их все для блока завершения?). Каков наилучший шаблон для работы с управляемыми объектами асинхронно?
Извините за длительные исследования, спасибо заранее.

P.S. Вы все еще можете повторить свои шаги или сделать свои собственные эксперименты с Test project

+0

Я обновил репо, чтобы содержать правильную реализацию в соответствии с ответом @ Tommy –

ответ

2

Не используйте objectWithID:. Используйте existingObjectWithID:error:. За документацией, the former:

... always returns an object. The data in the persistent store represented by objectID is assumed to exist—if it does not, the returned object throws an exception when you access any property (that is, when the fault is fired). The benefit of this behavior is that it allows you to create and use faults, then create the underlying data later or in a separate context.

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

existingObject... вернет объект только в том случае, если он существует.

+0

Работает как очарование! Я был почти уверен, что должно быть простое решение в CoreData, и я просто не знал его =) Спасибо, следующая сборка моего приложения будет с 'existingObjectWithID: error:' =) –

Смежные вопросы