5

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.

enter image description here

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

  • Parent/child MOCs were used to improve stability and responsiveness in the app that wasn't properly written from the beginning. However, because now everything related to Coredata is more or less centralized, it is possible to change the stack to something different.
  • SectionIdentifier is used for grouping messages by day like this: https://i.stack.imgur.com/KvlDl.png
  • Something else I might add later, also sorry for links and images: reputation and silly stuff
Community
  • 1
  • 1
Emil
  • 137
  • 1
  • 12
  • Do you use any of the worker MOCs in threads? – AntonijoDev Dec 24 '13 at 08:30
  • I use worker MOC in Master screen on button touch up in IBAction selector to create Chat and its Messages. I use performBlock selector on worker MOC – Emil Dec 24 '13 at 09:04
  • I can see you are creating all of your MOCs in same thread, so I was wondering if u use any of them in other threads cause in case you are, you would have problems with saving and fetching. – AntonijoDev Dec 24 '13 at 15:29
  • That shouldn't be a problem since I'm saving all data in a sequence of performBlock calls, I'll add this code to the question. – Emil Dec 25 '13 at 07:06
  • That and also for fetching I'm using NSMainQueueConcurrencyType MOCs which are unique across the app, when worker MOCs are usually for creating/deleting stuff and later they get disposed. I'll make sure that all calls to MOCs are made through performBlock selector. – Emil Dec 25 '13 at 07:12
  • 1
    Could it be related to this issue? http://stackoverflow.com/questions/11990279/core-data-do-child-contexts-ever-get-permanent-objectids-for-newly-inserted-obj. Does it make a difference if you call obtainPermanentIDsForObjects: before saving newly inserted objects? – Jesse Jan 15 '14 at 02:27
  • @Jesse It actually does make a difference, obtainPermanentIDsForObjects before save resolved this problem. Where should we go now: will you create an answer or it is possible to promote comment to an answer? – Emil Jan 20 '14 at 11:27
  • @Emil Great, I'm glad it resolved the issue. I added it as an answer. – Jesse Jan 20 '14 at 18:55

1 Answers1

1

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?

Community
  • 1
  • 1
Jesse
  • 1,667
  • 12
  • 16
  • I'm curious if this obtainPermanentIDsForObjects: is an expensive operation and if I should call it for all inserted objects – Emil Jan 21 '14 at 07:02
  • It sounds like it's only necessary in certain cases before saving inserted objects in a child context. It does require communicating with the store and will therefore cause some cycles on the main thread. I would use it in the cases where you are seeing issues. – Jesse Jan 21 '14 at 23:09