问题描述
我遇到了奇怪的CoreData问题。
首先,在我的项目中,我使用了很多框架,所以有很多问题的来源 - 所以我认为创建最小的项目,重复我的问题。您可以克隆
进行自己的实验
不要使用 objectWithID:
。使用 existingObjectWithID:error:
。根据文档,:
这正是你看到的。你得到一个对象,因为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:
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行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!