levigroker levigroker - 16 hours ago 2
Objective-C Question

Fetching array of NSManagedObjects by objectID returns empty array

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.

Answer

Okay, so ready for a trip down the Core Data rabbit hole?

TL;DR

NSManagedObjectIDs whose persistent store coordinator is no longer in memory lose their NSEntityDescription (entity) and do not equate (isEqual: returns NO) to NSManagedObjectIDs 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 NSManagedObjectIDs 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:

  1. Use the same Persistent Store Coordinator (PSC) when communicating NSManagedObjectIDs between contexts (and make sure it stays resident in memory). If this is done, the NSManagedObjectIDs never lose their entity and everything works as expected.

  2. Instead of passing NSManagedObjectIDs around from context to context, use their URIRepresentation. i.e. instead of passing an array of NSManagedObjectID objects, pass an array of NSURLs 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 NSURLs to NSManagedObjectIDs 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