core-dataswift2nsmanagedobjectmodel

Building a NSManagedObjectModel from several models


There are several reasons why somebody wants to merge multiple NSManagedObjectModel's. If you search the web, all responses are that it is not possible or that it is only possible for two unrelated entities that share one or more relationships. See this and this link for example.

However with a bit or more work it is (I think) possible to merge NSManagedObjectModels, even if the entities are related (as in parent-child) or if the attributes are spread out across multiple models.

Though it will not show as readily in the Xcode model editor and out-of-box transitions (probably) won't work.

In the answer below my observations about core data and my code on merging several models. If you find any bugs or have suggestions for improvements, please respond here.


Solution

  • Some things I noticed:

    1. Copying a NSPropertyDescription (attribute, relationship) copies all its values, but not the entity to which it belongs. Same for the destinationEntity and inverseRelationship.

    2. Thus a copied NSPropertyDescription should be added to an entity. As a result, all the children entities of that entity automatically get the property as well.

    3. Copying a NSEntityDescription does not include the parent entity. So the tree (of NSManagedObjectEntity) has to rebuild manually.

    4. If you set the parent of an entity, that (child) entity will immediately and automatically inherit all its parent properties. In other words when you ask an entity for its attributes, this entity already knows about all its attributes. It will not first query its parent. (reasonable assumption)

    5. Adding entities to a model fills in the destination entities and inverse relationship descriptions of the relationsDescriptions of the added entities.

    6. if you do not set the name of any entity or property before using it, core data will complain. That is the copy by name instead of value aspect.

    7. Adding a property to an entity which already has a property with the same name (either from itself or inherited from its ancestor) will make core data complain.

    This translates into the following code:

    extension NSPropertyDescription
    {
       var isPlaceholder : Bool { return self.userInfo?["isPlaceholder"] != nil }
    }
    
    extension NSEntityDescription
    {
       var isPlaceholder : Bool { return self.userInfo?["isPlaceholder"] != nil }
    }
    
    func mergeModels(models: [NSManagedObjectModel]) -> NSManagedObjectModel?
    {
        var entities : [String : NSEntityDescription] = [:]
    
        //support functions
        let makeEntity : String -> NSEntityDescription = { entityName in
            let newEntity = NSEntityDescription()
            entities[entityName] = newEntity
            newEntity.name = entityName
            return newEntity
        }
    
        let setParent : (String, NSEntityDescription) -> () = { parentName, child in
            if let parent = entities[parentName]
            {
                parent.subentities.append(child)
            }
            else //parent has not yet been encountered, so generate it
            {
                let newParentEntity = makeEntity(parentName)
                newParentEntity.subentities.append(child)
            }
        }
    
    
        //rebuild model: generate new description for each entity and add non-placeholder properties
        for model in models
        {
            for entity in model.entities
            {
                guard let entityName = entity.name else { fatalError() }
                let mergedEntity = entities[entityName] ?? makeEntity(entityName)
    
                //set entity properties
                if !entity.isPlaceholder
                {
                    mergedEntity.abstract = entity.abstract
                    mergedEntity.managedObjectClassName = entity.managedObjectClassName
                }
    
                //set parent, if any
                if mergedEntity.superentity == nil, //no parent set
                    let parentName = entity.superentity?.name //but parent is required
                {
                    setParent(parentName, mergedEntity)
                }
    
                //set properties
                for property in entity.properties
                {
                    if property.isPlaceholder ||
                        mergedEntity.properties.contains({$0.name == property.name})
                    { continue }
    
                    let newProperty = property.copy() as! NSPropertyDescription
                    mergedEntity.properties.append(newProperty)
    
                }
            }
        }
    
        //generate final model
        let mergedModel = NSManagedObjectModel()
        mergedModel.entities = Array(entities.values) //sets the destination entity and inverse relationship descriptions
        return mergedModel
    }
    

    In the managedObjectModel (xcode editor) set the "placeholder" flag in the user info dictionary of the entity and/or the property.

    The model generation can be refined by setting additional keys in the user info dictionary to specify which model has the prime entity/attribute/relationship (settings) and appropriately adjusting this code fragment.

    However, if you can avoid using multiple models then avoid it. Your life will be much simpler by sticking to the standard single Model approach.

    [Disclaimer: as far as I can tell, this code should work. No guarantees though.]