Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multi-Context CoreData with batch fetch by relationship

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: http://i.imgur.com/17tuKS7.png
  • Something else I might add later, also sorry for links and images: reputation and silly stuff
like image 493
Emil Avatar asked Nov 12 '22 19:11

Emil


1 Answers

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?

like image 97
Jesse Avatar answered Nov 15 '22 05:11

Jesse