views:

57

answers:

2

Simple, common pattern I can't find in Apple's docs:

  1. Load a coredata store
  2. Download new data, creating objects in memory
  3. 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:

  1. 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)
  2. 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)
  3. 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".

A: 

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.

Jaanus
In general, you never know that data until AFTER you've created the object. This is standard OOP: alloc/init first, fill-in the remaining data second. For instance, XML parsing: you very very rarely know the unique ID until after you've parsed at least some of the object (e.g. look at RSS: if the ID were an attribute on the parent node, your suggestion would work; instead, the ID is embedded deep inside the child nodes).
Adam
Well... you are not downloading data directly in Core Data format. The pattern that I use myself is that I get data as JSON, and can parse that JSON into a NS* object like NSDictionary. I can get the ID from dictionary, and then decide whether to create a new Core Data object or associate this data with an existing object.
Jaanus
+1  A: 

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:

  1. Original class MUST have a Context of its own. It MUST subscribe to listen to "changes" (via NSNotification) upon its private context.
  2. ONLY invoke the download methods using "performSelectorOnBackgroundThread" or similar (force them to go on a different thread)
  3. 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)
  4. ALWAYS provide IDs for the "pre-existing" managedObjects that the new ones need to hook-up to
  5. ALWAYS create a new, temporary, NSManagedContext before you start the download, and:...
  6. ...ALWAYS register the original class you were running to listen (using NSNotifications) to the "save" of this "temporary" context
  7. Do the download, create the objects, delete ones you don't want
  8. 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
  9. Save the temporary context
  10. ORIGINAL class reacts to "context saved" by re-invoking the callback but on the main thread (if not already on main thread - [NSThread isMainThread])
  11. 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
  12. 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.

Adam
Modification: with OS 4, and in some rare cases with OS 3, this works badly, unless you use NSOperationQueue's instead of performSelectorInBackground (the threading is smoother using queues - on OS 3 it prevents the foreground thread from getting starved, on OS 4 I'm not sure what's happening yet, but it felt smoother)
Adam
ADDITIONALLY: some of my "merge" problems I had were because I hadn't discovered the (mostly undocumented) [NSManagedObject prepareForDeletion] method; in 99% of cases you *must* override that method on EVERY object in your application (Apple never mentions this in the docs) in order to get a working merge-notification from Apple. Otherwise, Apple deletes the data you need to process merges BEFORE delivering the notifcatio to you!
Adam