奇怪的NSManagedObject行为 [英] Strange NSManagedObject behaviour

查看:124
本文介绍了奇怪的NSManagedObject行为的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我遇到了奇怪的CoreData问题。

首先,在我的项目中,我使用了很多框架,所以有很多问题的来源 - 所以我认为创建最小的项目,重复我的问题。您可以克隆


进行自己的实验

解决方案

不要使用 objectWithID:。使用 existingObjectWithID:error:。根据文档,前者


...总是返回一个对象。由objectID表示的持久存储
中的数据被假定为存在 - 如果不存在,则
返回的对象在您访问任何属性(
是,当故障是烧)。这种行为的好处是它
允许你创建和使用故障,然后创建基础数据
以后或在单独的上下文。


这正是你看到的。你得到一个对象,因为Core Data认为你必须要有一个ID,即使它没有一个。当你试图存储它,而不是在临时创建一个实际的对象,它不知道该怎么做,你会得到异常。



existingObject ... 将返回一个对象(如果存在)。


I'm experiencing strange CoreData issue.
First of all, in my project i use lot of frameworks, so there are many sources of problem - so i considered to create minimal project which repeats my issue. You can clone Test project on Github and repeat my test step-by-step.
So, the problem:
NSManagedObject is tied to it's NSManagedObjectID which doesn't let object to be deleted from NSManagedObjectContext properly
So, steps to reproduce:
In my AppDelegate, i setup CoreData stack as usual. AppDelegate has managedObjectContext property, which can be accessed to obtain NSManagedObjectContext for main thread. Application's object graph consists of one entity Message with body, from, timestamp attributes. Application has only one viewController with only method viewDidLoad. It looks so:

- (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;
        }
    });
}

Well, i detected app crash so i try to fetch message another way. I changed fetch code:

...
// 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;
    }

});
...

Which NSLog'ed message (null)
So, it shows:
1) Message is actually not existent in DB. It cannot be fetched.
2) First version of code someway kept deleted message object in context (Probably cause it's object id was retained for block call).
But why i could obtain deleted object by its id? I need to know.
Obviously, first of all, i changed objectId to __weak. Got crash even before blocks:)

So CoreData is built without ARC? Hmm interesting.
Well, i considered to copy NSManagedObjectID. What i've gotten?

(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>

See what's wrong? NSCopying's -copy is implemented like return self on NSManagedObjectID
Last try was __unsafe_unretained for objectId. Here we go:

...    
    __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: implementation:

#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

Brief explanation - we use __unsafe_unretained variable, so at time of block call it can be freed, so we have to check whether it's valid object. So we save it's class before block (it's not retain, it's assign) and check it in block via safePointer:isMemberOfClass:
So for now, refetching object by it's managedObjectId is UNTRUSTED pattern for me.
Does anybody have any suggestions how i should do in this situation? To use __unsafe_unretained and check? However, this managedObjectId can be also retained by another code, so it will cause could not fulfill crash on property access. Or to fetch object everytime by predicate? (and what to do if object is uniquely defined by 3-4 attributes? Retain them all for completion block?). What is the best pattern for working with managed objects asynchronously?
Sorry for long research, thanks in advance.

P.S. You still can repeat my steps or make your own experiments with Test project

解决方案

Don't use objectWithID:. Use existingObjectWithID:error:. Per the documentation, 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.

Which is exactly what you're seeing. You get an object back because Core Data thinks you must want one with that ID even though it doesn't have one. When you try to store to it, without having created an actual object in the interim, it doesn't know what to do and you get the exception.

existingObject... will return an object only if one exists.

这篇关于奇怪的NSManagedObject行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆