Search code examples
iosmultithreadingmagicalrecord

Magical Record with concurrency


I'm in the middle of development of an iOS application after working quite some time with Core Data and Magical Record and having an error:

error: NULL _cd_rawData but the object is not being turned into a fault

I didn't know Core Data before this project and as it turns out I was very naive to think I can just work with Magical Record without worrying about concurrency, as I haven't dedicated any thoughts/work about managed contexts for the main thread and background threads.

After A LOT of reading about Core Data Managed Object Contexts and Magical Record, I understand that:

  • NSManagedObjects are not thread safe.
  • NSManagedObjectId IS thread safe.
  • I can use: Entity *localEntity = [entity MR_inContext:localContext] of Magical Record to work with an entity in a background thread's context.
  • I should use Magical Record's saveWithBlock:completion: and saveWithBlockAndWait: methods to get a managed context to use for background threads.

A little information regarding my application:

  • I'm using the latest version of Magical Record which is 2.2.
  • I have a backend server which my application talks to a lot.
  • Their communication is similar to Whatsapp, as it uses background threads for communicating with the server and updating managed objects upon successful responses.
  • I'm wrapping the model with DataModel objects that hold the managed objects in arrays for quick referencing of UI/background use.

Now - my questions are:

  1. Should I fetch only from the UI thread? Is it okay that I'm holding the managed objects in DataModel objects?
  2. How can I create a new entity from a background thread and use the newly created entity in the DataModel objects?
  3. Is there a best design scenario that I should use? specifically when sending a request to the server and getting a response, should I create a new managed context and use it throughout the thread's activity?

Let me know if everything is clear. If not, I'll try and add clarity.

Any help or guidelines would be appreciated.

Thanks!


Solution

  • I'm not working with MagicalRecord, but these questions are more related to CoreData than to MagicalRecord, so I'll try to answer them :).

    1) Fetching from main(UI) thread

    There are many ways how to design app model, so two important things I've learned using CoreData for few years:

    • when dealing with UI, always fetch objects on main thread. As You correctly stated NSManagedObjects are not thread safe so you can't (well, can, but shouldn't) access their data from different thread. NSFetchedResultsController is Your best friend when you need to display long lists (eg. for messages – but watch out for request's batchSize).

    • you should design your storage and fetches to be fast and responsive. Use indexes, fetch only needed properties, prefetch relationships etc.

    • on the other hand if you need to fetch from large amount of data, you can use context on different thread and transfer NSManagedObjectIDs only. Let's say your user has huge amount of messages and you want to show him latest 10 from specific contact. You can create background context (private concurrency), fetch these 10 message IDs (NSManagedObjectIDResultType), store them in array (or any other suitable format for you), return them to your main thread and fetch those IDs only. Note that this approach speed things up if fetch takes long because of predicate/sortDescriptor not if the "problem" is in the process of turning faults into objects (eg. large UIImage stored in transformable attribute :) )

    2) Creating entity in background

    You can create the object in background context, store it's NSManagedObjectID after you save context (object has only temporary ID before save) and send it back to your main thread, where you can perform fetch by ID and get the object in your main context.

    3) Working with background context

    I don't know if it's exactly the best, but I'm pretty much satisfied with NSManagedObjectContext observation and merging from notifications. Checkout: mergeChangesFromContextDidSaveNotification:

    So, you create background context, add main context as observer for changes (NSManagedObjectContextObjectsDidChangeNotification) and background context automatically sends you notifications (every time you perform save) about all of it's changes – inserted/updated/deleted objects (no worries, you can just merge it by calling mergeChangesFromContextDidSaveNotification:). This has many advantages, such as:

    • everything is updated automatically (every object you have fetched in "observing context" gets updated/deleted)
    • every merge runs in memory (no fetches, no persisting on main thread)
    • if you implement NSFetchedResultsController's delegate method, everything updates automatically (not exactly everything – see below)

    On the other side:

    • take care about merging policy (NSMangedObjectContext mergePolicy)
    • watchout for referencing managed objects that got deleted from background (or just another context)
    • NSFetchedResultsController updates only on changes to "direct" attributes (checkout this SO question)

    Well, I hope it answers your questions. If everything comes up, don't hesitate to ask :)

    Side note about child contexts

    Also take a peek to child contexts. They can be powerful too. Basically every child context sends it's changes to parent context on save (in case of "base" context (no parent context), it sends it's changes to persistent coordinator).

    For example when you are creating edit/add controller you can create child context from your main context and perform all changes in it. When user decides to cancel the operation, you just destroy (remove reference) the child context and no changes will be stored. If user decides to accept changes he/she made, save the child context and destroy it. By saving child context all changes are propagated to it's parent store (in this example your main context). Just be sure to also save the parent context (at some point) to persist these changes (save: method doesn't do the bubbling). Checkout documentation of managing parent store.

    Happy coding!