【问题标题】:Batch size does not work when using parent/child contexts使用父/子上下文时,批量大小不起作用
【发布时间】:2012-07-06 15:51:15
【问题描述】:

我已经能够在我的应用程序中确认这一点,并且我创建了一个快速示例应用程序来确认这一点。这是设置:

您有两个托管对象上下文:

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

此设置的灵感来自 WWDC 视频,这表明我们可以通过将 masterMOC 设置为私有队列并将其绑定到持久存储来保存在后台线程上。如果您使用mainMOC 设置NSFetchedResultsController(它必须是mainMOC,因为它与UI 相关联),并设置fetchBatchSize,批量大小将被忽略,所有实体都出现故障立刻。我启用了 SQLite 调试注释,当滚动数千行(批量大小为 20)时,不会触发任何错误。

如果我做一个简单的调整,即将持久存储协调器绑定到 mainMOC 并使其成为根上下文(即,它不再是 master 的子级),那么批量大小就可以完美地工作,并且作为我滚动了数千行,触发了几个错误。

这是预期的行为吗?我错过了什么吗?

您可以下载示例项目here

【问题讨论】:

标签: iphone objective-c ios core-data


【解决方案1】:

文档中对嵌套上下文的讨论有限,它只出现在“iOS v5.0 的核心数据发行说明”和UIManagedDocument 中。关于获取和嵌套上下文的唯一评论是:

获取和保存操作由父上下文而不是协调器进行调解。

鉴于没有任何与嵌套上下文的批量获取功能相关的免责声明,我建议不要期望批量获取和嵌套上下文不兼容。但是,这似乎是这种情况,因为最基本的示例不起作用。 (见下面的测试代码).

这里还有一个公开的雷达提交描述了相同的问题:http://openradar.appspot.com/11235622,以及与 FetchedResultsControllers 和嵌套上下文有关的其他问题:Duplication of entity when change made by a child ManagedObjectContext is pushed (saved) to its parent

一个可能的部分解决方案是直接向同一个NSPersistentStoreCoordinator 添加一个额外的NSManagedObjectContextNSMainQueueConcurrencyType,其唯一目的是为NSFetchedResultsController 提供服务。然后,当用户选择项目时,ObjectID 可以返回给嵌套的子上下文,然后可以在嵌套的上下文中执行任何后续编辑。

这显然会降低使用嵌套上下文的好处,并且需要更频繁地保存以在嵌套上下文和NSFetchedResultsControllers 上下文之间进行同步。但是,根据应用程序的设计以及嵌套上下文与批量加载的相对优势,这可能很有用。 (参见下面的示例代码)

测试代码显示嵌套上下文中最简单的案例批量提取失败:

#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

示例代码显示了使用附加上下文来为 NSFetchedResultsController 提供批处理工作:

#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

【讨论】:

  • 是的,但我喜欢将持久存储附加到私有队列上下文的原因是能够在后台保存,这听起来很棒。关于如何恢复该功能的任何想法?
  • 所以建议保留大多数事情的现有设置,但只需使用额外的上下文来仅服务NSFetchedResultsController,即只读。您仍将在嵌套上下文中编辑对象并将它们保存在后台。我假设NSFetchedResultsControllers 主要用于填充UITableViews,当表格视图可见时您没有进行编辑,因此可以将选择的对象传递回嵌套上下文(使用它的objectID)进行编辑,然后通知保存时获取的结果控制器上下文(在后台)。
  • 在保存运行期间,您可能会看到任何更改出现在表格视图中之前的延迟。但是,如果这是一个问题,也许您可​​以将任何长时间运行的保存管理到不同的点。它更紧凑,但批处理被破坏了,所以我没有看到任何其他选择,除了等待 Apple 的修复。
  • 等一下,您可以将同一个持久存储协调器设置为多个上下文吗?
  • 是的。如果您想进行实验,上面的代码应该很容易运行。
【解决方案2】:

来自NSFetchRequest Class Reference

当执行 fetch 时,会评估整个请求并记录所有匹配对象的身份,但一次从持久存储中获取不超过 batchSize 对象的数据。执行请求返回的数组将是一个代理对象,可以根据需要透明地对批次进行故障处理。

因此,如果托管对象上下文未附加到持久存储,则没有可取的东西 - 因此您看到的行为至少与文档有些一致。 fetchLimit 可能适用于您描述的场景。我仍然会在 bugreporter.apple.com 上发射雷达。

【讨论】:

    猜你喜欢
    • 2014-02-12
    • 2015-10-14
    • 1970-01-01
    • 1970-01-01
    • 2012-08-04
    • 2022-12-12
    • 2014-08-13
    • 2013-08-01
    • 2015-02-06
    相关资源
    最近更新 更多