Problem in short
Since NSManagedObjectContext without persistent store coordinator doesn't support setFetchBatchSize selector, I've used a solution from this post and it works with certain issue, that I would like to resolve.
Here is the database scheme and Coredata structure with terms in brackets. Test application has two screens: master table with list of Chats and detail table with list of Messages. Master screen uses Main MOC in fetch controller for showing data in table and Worker MOC to create Chats and Messages. Detail screen uses Fetch MOC for showing data in table.
After I create a new Chat with Messages on master screen and save them with calling save on all MOCs in the hierarchy, I can't fetch Messages by a selected Chat in detail screen. All I got in console is: "CoreData: annotation: total fetch execution time: 0.0000s for 0 rows". it is possible to fetch this data after app restart.
It seems it has something to do with fault Messages in Fetch MOC having fault relations with Chats that have different objectID than Chats I have in Main MOC. Because when I fetch Chat object in Fetch MOC and then use it for finding Messages, everything is working fine.
I would appreciate it if someone could help me resolve this issue with Fetch MOC or maybe it is just fine to screw with all Object Graph concept and fetch data by my own ID fields instead of using relations.
Some code
Here is Coredata stack initialization which is done on didFinishLaunchingWithOptions:
- (void)initializeCoreDataStack
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"FaultsFetching" withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_managedObjectModel];
_writerMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_writerMOC setUndoManager:nil];
[_writerMOC setPersistentStoreCoordinator:_persistentStoreCoordinator];
_mainThreadMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainThreadMOC setUndoManager:nil];
[_mainThreadMOC setParentContext:_writerMOC];
_fetchMainThreadMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_fetchMainThreadMOC setUndoManager:nil];
[_fetchMainThreadMOC setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[_fetchMainThreadMOC setPersistentStoreCoordinator:_persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:_writerMOC];
NSURL *storeURL = [APP_DOC_DIR URLByAppendingPathComponent:@"FaultsFetching.sqlite"];
NSError *error = nil;
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
{
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
- (void)backgroundContextDidSave:(NSNotification *)notification
{
[_fetchMainThreadMOC mergeChangesFromContextDidSaveNotification:notification];
NSLog(@"Yep, everything is merged");
}
Here is how I create Worker MOCs:
+ (NSManagedObjectContext *)createPrivateMOC
{
CoreDataManager *scope = [CoreDataManager sharedInstance];
NSManagedObjectContext *workerMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
workerMOC.parentContext = scope.mainThreadMOC;
[workerMOC setUndoManager:nil];
workerMOC.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
return workerMOC;
}
Here is how Multi-Context save looks like. Argument async is YES. Naturally this selector is called within performBlock selector of a worker MOC
+ (void)writeToDiskAsync:(BOOL)async
{
CoreDataManager *scope = [CoreDataManager sharedInstance];
NSManagedObjectContext *writeManagedObjectContext = scope.writerMOC;
NSManagedObjectContext *mainManagedObjectContext = scope.mainThreadMOC;
PerformBlock mainMOCBlock = ^
{
NSError *mainError = nil;
if ([mainManagedObjectContext hasChanges] && ![mainManagedObjectContext save:&mainError])
{
ALog(@"Unresolved error %@, %@", mainError, [mainError userInfo]);
}
PerformBlock writerBlock = ^
{
NSError *writeError = nil;
if ([writeManagedObjectContext hasChanges] && ![writeManagedObjectContext save:&writeError])
{
ALog(@"Unresolved error %@, %@", writeError, [writeError userInfo]);
}
NSLog(@"Yep, everything is saved");
};
[scope performBlock:writerBlock onMOC:writeManagedObjectContext async:async];
};
[scope performBlock:mainMOCBlock onMOC:mainManagedObjectContext async:async];
}
- (void)performBlock:(PerformBlock)block onMOC:(NSManagedObjectContext *)target async:(BOOL)async
{
if (async)
[target performBlock:block];
else
[target performBlockAndWait:block];
}
Here is my fetch results controller on detail screen, where "detailItem" is a Chat entity set from master screen and "[CoreDataManager sharedInstance]" is a singleton:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
if (self.detailItem == nil)
return nil;
NSManagedObjectContext *fetchMOC = [CoreDataManager sharedInstance].fetchMainThreadMOC;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Messages" inManagedObjectContext:fetchMOC];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"sentDate" ascending:NO];
[fetchRequest setSortDescriptors:@[sortDescriptor]];
NSPredicate *chatPredicate = [NSPredicate predicateWithFormat:@"relatedChat=%@", self.detailItem.objectID];
[fetchRequest setPredicate:chatPredicate];
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:fetchMOC sectionNameKeyPath:@"sectionIdentifier" cacheName:nil];
_fetchedResultsController.delegate = self;
NSError *error = nil;
if (![_fetchedResultsController performFetch:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
A bit of background
This is due to a bug. The workaround is to call obtainPermanentIDsForObjects:
before saving the newly inserted objects.
See the following SO issue for more details:
Core Data: Do child contexts ever get permanent objectIDs for newly inserted objects?
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