The idea of making a deep copy of NSMangedObject is to create a controlled copy of an NS-Managed Object that contains all the data involved in the relationship hierarchy of that managed object.

Although Core Data is a powerful object graph management framework, it doesn’t directly provide a way to copy managed objects itself. If a developer wants to create a copy of a managed object, the only way to do so is to write a specific piece of code that reads out the contents of the properties in the original object and assigns them to the new object. This method is more effective when the managed object has a simple structure. Once the managed object has a complex structure and many relationships, the amount of code will increase significantly and it is easy to make mistakes.

Developers have been looking for a convenient and universal tool to solve the problem of deep copy for years, but until now there hasn’t been a widely accepted solution.

I also encountered this problem when developing the new version of Health Note, which required a deep copy of a managed object with a complex structure and a large number of data relationships. Considering that I may encounter similar situations in the future, I decided to write a simple and widely applicable code to facilitate my own use.

This article will discuss the technical difficulties and solutions of deep copy of NS-managed object in Core Data, and introduce the tool I wrote — MOCloner.

The original post was posted on my blog wwww.fatbobman.com

Welcome to subscribe my public account: [Elbow’s Swift Notepad]

The difficulty of making a deep copy of NS-managed object

Complex relationship structure

Below is an excerpt from the health Note data model diagram. Although only part of the model relationship is selected, almost all relationship types are covered between entities, including one-to-one, one-to-many, many-to-many, and so on.

Each copy of a Note object involves hundreds or thousands of other objects in the relational chain. Implementing fast and accurate deep copies of all data can be quite challenging.

Selective copy

When doing deep copy, sometimes we do not need to copy all the data in all the relationship levels and may want to ignore an N +1 relationship branch at the NTH level.

Or when copying a managed object property (optional or with a default value), optionally not copying its contents.

All of this is best done in deep copy.

Data validity

Some properties in a managed pair have uniqueness or immediacy that requires special treatment in a deep copy.

Such as:

  • The id of the Note in the figure above is of type UUID. In deep copy, the original content should not be copied but new data should be created for the new object
  • NoteID in Item should correspond to the ID of Note. How to keep it consistent in the replication process
  • The createDate of ItemDate should be the time when the record was created. How can I set it to the date of deep copy

If similar problems cannot be handled at the same time during deep copy, post-copy adjustment will be difficult in the case of large data volume.

Converse to many relationships

In the figure above, there is a many-to-many relationship between Tag and Memo. Special care should be taken when a reverse pair Tag occurs on a relationship chain. Tag does not belong to a specific branch of Note logically, and how to deal with this situation has always been a difficult problem for Core Data synchronization.

Deep copy solution

While there are plenty of problems to face, there are still plenty of tools that Core Data provides to solve them.

Use the Description

Data models created in Xcode using the Data Model Editor are converted to momD files and stored in bundles at compile time. When you create NSPersistentContainer, the NS-Managed DobJectModel will use this file to transform the model definition into a program implementation. The code gets the information it needs by accessing the various descriptions provided by Core Data.

Developers are most often the Description of contact may be NSPersistentStoreDescription, we can obtain the Config or set up the options (for more information, please refer to master the Core Data Stack).

Other descriptions include but are not limited to:

  • NSEntityDescription

    Entity description

  • NSRelationshipDescription

    Description of entity relationships

  • NSAttributeDescription

    Description of an entity Attribute

  • NSFetchIndexDescription

    Index Indicates the description of Index

  • NSDerivedAttributeDescription

    Description of derived properties

The following code will create a new object with the same structure using the NSEntityDescription of the given managed object:

guard let context = originalObject.managedObjectContext else {
    throw CloneNSManagedObjectError.contextError
}

// create clone NSManagedObject
guard let entityName = originalObject.entity.name else {
    throw CloneNSManagedObjectError.entityNameError
}
let cloneObject = NSEntityDescription.insertNewObject(
    forEntityName: entityName,
    into: context
)
Copy the code

With NSAttributeDescription, we get all the attribute descriptions of the managed object:

let attributes = originalObject.entity.attributesByName
for (attributeName, attributeDescription) in attributes {
    .
}
Copy the code

All relations through NSRelationshipDescription, traverse managed object description:

let relationships = originalObject.entity.relationshipsByName

for (relationshipName, relationshipDescription) in relationships {
    .
}
Copy the code

Get the entity corresponding to the reverse relationship description:

let inverseEntity = relationshipDescription.inverseRelationship?.entity
Copy the code

These descriptions are the building blocks for developing nS-managed Object deep-copy generic code.

Use userInfo to pass information

To solve the problems mentioned above, such as selective copying and data validity, you need to provide sufficient information to the code during deep copying.

Since this information may be distributed at all levels of the chain of relationships, the most direct and effective way is to add the corresponding content to the User Info provided by Xcode’s data model editor.

Every developer who has used the Xcode data model editor should have seen the User Info input box on the right. Through this input box, we can set the information we want to pass for Entity, Attribute, and Relationship and extract it from the corresponding Description.

The following code determines whether there are exclusion flags in the Attribute’s userInfo:

if let userInfo = attributeDescription.userInfo {
    // Check if the "exclude" flag is added to this attribute
    // Only detemine whether the Key is "exclude" or note, do not care about the Vlaue
    if userInfo[config.exclude] ! = nil {
        if attributeDescription.isOptional || attributeDescription.defaultValue ! = nil {
            continue
        } else {
            throw CloneNSManagedObjectError.attributeExcludeError
        }
    }
}
Copy the code

The following code creates a new UUID for the Attribute (of type UUID) in userInfo that contains the rebuild: uUID flag:

if let action = userInfo[config.rebuild] as? String {
                    switch action {
                    case "uuid":
                        if attributeDescription.attributeType = = NSAttributeType.UUIDAttributeType {
                            newValue = UUID()}else {
                            throw CloneNSManagedObjectError.uuidTypeError
                        }
                    .
                    default:
                        break}}Copy the code

SetPrimitiveValue and setValue

SetPrimitiveValue is used in many cases during Core Data development. For example, setting the initial value of the property in awakeFromInsert, checking the validity of the property value in willSave, and so on. SetPrimitiveValue can be used to set an AttributeName Value, especially if we cannot call the managed object instance attribute directly.

for (attributeName, attributeDescription) in attributes {
    var newValue = originalObject.primitiveValue(forKey: attributeName)
    cloneObject.setPrimitiveValue(newValue, forKey: attributeName)
}
Copy the code

Because setPrimitiveValue directly access the original values of managed objects (skipping snapshots), it is more efficient and does not trigger KVO observations.

SetPrimitiveValue also has a disadvantage – it does not automatically process backward relationships. Using it to set up the relationship content requires work on both sides of the relationship, and the amount of code increases significantly.

For managed object instances, most of the time the relationship management methods generated by Core Data are used directly for relational operations, such as:

@objc(addItemsObject:)
@NSManaged public func addToItems(_ value: Item)

@objc(removeItemsObject:)
@NSManaged public func removeFromItems(_ value: Item)

@objc(addItems:)
@NSManaged public func addToItems(_ values: NSSet)

@objc(removeItems:)
@NSManaged public func removeFromItems(_ values: NSSet)
// Note and Item are one-to-many
let note = Note(context: viewContext)
let item = Item(context: viewContext)
note.addToItems(item)
item.note = note
Copy the code

In general-purpose deep-copy code, we cannot use these system-preset methods directly, but we can set relational data through setValue.

SetValue internally looks for the corresponding Setter to manage the bidirectional relationship.

Here is the code to set up the one-to-one relationship:

if !relationshipDescription.isToMany,
   let originalToOneObject = originalObject.primitiveValue(forKey: relationshipName) as? NSManagedObject {
    let newToOneObject = try cloneNSMangedObject(
        originalToOneObject,
        parentObject: originalObject,
        parentCloneObject: cloneObject,
        excludedRelationshipNames: passingExclusionList ? excludedRelationshipNames : [],
        saveBeforeReturn: false,
        root: false,
        config: config
    )
    cloneObject.setValue(newToOneObject, forKey: relationshipName)
}
Copy the code

NSSet and NSOrderedSet

In Core Data, for many relationships the corresponding type in the generated NSMangedObject Subclass code is NSSet, right? , but if you set the pairwise relationship to order, the corresponding type will become NSOrderedSet, right? .

By judging NSRelationshipDescription isOrdered, choose the right type. Such as:

if relationshipDescription.isOrdered {
    if let originalToManyObjects = (originalObject.primitiveValue(forKey: relationshipName) as? NSOrderedSet) {
        for needToCloneObject in originalToManyObjects {
            if let object = needToCloneObject as? NSManagedObject {
                let newObject = try cloneNSMangedObject(
                    object,
                    parentObject: originalObject,
                    parentCloneObject: cloneObject,
                    excludedRelationshipNames: passingExclusionList ? excludedRelationshipNames : [],
                    saveBeforeReturn: false,
                    root: false,
                    config: config
                )
                newToManyObjects.append(newObject)
            }
        }
    }
}
Copy the code

Inverse relation to many processing logic

Down the relationship chain, if the inverse relationship of a relationship is one-to-many, then no matter whether the positive relationship is one-to-one or one-to-many, an awkward situation will be formed in deep copy — the entity with the inverse relationship is one-to-many, serving the entire positive relationship tree.

For example, in the preceding figure, a Memo and Tag can correspond to multiple labels, and a label can correspond to multiple remarks. If we continue to copy the Tag as we go deep from Note to Memo, it will defeat the purpose of the Tag.

The solution is to stop copying down when encountering an entity A in the relationship chain whose inverse relationship is opposite to many. Instead, A new copy of the managed object is added to the relationship with A, satisfying the design intent of the data model.

if let inverseRelDesc = relationshipDescription.inverseRelationship, inverseRelDesc.isToMany {
    let relationshipObjects = originalObject.primitiveValue(forKey: relationshipName)
    cloneObject.setValue(relationshipObjects, forKey: relationshipName)
}
Copy the code

Deep copy with MOCloner

With that in mind, I wrote a library called MOCloner for making deep copies of NS-managed object in Core Data

MOCloner instructions

MOCloner is a small library designed to make customizable deep copies of NS-managed object. Supports one-to-one, one-to-many, and many-to-many relationship modes. In addition to the copy mode that is faithful to the original data, it also provides the functions of selective copy and generating new values during copy.

Based on demonstration

Create a deep copy of the Note shown above

let cloneNote = try! MOCloner().clone(object: note) as! Note
Copy the code

Deep copy from the middle part of the relationship chain down (do not copy the part up the relationship chain)

/ / ignore is added in the excludedRelationshipNames relationship name
let cloneItem = try! MOCloner().clone(object: item, excludedRelationshipNames: ["note"]) as! Item
Copy the code

The custom

MOCloner customizes the deep-copy process by adding key values to the User Info in Xcode’s Data Model Editor. Currently, the following commands are supported:

  • exclude

    This key can be set in Attribute or Relationship. Whenever the exclude key is present, exclusion logic is enabled for any value.

    When set to userInfo of the Attribute, the deep copy does not copy the value of the original object Attribute (the Attribute must be Optional or Default value has been set).

    When set in the Relationship’s userinfo, deep copy ignores all relationships and data under the Relationship branch.

    In order to facilitate some not suitable for setting in the userinfo (for example, from the relationship among chain for deep copy), also need to rule out the relationship between the name can be added to the excludedRelationshipNames parameters (such as basic demo 2).

  • rebuild

    Used to dynamically generate new data during deep copy. Only used to set Attribute. Currently, two values are supported: uUID and now.

    Uuid: Attribute whose type is UUID. A new UUID is created for the Attribute in deep copy

    Now: An Attribute of type Date, for which a new current Date (date.now) is created during deep copy.

  • followParent

    A simplified version of Derived. Only used to set Attribute. You can specify the Attribute of the Entity at the lower level of the relationship chain to obtain the specified Attribute value of the managed object instance corresponding to the upper level of the relationship chain (the two Attribute types must be consistent). In the figure below, the noteID of Item will get the ID value of Note.

  • withoutParent

    Use only with followParent. Handle the case where followParent is set but ParentObject cannot be obtained when deep copying from the middle of the relationship chain.

    When withoutParent is keep, the original value of the copied object is kept

    When withoutParent is blank, no value will be set (Attribute is Optional or has a Default value)

If the above the userinfo key name as the name of your project has been used in the key conflict, can pass the custom MOClonerUserInfoKeyConfig reset.

let moConfig = MOCloner.MOClonerUserInfoKeyConfig(
    rebuild: "newRebuild".// new Key Name
    followParent: "followParent",
    withoutParent: "withoutParent",
    exclude: "exclude"
)

let cloneNote = try cloner.clone(object: note,config: moConfig) as! Note
Copy the code

System requirements

MOCloner has minimum requirements for macOS 10.13, iOS 11, tvOS 11, watchOS 4 and above.

The installation

MOCloner is distributed using Swift Package Manager. To use it in another Swift Package, add it as a dependency in your package.swift.

let package = Package(
    .
    dependencies: [
        .package(url: "https://github.com/fatbobman/MOCloner.git", from: "0.1.0 from")]..
)
Copy the code

If you want to use MOCloner in your application, use Xcode’s File > Add Packages… Add it to your project.

import MOCloner
Copy the code

Since MOCloner is only a few hundred lines of code, you can copy the code into your project and use it directly.

Considerations for using MOCloner

Do it in a private context

When deep copy involves a large amount of data, do it in a private context to avoid using the main thread.

It is best to use NS-managed DObjectid before and after deep-copy operations for data transfer.

Memory footprint

When a deep-copy managed object involves a large amount of relational data, it can create a large footprint. This is especially true when you include binary type data (such as storing large amounts of image data in SQLite). You can use the following methods to control memory usage:

  • In deep copy, attributes or relationships with high memory footprint are temporarily excluded. After deep copy, add them one by one through other code.
  • When deeply copying multiple managed objects, consider using performBackgroundTask one by one.

Versions and Support

MOCloner uses the MIT protocol and you are free to use it in your projects. Note, however, that MOCloner does not come with any official support channels.

Core Data provides a wealth of features and options that developers can use to create a large number of different combinations of diagrams. MOCloner only tested some of these cases. Therefore, before you start preparing to use MOCloner for your project, it is highly recommended that you spend some time familiarizing yourself with the implementation and do more unit testing to avoid any data errors that might arise.

If you find problems, errors, or want to suggest improvements, create Issues or Pull Requests.

conclusion

Deep copy of NS-managed object is not a common functional requirement. But when you have a solution that can be easily done, you might want to try some new design ideas in your Core Data project.

Hopefully MOCloner and this article have been helpful.

The original post was posted on my blog wwww.fatbobman.com

Welcome to subscribe my public account: [Elbow’s Swift Notepad]