Enable saving NSManagedObjectContext document immediately?

Starting with the standard Xcode Document-based Application w / CoreData template 10.7, I experience some frustration. I am sure this is something simple that I am missing.

Let's say in my subclass of NSPersistentDocument I have something like this by connecting to a button in a window:

- (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]; } 

If I create a new document and click on this button, I will get the following error: This NSPersistentStoreCoordinator has no persistent stores. It cannot perform a save operation. This NSPersistentStoreCoordinator has no persistent stores. It cannot perform a save operation. I get it. We have not yet saved, there are no permanent stores. Has the meaning.

Now let's say I divided this into two actions by connecting to various buttons, for example:

 - (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]; } 

If I create a new document and press the first button, then at some indefinite time after clicking this button (document pollution), autosave and autosave of the document will automatically enter, which will create a repository in a temporary location. If I then press the second button, there are no complaints (since there is now a store.)

I need my document to be able to manage managedObjectContext from the start. I run some things in the background thread, and I need the operation to save the background context (and notification) in order to merge the changes made by the background thread into the main managedObjectContext stream.

I was thinking about trying to force autosave, but the autosave process looks completely asynchronous, so I would have to jump through the hoops to disable any user interface interaction that could lead to the saving of managed objects until the first autosave operation completes.

I also thought about creating a storage in memory in order to bridge the gap between creating a new document and the first autosave, but then it’s not clear to me how I will transfer this to the storage in the disk storage and delete the storage in synchronization with the first autosave.

Anyone have any ideas on how I can handle this?

+4
source share
4 answers

So I cheated on this for a while, including trying to @Aderstedt's suggestion. This approach did not work, because the fake notification, it seems, just tells the receiving context “hey, check with the permanent stores, I updated them!”, When in fact I did not, because they are not. In the end, I found an approach that worked. Unfortunately, it relies only on Lion functions, so I'm still looking for a way to do this, which does not require Lion.

Background

I wanted to work with the NSPersistentDocument approach. Although I have not found this documented anywhere, I found several forum posts and experienced a bunch of empirical evidence that you cannot call -[NSManagedObjectContext save:] in a context related to NSPersistentDocument. As mentioned in the question, if you call this before the document has ever been saved, it will not have stores, so a failure will occur. Even after the repository exists, preserving the context directly (and not through the document preservation API), you actually change the view on the disk behind the NSPersistentDocument, and you get a document that spreads sheets that say:

The file was modified by another application.

In short, an NSPersistentDocument expects control over the save action of the associated NSManagedObjectContext itself.

The front is also worth mentioning: the goal here was to make sure that the context used in the user interface would not cause a single (or at least minimal) I / O to remain responsive. In the end, I determined that I should have 3 contexts. One context, owned by NSPersistentDocument, that will be responsible for performing file I / O in conjunction with the document. A second, read-only, context-friendly user interface binding. (I understand that many people want the user interface to distort the model, so it might be less exciting for them, but it wasn’t a requirement for me.) And the third context is for use in a background thread that asynchronously downloads data from the Internet to a service and I hope it pushes it into other contexts, so that it can be saved to disk and presented in the user interface without potentially blocking the user interface during network input / output.

Lion-only solution

The new parent / child function NSManagedObjectContext in the Lion CoreData implementation is ideal for this. I replaced NSPersistentDocument NSManagedObjectContext with a new MOC concurrency of type NSPrivateQueueConcurrencyType. This will be the “root” context. Then I created a UI context with the NSMainQueueConcurrencyType concurrency and made it a child of the root context. Finally, I made the network boot context the NSPrivateQueueConcurrencyType context, which is a child of the user interface context. How it works, we run the network boot operation in the background, it updates the network context. When this is done, it will keep the context. With parent / child relationships, saving the child context transfers the changes to the parent context (user interface context), but does not save the parent context in the repository. In my case, I also listen to the NSManagedObjectContextDidSaveNotification notification from the network context, and then also tell him that it also saves it (which will lead to changes from the user interface context in the root / disk context, but will not save it to disk).

At the end of this chain of events, all contexts are consistent, and we still did not force the main root context to be saved, and therefore we did not encounter NSPersistentDocument in its management role on disk.

One of the tricks is that if you want to prevent the saving of child contexts due to the cancellation of the cancellation (i.e. it was a network boot operation, nothing was canceled there), you need to disable the account in each parent context when propagating the changes in the chain.

Efforts to the Lion

I would really like to find a solution compatible with pre-Lion for this problem. I tried a few things before giving up. First, I tried to associate the in-memory storage with the PSC in the init document so that I could execute the NSManagedObjectContext, saving before saving the document, and then transfer the storage to the memory the first time it was saved. This part worked just fine. But as soon as the storage on disk existed, this approach was fictitious, because after saving it to disk we have the same problem when any saving of MOCs connected to the PSC belonging to NSPsistentDocument must be performed by a document.

I also tried to crack the mechanism for moving changes from one context to another using the NSManagedObjectContextObjectsDidChangeNotification payload. Although I was able to get this to work (for some nominal definition of “work”), I saw big problems arising on the horizon with this approach. In particular, it’s easy to transfer these changes once, but what if it changes again before the save operation? Then I would be stuck keeping a long-running OID mapping in the source context to the OID in the destination context (s). It was awfully fast. If anyone is interested, here is what I came up with:

 @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 

Conclusion

Between the improvements in concurrency management and this parent / child function, I quickly lost interest in the pre-Lyon solution. I am starting to realize that the pre-Lion solution will effectively "Do not use NSPersistentDocument". As far as I can tell, all these pain points go away if I refuse this requirement. Without this, you can save contexts and transfer stores whenever you want, but, of course, you have to do it all yourself.

+6
source

The problem is that by default, the NSManagedObjectContext created by the NSPersistentDocument has a concurrency type of NSConfinementConcurrencyType. CoreData does not allow you to create a child context context with this type.

As a workaround this works for me. An NSManagedObjectContext is created by your NSPersistentDocument, so you can override this method:

 - (NSManagedObjectContext *)managedObjectContext { if (!_context) { NSManagedObjectContext *_default = [super managedObjectContext]; _context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; _context.persistentStoreCoordinator = _default.persistentStoreCoordinator; } return _context; } 

I had no idea where to get persistentStoreCoordinator, so I called a super implementation and got it there. In this context, you should be able to create a child context that can be used in background operations.

+2
source

If you do not have a store, you cannot save it. It seems that you want to save the document to combine the changes made in the background thread; well, you can merge these changes manually. When the background thread completes, tell the main thread which objects have been updated / inserted, and then make the same changes to the main thread.

If the changes are close to arbitrary and therefore tedious for duplication, you can even create your own NSManagedObjectContextDidSaveNotification in the background thread and then merge it using [NSManagedObjectContext mergeChangesFromContextDidSaveNotification:] in the main thread.

+1
source

I found a very beautifully written solution according to the ipmcc solution (also executing the NSPersistentDocument MOC in the background thread and creating its parent context for the main MOC thread) here.

It includes a full BSD license code for a document application based on this (mainly the iOS version of UIManagedDocument for Mac OS).

0
source

Source: https://habr.com/ru/post/1394300/


All Articles