从10.7上的标准Xcode基于文档的应用程序w / CoreData模板开始,我遇到了一些令人沮丧的行为。我确信这是一件很简单的事,我忽视了。
让我们说在我的NSPersistentDocument子类中,我有这样的东西,连接到窗口中的一个按钮:
- (IBAction)doStuff:(id)sender
{
NSEntityDescription* ent = [[self.managedObjectModel entitiesByName] valueForKey: @"MyEntity"];
NSManagedObject* foo = [[[NSManagedObject alloc] initWithEntity: ent insertIntoManagedObjectContext: self.managedObjectContext] autorelease];
[self.managedObjectContext save: NULL];
}
如果我创建一个新文档并单击该按钮,我将收到以下错误:This NSPersistentStoreCoordinator has no persistent stores. It cannot perform a save operation.
我明白了。我们还没有保存,没有持久的商店。说得通。
现在让我说我把它分成两个动作,连接到不同的按钮,如下:
- (IBAction)doStuff:(id)sender
{
NSEntityDescription* ent = [[self.managedObjectModel entitiesByName] valueForKey: @"MyEntity"];
NSManagedObject* foo = [[[NSManagedObject alloc] initWithEntity: ent insertIntoManagedObjectContext: self.managedObjectContext] autorelease];
}
- (IBAction)doOtherStuff:(id)sender
{
[self.managedObjectContext save: NULL];
}
如果我创建一个新文档并按下第一个按钮,那么在按下该按钮后会在某个不确定的时间(污染文档),自动保存将自动保存并自动保存文档,这将在临时位置创建一个商店。如果我按下第二个按钮,则没有任何抱怨(因为现在有一个商店。)
我需要我的文档能够从一开始就进行managedObjectContext保存。我在后台线程上踢了一些东西,我需要后台上下文的保存操作(和通知),以便将后台线程所做的更改合并到主线程的managedObjectContext中。
我想过尝试强制自动保存,但是自动保存过程看起来完全异步,所以我必须跳过箍来禁用任何可能导致managedObjectContext保存的UI交互,直到第一次自动保存操作完成。
我还考虑创建一个内存存储来弥补创建新文档和第一次自动保存之间的差距,但是我不清楚如何将内存存储中的内容迁移到磁盘存储并删除内存存储与第一次自动保存操作同步。
任何人对我如何处理这个问题都有任何想法?
答案 0 :(得分:6)
所以我愚弄了一段时间,包括尝试@ Aderstedt的建议。这种方法不起作用,因为伪造通知似乎只是告诉接收上下文"嘿,检查持久性商店,我已经更新了它们!",当实际上,我没有,因为没有。我最终找到了一种有效的方法。不幸的是,它依赖于Lion-only功能,所以我仍然在寻找一种不需要Lion的方法。
我想使用NSPersistentDocument方法。虽然我在任何地方都没有明确记录这一点,但我发现了一些论坛帖子,并且经历了一系列经验证据,证明你不能在属于NSPersistentDocument的上下文中调用-[NSManagedObjectContext save:]
。如问题中所述,如果您在保存文档之前调用它,则它将 no 存储,因此保存将失败。即使在商店存在之后,通过直接保存上下文(而不是通过文档保存API),您实际上正在更改NSPersistentDocument后面的磁盘表示,并且您将获得文档弹出表,其中说明: / p>
文件已被其他应用程序修改
简而言之,NSPersistentDocument希望控制相关NSManagedObjectContext本身的保存操作。
前面也值得一提:这里的目标是确保UI使用的上下文不会触发(或至少是最小的)I / O以保持响应。我最终确定的模式是有3个上下文。 NSPersistentDocument拥有的一个上下文,它负责与文档一起执行文件I / O.用于绑定UI的第二个有效只读的上下文。 (我意识到很多人都希望用户界面改变模型,所以这对他们来说可能不那么令人兴奋,但对我来说这并不是必需的。)还有第三个用于后台线程的上下文,它异步加载来自a的数据。 Web服务,并希望将其推送到其他上下文中,以便它可以保存在磁盘上并在UI中显示,而不会阻止网络I / O上的UI。
Lion的CoreData实现中的新父/子NSManagedObjectContext功能为完美。我用一个新的并发类型为NSPrivateQueueConcurrencyType的MOC替换了NSPersistentDocument的NSManagedObjectContext。这将是" root"上下文。然后我使用NSMainQueueConcurrencyType并发创建了UI上下文,并使其成为根上下文的子项。最后,我将网络加载上下文设置为NSPrivateQueueConcurrencyType上下文,该上下文是UI上下文的子级。这种方式的工作方式是我们在后台启动网络加载操作,它更新网络上下文。完成后,它会保存上下文。通过父/子关系,保存子上下文会将更改推送到父上下文(UI上下文),但不将父上下文保存到商店。在我的例子中,我还从网络上下文中侦听NSManagedObjectContextDidSaveNotification通知,然后告诉它的父节点也是如此(它会将更改从UI上下文推送到根/磁盘上下文,但不会保存它到磁盘。)
在这一系列事件结束时,所有情境都是一致的,我们仍然没有强制真正保存潜在的根上下文,因此我们不会在其中与NSPersistentDocument发生冲突管理磁盘表示的角色。
一个问题是,如果你想阻止子上下文的保存生成撤销(即这是一个网络加载操作,没有什么可以撤消)你必须在传播更改时禁用每个父上下文的撤销注册上链。
我真的很想找到针对这个问题的狮子座前兼容解决方案。在放弃之前,我尝试了一些事情。我首先尝试将内存存储与文档init上的PSC相关联,这样我就可以在保存文档之前执行NSManagedObjectContext保存,然后在第一次保存时迁移内存存储。那部分很有效。但是一旦存在磁盘存储,这种方法就是假的,因为在将其保存到磁盘后,我们遇到了同样的问题,即NSPersistentDocument拥有的与PSC连接的MOC的任何保存必须由文档完成。
我还试图破解一种机制,使用NSManagedObjectContextObjectsDidChangeNotification有效负载将更改从一个上下文移动到另一个上下文。虽然我能够实现这一点(对于工作的一些名义定义),我看到这种方法即将出现大问题。具体来说,将一次更改很容易,但是如果它在保存操作之前再次更改会怎么样?然后,我不得不在源上下文中将OID的长期映射维护到目标上下文中的OID。这很快就变丑了。如果有人感兴趣,请按照我的想法点击:
@interface NSManagedObjectContext (MergeChangesFromObjectsDidChangeNotification)
- (void)mergeChangesFromObjectsDidChangeNotification: (NSNotification*)notification;
@end
@implementation NSManagedObjectContext (MergeChangesFromObjectsDidChangeNotification)
- (void)mergeChangesFromObjectsDidChangeNotification: (NSNotification*)notification
{
if (![NSManagedObjectContextObjectsDidChangeNotification isEqual: notification.name])
return;
if (notification.object == self)
return;
NSManagedObjectContext* sourceContext = (NSManagedObjectContext*)notification.object;
NSAssert(self.persistentStoreCoordinator == sourceContext.persistentStoreCoordinator, @"Can't merge changes between MOCs with different persistent store coordinators.");
[sourceContext lock];
// Create object in the local context to correspond to inserted objects...
NSMutableDictionary* foreignOIDsToLocalOIDs = [NSMutableDictionary dictionary];
for (NSManagedObject* foreignMO in [[notification userInfo] objectForKey: NSInsertedObjectsKey])
{
NSManagedObjectID* foreignOID = foreignMO.objectID;
NSManagedObject* localMO = [[[NSManagedObject alloc] initWithEntity: foreignMO.entity insertIntoManagedObjectContext: self] autorelease];
[foreignOIDsToLocalOIDs setObject: localMO.objectID forKey: foreignOID];
}
// Bring over all the attributes and relationships...
NSMutableSet* insertedOrUpdated = [NSMutableSet set];
[insertedOrUpdated unionSet: [[notification userInfo] objectForKey: NSInsertedObjectsKey]];
[insertedOrUpdated unionSet: [[notification userInfo] objectForKey: NSUpdatedObjectsKey]];
for (NSManagedObject* foreignMO in insertedOrUpdated)
{
NSManagedObjectID* foreignOID = foreignMO.objectID;
NSManagedObjectID* localOID = [foreignOIDsToLocalOIDs objectForKey: foreignOID];
localOID = localOID ? localOID : foreignOID;
NSManagedObject* localMO = [self objectWithID: localOID];
// Do the attributes.
[localMO setValuesForKeysWithDictionary: [foreignMO dictionaryWithValuesForKeys: [[foreignMO.entity attributesByName] allKeys]]];
// Do the relationships.
NSDictionary* rByName = foreignMO.entity.relationshipsByName;
for (NSString* key in [rByName allKeys])
{
NSRelationshipDescription* desc = [rByName objectForKey: key];
if (!desc.isToMany)
{
NSManagedObject* relatedForeignMO = [foreignMO valueForKey: key];
NSManagedObjectID* relatedForeignOID = relatedForeignMO.objectID;
NSManagedObjectID* relatedLocalOID = [foreignOIDsToLocalOIDs objectForKey: relatedForeignOID];
relatedLocalOID = relatedLocalOID ? relatedLocalOID : relatedForeignOID;
NSManagedObject* localRelatedMO = [self objectWithID: relatedLocalOID];
[localMO setValue: localRelatedMO forKey: key];
}
else
{
id collection = [foreignMO valueForKey: key];
id newCollection = [NSMutableSet set];
if ([collection isKindOfClass: [NSOrderedSet class]])
{
newCollection = [NSOrderedSet orderedSet];
}
for (NSManagedObject* relatedForeignMO in collection)
{
NSManagedObjectID* relatedForeignOID = relatedForeignMO.objectID;
NSManagedObjectID* relatedLocalOID = [foreignOIDsToLocalOIDs objectForKey: relatedForeignOID];
relatedLocalOID = relatedLocalOID ? relatedLocalOID : relatedForeignOID;
NSManagedObject* localRelatedMO = [self objectWithID: relatedLocalOID];
[newCollection addObject: localRelatedMO];
}
[localMO setValue: newCollection forKey: key];
}
}
}
// And delete any objects which pre-existed in my context.
for (NSManagedObject* foreignMO in [[notification userInfo] objectForKey: NSDeletedObjectsKey])
{
NSManagedObjectID* foreignOID = foreignMO.objectID;
NSManagedObject* localMO = [self existingObjectWithID: foreignOID error: NULL];
if (localMO)
{
[self deleteObject: localMO];
}
}
[sourceContext unlock];
}
@end
在并发管理和这个父/子功能的改进之间,我很快就失去了追求前狮子解决方案的兴趣。我开始认为,狮子会之前的解决方案实际上是“不要使用NSPersistentDocument”。"我可以说,如果我放弃这个要求,所有这些痛点都会消失。没有它,你可以随时保存上下文并迁移商店,但自然你必须自己完成所有工作。
答案 1 :(得分:2)
这里的问题是NSPersistentDocument创建的默认NSManagedObjectContext是并发类型NSConfinementConcurrencyType。 CoreData不允许使用该类型创建上下文的子上下文。
作为一种解决方法,这对我有用。 NSManagedObjectContext由NSPersistentDocument创建,因此您可以覆盖该方法:
- (NSManagedObjectContext *)managedObjectContext {
if (!_context) {
NSManagedObjectContext *_default = [super managedObjectContext];
_context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_context.persistentStoreCoordinator = _default.persistentStoreCoordinator;
}
return _context;
}
我不知道从哪里获取persistentStoreCoordinator,所以我调用了超级实现并从中获取。使用此上下文,您应该能够创建可在后台操作中使用的子上下文。
答案 2 :(得分:1)
如果您没有商店,则无法保存。您似乎要保存文档以合并在后台线程上所做的更改;好吧,您可以手动合并这些更改。后台线程完成后,告诉主线程哪些对象已更新/插入,然后在主线程上进行相同的更改。
如果更改接近任意且因此很复杂,您甚至可以在后台线程上构建自己的NSManagedObjectContextDidSaveNotification,然后使用主线程上的[NSManagedObjectContext mergeChangesFromContextDidSaveNotification:]合并它。
答案 3 :(得分:0)
我找到了一个非常好的写作解决方案,与ipmcc的解决方案一致(在后台线程中运行NSPersistentDocument
的MOC,并使其成为主线程MOC的父上下文){{3 }}
它包含基于该文档应用程序的完整BSD许可代码(基本上是iOS的UIManagedDocument
的Mac OS版本。)