I'm trying to perform a fetch to retrieve managed objects from the context using an array of object IDs I gathered from a separate context. However, the fetch comes back with an empty array.
From the "Retrieving Specific Objects" section of the "Core Data Programming Guide" link:
If your application uses multiple contexts and you want to test whether an object has been deleted from a persistent store, you can create a fetch request with a predicate of the form self == %@. The object you pass in as the variable can be either a managed object or a managed object ID..."
If you need to test for the existence of several objects, it is more efficient to use the IN operator than it is to execute multiple fetches for individual objects, for example:
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self IN %@", arrayOfManagedObjectIDs];
While this is talking about testing for object deletion, I presumed if the objects are not deleted then the results array is not empty, and will contain the actual NSManagedObjects. However, when I execute this code:
- (NSArray *)managedObjectsOfEntityName:(NSString *)entityName fromObjectIDs:(NSArray *)objectIDs managedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
request.predicate = [NSPredicate predicateWithFormat:@"self IN %@", objectIDs];
__autoreleasing NSError *error = nil;
NSManagedObjectID *testID = objectIDs[0];
NSManagedObject *obj = [managedObjectContext existingObjectWithID:testID error:&error];
if (!obj)
{
NSLog(@"Unable to perform fetch. Error: %@", error);
}
error = nil;
NSArray *results = [managedObjectContext executeFetchRequest:request error:&error];
if (!results)
{
NSLog(@"Unable to perform fetch. Error: %@", error);
}
return results;
}
The results
array is non-nil and empty, while obj
is properly populated.
I've added the explicit call to existingObjectWithID:
as a sanity check, and it comes back with the expected object, without error.
Here's the debugger output for the pertinent variables:
(lldb) po entityName
Foo
(lldb) po objectIDs
<__NSArrayI 0x1170d4950>(
0xd0000000055c0082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p343>,
0xd000000008e80082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p570>,
0xd000000008840082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p545>,
0xd000000006040082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p385>,
0xd000000007740082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p477>,
0xd000000008280082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p522>,
0xd000000008e40082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p569>
)
(lldb) po request
<NSFetchRequest: 0x10f338840> (entity: Foo; predicate: (SELF IN {
0xd0000000055c0082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p343>,
0xd000000008e80082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p570>,
0xd000000008840082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p545>,
0xd000000006040082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p385>,
0xd000000007740082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p477>,
0xd000000008280082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p522>,
0xd000000008e40082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p569>});
sortDescriptors: ((null)); type: NSManagedObjectResultType; )
(lldb) po request.predicate
SELF IN {
0xd0000000055c0082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p343>,
0xd000000008e80082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p570>,
0xd000000008840082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p545>,
0xd000000006040082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p385>,
0xd000000007740082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p477>,
0xd000000008280082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p522>,
0xd000000008e40082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p569>}
(lldb) po testID
0xd0000000055c0082 <x-coredata://CEB1EDA5-7F7A-4342-85E5-6C2E261308CC/Foo/p343>
(lldb) p testID.isTemporaryID
(bool) $7 = false
(lldb) p obj
(NSManagedObject *) $9 = 0x000000010f338630
(lldb) po results
<__NSArrayI 0x10f205c70>(
)
(lldb) po testID.entity
nil
The nil
entity
on the testID
is strange, but I'm not sure that's "bad."
So, I'm obviously at a loss as to why the fetch is coming back empty. The entity name is correct, the fetch and predicate look good, and the object is in the context, but still zero results.
Additional Context:
Essentially, the larger picture is that I have a background operation which uses its own Managed Object Context (MOC) to perform some work. The results of that work are needed on the main queue, so I package up the objectIDs resulting from the work and pass those to the main queue. On the main queue I need the actual managed objects back, so I'm trying to fetch them, by objectID, from the main queue's MOC. I realize I can use objectWithID:
or existingObjectWithID:error:
or even objectRegisteredForID:
on the MOC to get these objects, but each have their own special issues:
objectWithID:
might return an object which is bogus if the object is not in the context, and if it is in the context it will return a fault.existingObjectWithID:error:
is great, since we'll get back nil
(rather than a bogus object) but it too returns a fault.objectRegisteredForID:
will return nil
if the object is not in the context already.So, if I use either objectWithID:
or existingObjectWithID:error:
in a loop to get a bunch of objects, that's potentially n
trips to the persisted store to fault the objects, meaning performance is going to be potentially awful. If I use objectRegisteredForID:
I might not get the object at all if it happens to not already be in the main queue's MOC.
So, rather than try to iterate over the array, I expected a fetch request would limit the overhead of interacting with the persistance store and return all the objects I needed in one fell swoop.
Addition:
As an aside, the issue really feels like it has to do with the @"self IN %@"
predicate, since I can remove that predicate and fetch all objects of entityName
without issue. I have also tried @"objectID IN %@"
as the predicate with the same (zero) results.
Okay, so ready for a trip down the Core Data rabbit hole?
TL;DR
NSManagedObjectID
s whose persistent store coordinator is no longer in memory lose their NSEntityDescription
(entity
) and do not equate (isEqual:
returns NO
) to NSManagedObjectID
s from a different persistent store coordinator even though their URIRepresentation
is the same.
Down the Rabbit Hole
Sweet... here we go.
Remember, I'm gathering the array of objectIDs from a separate Managed Object Context (MOC) on a separate thread? Good. I didn't mention, however, that that MOC is using its own Persistent Store Coordinator (PSC) (pointing at the same file, of course). That PSC and MOC are short lived since once the work is done they are autoreleased, but while they are alive, I save off instances of NSManagedObjectID
into an NSArray
(which gets passed to a different thread).
Once received in the separate thread, the NSManagedObjectID
s have a nil
entity. This seems to be happening (educated guess here...) because the PSC from which these objectIDs came from is now no longer in memory and the NSManagedObjectID
must keep a week reference to the NSEntityDescription
(entity
) which must be held by the PSC. The nil
entity
seems to cause issues, as commenters suspected...
I can't be sure of the internals, but it would appear that without the entity
the NSManagedObjectID
is not equal to another NSManagedObjectID
with the same URIRepresentation
. Let me illustrate:
In the following code:
* objectID
is an NSManagedObjectID *
from a persistant store which is no longer in memory and whose entity property is nil
.
* managedObjectContext
is our MOC on our current thread (of course)
NSURL *URIRep = objectID.URIRepresentation;
NSManagedObjectID *newObjectID = [managedObjectContext.persistentStoreCoordinator managedObjectIDForURIRepresentation:URIRep];
If we look at this in the debugger... guess what:
(lldb) p [ojectID isEqual:newObjectID]
(bool) $0 = false
Even though the URIRepresentation
of these two objectIDs must be the same, the objects do not equate.
This is almost certainly why the predicate [NSPredicate predicateWithFormat:@"self IN %@", objectIDs]
is failing to match any objects and causing the fetch request to return zero objects.
Interestingly, however, in the following code, where testID
is an NSManagedObjectID *
from a persistant store which is no longer in memory and whose entity property is nil
, the object is retrieved from the MOC without issue:
NSManagedObject *obj = [managedObjectContext existingObjectWithID:testID error:&error];
Workaround
There are two things which I've verified as a workaround for this:
Use the same Persistent Store Coordinator (PSC) when communicating NSManagedObjectID
s between contexts (and make sure it stays resident in memory). If this is done, the NSManagedObjectID
s never lose their entity
and everything works as expected.
Instead of passing NSManagedObjectID
s around from context to context, use their URIRepresentation
. i.e. instead of passing an array of NSManagedObjectID
objects, pass an array of NSURL
s representing the URIRepresentation
of each NSManagedObjectID
. Once ready to perform a fetch with those objectIDs use the managedObjectIDForURIRepresentation:
message to get a NSManagedObjectID
from the current MOC's PSC.
Option 1 is simpler, but might have some disadvantages with concurrency timing and may be too restrictive if, say, you wanted to have a separate PSC for your operations which was read only, for instance.
In my (relatively limited) experience calling managedObjectIDForURIRepresentation:
it appears to be very performant, so converting URIRepresentation
NSURL
s to NSManagedObjectID
s doesn't seem to add that much overhead.
Thanks
Thanks for following along... I hope this helps explain the issue and workarounds clearly. I certainly feel like I earned a merit badge in Core Data object IDs and persistent store coordinators by digging through all this.
Cheers,
Levi
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With