How to make/use temporary NSManagedObjects?
Simple, common pattern I can't开发者_开发问答 find in Apple's docs:
- Load a coredata store
- Download new data, creating objects in memory
- Save some of the new data to the store (usually "only the new bits / bits that haven't changed")
Instead, I can find these alternatives, none of which are correct:
- Don't create objects in memory (well, this means throwing away everything good about objects. Writing your code using lots of NSDictionary's who serve no purpose except to workaround CoreData's failings. Not workable in general)
- Create objects, but then delete the ones you don't want (Apple suggest this in docs, but their Notifications go horribly wrong: those "deletes" show up when you try to save, even though they shouldn't / can't)
- Create objects in a secondary Context (Apple strongly implies this is correct, but apparently doesn't provide any way for you to move objects from the temp context to the real one, without doing the above (deleting objects you just created, then doing a save). That's not possible in general, because the objects often need to be hooked-up to references in the new context, and a save will fail)
Surely, it shouldn't be this difficult?
If I have to write all the code to manually deep copy an object (by iterating down all of its fields and data structures), why does CoreData exist in the first place? This is a basic feature that CD provides internally.
The only solution I've had working so far is option 2 (from apple's docs), with custom heuristics to "guess" when Apple is sending NSNotifications for objects that should never have been saved in the first place (but Apple sends notofications for anyway). That's a horrible hack.
EDIT: clarification:
I can't figure out how to get Apple's Notifications to be delivered correctly. Apple's code seems to convert insertions into "updates", and convert "temporary objects" into "deletes", etc. I can't listen for "new objects".
There are 3 common ways to handle this.
Create a temporary object context with the same persistent store as your "real" context, add your objects to this temporary context, and once you know which objects you want to keep, remove all the others from your temporary context and save the temporary context. When you save, you can update your "real" context by observing the NSManagedObjectContextDidSaveNotification notification and merge it into your "real" context (ala [realContext mergeChangesFromContextDidSaveNotification:notification]). See Mike Weller's answer here for details.
(If you're concerned about I/O, you could use an in-memory context, which has pros and cons.)
Instead of using an NSManagedObject, use an NSDictionary. Once you know which objects you want to keep, instantiate a new managed object and call [managedObject setValuesForKeysWithDictionary:temporaryObject] to copy the values from the temporary object into the managed object, then save your "real" context. If you have code that needs to work with NSManagedObjects and temporary objects (e.g., a table view), you'd write that code using key-value coding (aka valueForKey:, setValue:forKeyPath:).
Add an "isTemporary" attribute to your entity model (defaulting to NO). When you create a temporary object, set isTemporary to YES and insert the object into your "real" context. Once you know which objects you want to keep, change their isTemporary attribute to NO. Of course, you need to periodically delete these temporary objects, but that's easy to do (e.g., when that task is completed, on app exit, etc).
The advantages of #1 and #3 are that your objects live in the CoreData world -- e.g., they can be queried, they can participate in relationships, etc. The advantage of #2 is that it's light and fast, especially if you have lots of temporary objects.
It seems that option 3 is the best alternative.
EDIT: after using this extensively on iOS 4, I'd say "always use NSOperationQueue instead of performSelectorOnBackgroundThread". If you don't know how to use NSOpQ the easy way, google it, but it can be done in fewer than 3 lines of code, so it's only a small change from using performSel. It works much better with iOS4's new thread-scheduler.
Based on "how could I force this to work?", I came up with this approach:
- Original class MUST have a Context of its own. It MUST subscribe to listen to "changes" (via NSNotification) upon its private context.
- ONLY invoke the download methods using "performSelectorOnBackgroundThread" or similar (force them to go on a different thread)
- ALWAYS pass arguments to the above method call that are NOT NSManagedObjects and DO NOT refernece them (this is forced by using performSelector... anyway - but even if you're on the same thread, it screws up Apple's code later-on if you do it any other way)
- ALWAYS provide IDs for the "pre-existing" managedObjects that the new ones need to hook-up to
- ALWAYS create a new, temporary, NSManagedContext before you start the download, and:...
- ...ALWAYS register the original class you were running to listen (using NSNotifications) to the "save" of this "temporary" context
- Do the download, create the objects, delete ones you don't want
- ALWAYS then re-fetch (in the temporary context) the objects which you passed-in by ID, and hook them up to the newly-created objects
- Save the temporary context
- ORIGINAL class reacts to "context saved" by re-invoking the callback but on the main thread (if not already on main thread - [NSThread isMainThread])
- ORIGINAL class, as soon as it is executing on main thread, uses the "merge" method from Apple to merge the NSNotificaiton object into its own store
- ORIGINAL class reacts to "context objects changed" by processing the changes
HOWEVER ... this ALSO requires something that Apple's docs don't mention: never save references to any managed objects EXCEPT FOR a "root" object that has references to all the rest.
Otherwise, Apple's "merge" breaks, badly.
ALSO ... you may need to manually "stimulate" faulting to make this work; there's a few SO questions about that (I have no idea why Apple doesn't do this automatically - maybe they do, but if so I haven't found the magic option to make this happen yet).
I think there are some other caveats, too. I'll edit this later if I remember them.
NB: this sounds like a heck of a lot of code. Yes, but ... it turns out to be a lot LESS than trying to follow tortuous examples using manual copying of objects by dictionary etc.
Once you have this setup and working, it is conceptually very easy to follow. ALSO ... if you do all the above steps, Apple gets "most" of the NSNotifications correct. The remaining ones that appear incorrect (e.g. some deletions) are "as described in the documentation". They don't make sense to me, but at least that's how it's documented to work.
Your objects should have some unique identifier, like unique integer ID. This comes from outside Core Data and depends on your business logic. So when you receive a new object from outside, you check whether object with this ID exist already in Core Data: if yes, you edit the existing object; if no, you add the new object.
精彩评论