2013-05-31

Patching NSManagedObjects

Previous posts discussed how to create a diff of an NSManagedObject pair. Now we’ll look at how to apply that diff as a patch.

Creating a diff gives us a data structure that looks like this:

(
    {
        attributes: {
            new: {
                city: "Sometown"
            },
            old: {
                city: "Townsville"
            }
        },
        entityName = "Address",
        guid: "123"
    }
)

The diff tells us that the Address entity with the GUID “123” changed its city attribute from “Townsville” to “Sometown”.

The patcher has to be able to do a number of things with a patch like this:

  • It must update the CoreData entity’s attributes to match the new values for each change in the patch.
  • It must be able to traverse both the nested patch structure and the CoreData graph and apply patches to sub-entities.
  • It must be able to apply patch arrays to sets of sub-entities (one-to-many relationships).
  • It must be able to delete entities from the CoreData graph if the patch indicates a entity was deleted.
  • It must be able to create new entities within the CoreData graph if the patch indicates a new entity was created.

The last requirement is tricky. Creating an entity is rarely a case of just calling insertNewObjectForEntityForName: inManagedObjectContext:. Typically, each entity will have associated sub-entities that should be created at the same time, so the patcher will inevitably use the delegate pattern to call back to an external object that can create new entity instances.

It would probably be handy if the patcher could allow manual patching for certain entity types, so that the developer could override its default behaviour for complex or unusual object types. For example, we might receive a patch for a Distance entity that includes two attributes: value and units. If we respect the semantics of the type we can’t update either attribute in isolation. We would expect the patcher to only overwrite one attribute if only one attribute has changed, but we should really overwrite both values to maintain the integrity of the data. (In contrast to the earlier posts about diffing, the current version of the differ in SZManagedObject emits the entire set of old attributes if it detects a change, precisely to cater for this situation.) Allowing for the ability to manually patch objects and override the patcher’s default behaviour lets us do this.

Something else I’m going to bake in is the ability to manually convert types for date and binary attributes. I want to be able to send diffs over the wire as JSON, but JSON doesn’t like date and binary data. They typically get sent as ISO strings or JavaScript date object constructors (for dates) and base64-encoded strings (for binary). A patch will therefore not necessarily have the correct data types for these two attribute types. Whenever they are encountered in the entity I’ll check the type of data in the patch, and if they disagree I’ll call out to a delegate to perform a conversion process.

Patching an entity requires two steps: attributes then relationships. Patching attributes is easy:

  • If there are old values but no new values, the entity has been deleted.
  • Loop through keys in the new dictionary and apply the values to the entity.

Patching relationships is really just a matter of creating new entities if necessary and using recursion to patch the entire graph:

  • If the relationship is one-to-many, patch the relationship as a set.
  • If not:
    • If no sub-entity with the required identifier can be found, call out to the delegate to create it.
    • Recursively patch the entity’s attributes and relationships.

Eventually we’ll hit a point where there are no more sub-patches, which means we have finished patching.

Patching one-to-many relationships is a little more complex, but it isn’t too hard:

  • Loop through sub-patches.
  • If the matching sub-entity does not exist and there is no old data in the patch, call out to the delegate to create it.
  • If there are no new values in the patch, delete the sub-entity.
  • If there are new values, old values and an object to update, recursively update it.

We now have a fully patched object graph.

Again, check out the SZManagedObject project on BitBucket for the sourcecode.