Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Batch size does not work when using parent/child contexts

I've been able to confirm this on my application, and a quick sample application I created to confirm this. Here's the setup:

You have two managed object contexts:

masterMOC: NSPrivateQueueConcurrencyType, tied to persistent store coordinator
mainMOC: NSMainQueueConcurrencyType, child of masterMOC, NOT tied to any store coordinator

This setup was inspired from the WWDC video, which suggests we can save on a background thread by setting the masterMOC to a private queue and tie it to the persistent store. If you set up an NSFetchedResultsController using the mainMOC (and it must be the mainMOC since that's the one tied to the UI), and set a fetchBatchSize, the batch size is disregarded and all entities are faulted in at once. I enabled the SQLite debug annotations, and when scrolling through thousands of rows (with a batch size of 20), no faults are fired what so ever.

If I make one simple adjustment, namely tie the persistent store coordinator to the mainMOC and make it a root context (that is, it is no longer a child of master), then the batch size works perfectly, and as I scroll through thousands of rows, several faults are fired.

Is this expected behavior? Am I missing something?

You can download the sample project here

like image 776
Snowman Avatar asked Jul 06 '12 15:07

Snowman


1 Answers

There is limited discussion of nested contexts in the documentation, it only appears in "Core Data Release Notes for iOS v5.0", and in UIManagedDocument. The only comment on fetching and nested contexts is:

fetch and save operations are mediated by the parent context instead of a coordinator.

Given the lack of any disclaimers relating to the functionality of batch fetching with nested contexts, I would suggest it is not expected that batch fetching and nested contexts are incompatible. However this seems to be the case as the most basic example does not function. (See test code below).

There is also an open radar submission describing the same problem here: http://openradar.appspot.com/11235622, and other problems noted with FetchedResultsControllers and nested contexts: Duplication of entity when change made by a child ManagedObjectContext is pushed (saved) to its parent.

A possible partial solution could be to add an additional NSManagedObjectContext of NSMainQueueConcurrencyType directly to the same NSPersistentStoreCoordinator for the sole purpose of serving the NSFetchedResultsController. ObjectIDs could then be handed back to the nested child context when the user selects items, and any subsequent editing can then be performed in the nested contexts.

This obviously reduces the benefit of using nested contexts and would require more frequent saving to synchronise between the nested contexts and the NSFetchedResultsControllers context. However depending on the design of the application and the relative benefits of nested contexts vs batch loading this may be useful. (See example code below)

Test code showing failure of simplest case batch fetching in nested contexts:

#import "AppDelegate.h"

// Xcode 4.3.3:
// Create a new iOS Master-Detail project called "BatchTest" tick the "Use Core Data" check box.
// Delete all files except the AppDelegate and the BatchTest data model (leave supporting files).
// Delete all properties and methods from AppDelegate.h
// Paste this code into AppDelegate.m
// Switch on core data debugging by editing the "BatchTest" scheme and adding 
//   -com.apple.CoreData.SQLDebug 1
// To the "arguments passed on launch" list in the "Run" step
// Run.

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    /////////////////////////////////////////////////////////////////////////////////////
    // Setup the core data stack.
    /////////////////////////////////////////////////////////////////////////////////////
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"BatchTest" withExtension:@"momd"];
    NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

    NSURL *appDocsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    NSURL *storeURL = [appDocsDirectory URLByAppendingPathComponent:@"BatchTest.sqlite"];

    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:nil];

    NSManagedObjectContext *parentContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    parentContext.persistentStoreCoordinator = coordinator;

    NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    childContext.parentContext = parentContext;


    /////////////////////////////////////////////////////////////////////////////////////
    // Load some test data and reset the context.
    /////////////////////////////////////////////////////////////////////////////////////
    [parentContext performBlockAndWait:^{
        for (int i=0; i<1000; i++) {
            [NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:parentContext];
        }
        [parentContext save:nil];
        [parentContext reset];
    }];


    /////////////////////////////////////////////////////////////////////////////////////
    // Test Batched Fetching
    /////////////////////////////////////////////////////////////////////////////////////

    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Event"];
    request.fetchBatchSize = 10;

    // Fetch from the child.
    NSArray *results = [childContext executeFetchRequest:request error:nil];
    NSLog(@"Object 500: %@", [results objectAtIndex:500]);
    // Result is all 1000 rows fetched in full, no subsequent batch fetching for event 500.

    [childContext reset];    
    [parentContext performBlockAndWait:^{
        [parentContext reset];

        // Fetch from the parent.
        NSArray *results = [parentContext executeFetchRequest:request error:nil];
        NSLog(@"Object 500: %@", [results objectAtIndex:500]);
        // Result is 1000 primary keys fetched, followed by a batch of 10 rows to find event 500.

    }];

    return YES;
}

@end

Example code showing use of an additional context to serve an NSFetchedResultsController with batching working:

#import "AppDelegate.h"

// Xcode 4.3.3:
// Create a new iOS Master-Detail project called "BatchTest" tick the "Use Core Data" check box.
// Delete all files except the AppDelegate and the BatchTest data model (leave supporting files).
// Delete all properties and methods from AppDelegate.h
// Paste this code into AppDelegate.m
// Switch on core data debugging by editing the "BatchTest" scheme and adding 
//   -com.apple.CoreData.SQLDebug 1
// To the "arguments passed on launch" list in the "Run" step
// Run.

@interface AppDelegate () {
    NSManagedObjectContext *backgroundContext;
    NSManagedObjectContext *editingContext;
    NSManagedObjectContext *fetchedResultsControllerContext;
    NSManagedObject *selectedObject;
}

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    /////////////////////////////////////////////////////////////////////////////////////
    // Setup the core data stack.
    /////////////////////////////////////////////////////////////////////////////////////
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"BatchTest" withExtension:@"momd"];
    NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

    NSURL *appDocsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    NSURL *storeURL = [appDocsDirectory URLByAppendingPathComponent:@"BatchTest.sqlite"];

    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:nil];

    backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    backgroundContext.persistentStoreCoordinator = coordinator;

    editingContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    editingContext.parentContext = backgroundContext;

    fetchedResultsControllerContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    fetchedResultsControllerContext.persistentStoreCoordinator = coordinator;

    /////////////////////////////////////////////////////////////////////////////////////
    // Load some test data and reset the context.
    /////////////////////////////////////////////////////////////////////////////////////
    [backgroundContext performBlockAndWait:^{
        for (int i=0; i<1000; i++) {
            [NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:backgroundContext];
        }
        [backgroundContext save:nil];
        [backgroundContext reset];
    }];

    /////////////////////////////////////////////////////////////////////////////////////
    // Example of three contexts performing different roles.
    /////////////////////////////////////////////////////////////////////////////////////

    // The fetchedResultsControllerContext will batch correctly as it is tied directly 
    // to the persistent store.  It can be used to drive the UI as it is a Main Queue context. 
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Event"];
    request.fetchBatchSize = 10;
    NSArray *fetchResults = [fetchedResultsControllerContext executeFetchRequest:request error:nil];

    // User selects an object in the fetchedResultsControllerContext (i.e. in a UITableView).
    selectedObject = [fetchResults lastObject];
    NSLog(@"**** selectedObject.timeStamp before editing:%@", [selectedObject valueForKey:@"timeStamp"]);

    // Pass the object to the editing context for editing using its objectID.
    NSManagedObjectID *selectedObjectID = selectedObject.objectID;
    NSManagedObject *objectForEditing = [editingContext objectWithID:selectedObjectID];

    // Edit the object
    [objectForEditing setValue:[NSDate date] forKey:@"timeStamp"];

    // Subscribe to save notifications of the background context so the 
    // fetchedResultsControllerContext will be updated after the background save occurs.
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(backgroundContextDidSave:) 
                                                 name:NSManagedObjectContextDidSaveNotification 
                                               object:backgroundContext];

    // Save the editing context to push changes up to the parent, then background save.
    [editingContext save:nil];
    [backgroundContext performBlock:^{
        [backgroundContext save:nil];
    }];

    return YES;
}

- (void)backgroundContextDidSave:(NSNotification *)notification {

    [fetchedResultsControllerContext mergeChangesFromContextDidSaveNotification:notification];
    NSLog(@"**** selectedObject.timeStamp after editing:%@", [selectedObject valueForKey:@"timeStamp"]);
    // Merging changes into the fetchedResultsControllerContext would trigger updates
    // to an NSFetchedResultsController and it's UITableView where these set up.

}

@end
like image 167
Rory O'Bryan Avatar answered Oct 05 '22 23:10

Rory O'Bryan