Breeze create entity from existing one

2019-02-18 01:33发布

问题:

I've been pounding my head for a few days now.

Imagine you have a car sales management application. You sell different models. Your Car model has 50 properties. Just for the example, let's say that you want to sell Bugatti Veyron. Now, you just received 5 of those cars. So, I log in to my application, create first Bugatti Veyron with specific ID. Then I want to add 2nd one, but there is a problem - I would have to write down all of those properties again! I'd like to have a Copy button and I'd just change serial number, breeze would change ID and voila, two cars in there!

For hack sake, at first I created this solution:

newCar(datacontext.createCar());
newCar().property1(oldCar().property1());
newCar().property2(oldCar().property2());
...

it was ugly, and after I proved I can do it, of course, request for application was to make everything copyable - no way I would do that! There must be a copy somewhere. After digging a lot of things, even trying to change some stuff in breeze itself, I couldn't do something like:

manager.createEntity('Car', oldCar);

Now, latest solution is a bit more viable than first one, but still requires more code than I would want and is not as intuitive as it could be:

        var newObject = {};
        manager.metadataStore._structuralTypeMap[oldCar.entityType.name].dataProperties.forEach(function (singleProperty) {
                if (!singleProperty.isPartOfKey)
                newObject[singleProperty.name] = oldCar[singleProperty.name];
            });
        var newCar = manager.createEntity('Equipment', newObject);

Is there any other "cleaner" way of making a new Entity with exactly the same properties, but of course, different id?

I should mention that Car entity has some ICollections in it, but this hack-ish solution ignores them which could be improved, but currently I handle that myself with a few .forEach loops.

回答1:

We're working on such a thing in the back room. We'll let you know when its ready. No promises or timing.

Meanwhile, I took a crack at it. I decided to leverage the fact that the Breeze EntityManager.exportEntities method knows how to clone an entity. If you read the breeze source code for that method, you know it's tricky.

This is what I came up with (as a civilian, not a Breeze developer):

function cloneItem(item) {
    // export w/o metadata and then parse the exported string.
    var exported = JSON.parse(manager.exportEntities([item], false));
    // extract the entity from the export
    var type = item.entityType;
    var copy = exported.entityGroupMap[type.name].entities[0];
    // remove the entityAspect
    delete copy.entityAspect; 
    // remove the key properties
    type.keyProperties.forEach(function (p) { delete copy[p.name]; });

    // the "copy" provides the initial values for the create
    return manager.createEntity(type, copy);
}

Like yours, it preserves the foreign key properties which means that a reference navigation property to a parent entity will have a value drawn from cache if the source had such a value.

Like yours, the collection navigation properties won't be populated. This method doesn't know how to clone the children. Nor is it self-evident that it should. That's extra credit for you.

Update 15 Dec 2013

Because you asked, I've re-implemented with ability to clone children (collection navigations). I've followed the syntax in you suggested so the usage would be:

cloneItem(something, ['collectionProp1', 'collectionProp2']); 

Note that I'm again relying on Breeze export to do the heavy lifting

Warning: this code is extremely fragile and not generalizable to all models

function cloneItem(item, collectionNames) {
    var manager = item.entityAspect.entityManager;
    // export w/o metadata and then parse the exported string.
    var exported = JSON.parse(manager.exportEntities([item], false));
    // extract the entity from the export
    var type = item.entityType;
    var copy = exported.entityGroupMap[type.name].entities[0];
    // remove the entityAspect (todo: remove complexAspect from nested complex types)
    delete copy.entityAspect;
    // remove the key properties (assumes key is store-generated)
    type.keyProperties.forEach(function (p) { delete copy[p.name]; });

    // the "copy" provides the initial values for the create
    var newItem = manager.createEntity(type, copy);

    if (collectionNames && collectionNames.length) {
        // can only handle parent w/ single PK values
        var parentKeyValue = newItem.entityAspect.getKey().values[0];
        collectionNames.forEach(copyChildren);
    }
    return newItem;

    function copyChildren(navPropName) {
        // todo: add much more error handling
        var navProp = type.getNavigationProperty(navPropName);
        if (navProp.isScalar) return; // only copies collection navigations. Todo: should it throw?

        // This method only copies children (dependent entities), not a related parent
        // Child (dependent) navigations have inverse FK names, not FK names
        var fk = navProp.invForeignKeyNames[0]; // can only handle child w/ single FK value
        if (!fk) return; 

        // Breeze `getProperty` gets values for all model libraries, e.g. both KO and Angular
        var children = item.getProperty(navPropName);
        if (children.length === 0) return;

        // Copy all children
        var childType = navProp.entityType;
        children = JSON.parse(manager.exportEntities(children, false));
        var copies = children.entityGroupMap[childType.name].entities;

        copies.forEach(function(c) {
            delete c.entityAspect;
            // remove key properties (assumes keys are store generated)
            childType.keyProperties.forEach(function (p) { delete c[p.name]; }); 
            // set the FK parent of the copy to the new item's PK               
            c[fk] = parentKeyValue;
            // merely creating them will cause Breeze to add them to the parent
            manager.createEntity(childType, c);
        });
    }


回答2:

Took Wards answer and extended it to allow for deep property linking. example usage is

cloneEntity(someEntity, ['collectionProp1.subCollection.another', 'collectionProp2']); 

Note, this is still quite untested and only applicable for certain models.

function cloneEntity(item, collectionNames) {
    var manager = item.entityAspect.entityManager;

    // export w/o metadata and then parse the exported string.
    var exported = JSON.parse(manager.exportEntities([item], false));

    // extract the entity from the export
    var type = item.entityType;
    var copy = exported.entityGroupMap[type.name].entities[0];

    // remove the entityAspect (todo: remove complexAspect from nested complex types)
    delete copy.entityAspect;

    // remove the key properties (assumes key is store-generated)
    type.keyProperties.forEach(function (p) { delete copy[p.name]; });

    // the "copy" provides the initial values for the create
    var newItem = manager.createEntity(type, copy);

    if (collectionNames && collectionNames.length) {
        // can only handle parent w/ single PK values
        var keyValue = newItem.entityAspect.getKey().values[0];
        collectionNames.forEach(function (propertyString) { copyChildren(item, propertyString, keyValue); });
    }
    return newItem;

    function copyChildren(parentItem, navPropString, parentKeyValue) {

        var navPropName;
        // todo: add much more error handling
        var parentType = parentItem.entityType;

        //parse deep properties
        if (navPropString.indexOf('.') >= 0) {
            navPropName = navPropString.substr(0, navPropString.indexOf('.'));
            navPropString = navPropString.substr(navPropString.indexOf('.') + 1);
        } else {
            navPropName = navPropString;
            navPropString = "";
        }

        var navProp = parentType.getNavigationProperty(navPropName);

        if (navProp.isScalar) return; // only copies collection navigations. Todo: should it throw?

        // This method only copies children (dependent entities), not a related parent
        // Child (dependent) navigations have inverse FK names, not FK names
        var fk = navProp.invForeignKeyNames[0]; // can only handle child w/ single FK value
        if (!fk) return;

        // Breeze `getProperty` gets values for all model libraries, e.g. both KO and Angular
        var children = parentItem.getProperty(navPropName);
        if (children.length === 0) return;

        // Copy all children
        var childType = navProp.entityType;
        var copies = JSON.parse(manager.exportEntities(children, false)).entityGroupMap[childType.name].entities;

        copies.forEach(function (c) {

            //Get the original childid for deeper copy
            var originalChildId = c.id;

            delete c.entityAspect;

            // remove key properties (assumes keys are store generated)
            childType.keyProperties.forEach(function (p) { delete c[p.name]; });

            // set the FK parent of the copy to the new item's PK               
            c[fk] = parentKeyValue;

            // merely creating them will cause Breeze to add them to the parent
            var childItem = manager.createEntity(childType, c);

            if (navPropString.length > 0) {
                //Copy children

                var originalChild = $.grep(children, function (a) {
                    return a.id() == originalChildId;
                })[0];

                var childKeyValue = childItem.entityAspect.getKey().values[0];
                copyChildren(originalChild, navPropString, childKeyValue);
            }
        });
    }
};


回答3:

My navigation property has subtypes, created by hand at object creation. Using Ward's cloneItem() threw error because

children.entityGroupMap has no childType.name entry.

Here's my solution for this case, in the last part of copyChildren() :

            .....
        // Copy all children
        var childType = navProp.entityType;
        children = JSON.parse(manager.exportEntities(children, false));

        var copies;
        if (children.entityGroupMap.hasOwnProperty(childType.name)) {
            copies = children.entityGroupMap[childType.name].entities;
            copyChildrenOfType(copies, childType);
        }
        else {
            childType.subtypes.forEach(function (subtype) {
                if (children.entityGroupMap.hasOwnProperty(subtype.name)) {
                    copies = children.entityGroupMap[subtype.name].entities;
                    copyChildrenOfType(copies, subtype);
                }
            });
        }
        function copyChildrenOfType(copies, childType) {
            copies.forEach(function (c) {
                delete c.entityAspect;
                // remove key properties (assumes keys are store generated)
                childType.keyProperties.forEach(function (p) { delete c[p.name]; });
                // set the FK parent of the copy to the new item's PK               
                c[fk] = parentKeyValue;
                // merely creating them will cause Breeze to add them to the parent
                manager.createEntity(childType, c);
            });
        }


回答4:

While Breeze team works on it, and if somebody needs this before it's done, here is code I wrote for making a copy of object and it's navigation properties:

    function createSimpleObject(heavyObject) {
        if (heavyObject === undefined) return {};
        var simpleObject = {};
        manager.metadataStore._structuralTypeMap[heavyObject.entityType.name].dataProperties.forEach(function (singleProperty) {
            if (!singleProperty.isPartOfKey)
                simpleObject[singleProperty.name] = heavyObject[singleProperty.name]();
        });
        return simpleObject;
    }

    function makeNavigationProperties(newObject, oldObject, navigationProperties) {
        if (oldObject === undefined || navigationProperties === undefined) return {};
        navigationProperties.forEach(function (singleNavigationProperty) {
            var selectedArray = [];
            if (ko.isObservable(oldObject[singleNavigationProperty])) {
                selectedArray = oldObject[singleNavigationProperty]();
            }
            else selectedArray = oldObject[singleNavigationProperty];
            if (selectedArray) {
                selectedArray.forEach(function (singleObject) {
                    var simpleObject = {};
                    manager.metadataStore._structuralTypeMap[singleObject.entityType.name].dataProperties.forEach(function (singleProperty) {
                        if (!singleProperty.isPartOfKey) {
                            if (singleProperty.relatedNavigationProperty) {
                                if (singleProperty.relatedNavigationProperty.entityTypeName === oldObject.entityType.name) {
                                    simpleObject[singleProperty.name] = newObject.id();
                                }
                                else {
                                    if (ko.isObservable(singleObject[singleProperty.name]))
                                        simpleObject[singleProperty.name] = singleObject[singleProperty.name]();
                                    else simpleObject[singleProperty.name] = singleObject[singleProperty.name];
                                }
                            }
                            else {
                                if (ko.isObservable(singleObject[singleProperty.name]))
                                    simpleObject[singleProperty.name] = singleObject[singleProperty.name]();
                                else simpleObject[singleProperty.name] = singleObject[singleProperty.name];
                            }
                        }
                    });
                    manager.createEntity(singleObject.entityType.shortName, simpleObject);
                });
            }
        });
    }

and here is method that creates object:

function createMyObject(originalObject, navigationProperties){
    var newMyObject = manager.createEntity('MyObject', createSimpleObject(originalObject));
    makeNavigationProperties(newMyObject, originalObject, navigationProperties);
    return newMyObject;
}

and finally code that calls creation of new object:

copiedObject(datacontext.createMyNewObject(originalObject(), ['navigationProperty1', 'navigationProperty2', 'navigationProperty3']));

where copiedObject is observable containing new object, originalObject is object that I want to copy and 2nd parameter contains properties I want to copy. It only works for direct children of original object, I didn't need children of children so that is missing. It can be used with or without parameters so I use exactly the same function to create empty object or to copy entities with no children.

EDIT

Wards code works perfectly! Only one thing I'd change is his fk detection and use following to enable copying Junction tables:

var fk = false;
navProp.entityType.foreignKeyProperties.forEach(function (singleProperty) {
if (singleProperty.relatedNavigationProperty.entityTypeName == newItem.entityType.name)
    fk = singleProperty.name;
});
if (!fk) return;


回答5:

I've used Ward's original code (without the children clone part) and added the following code to it so it recursively removes the complexAspect (I had a Geography-Attribute wich consisted of two nested complex-properties):

CloneEntity: function (originalEntity) {
    // NoRyb's change
    function recursiveFixEntity(entity) {
        if (entity && (typeof entity === 'object')) {
            delete entity.complexAspect;
            for (var propertyName in entity) {
                recursiveFixEntity(entity[propertyName]);
            }
        }
    };

    var manager = originalEntity.entityAspect.entityManager;
    // export w/o metadata and then parse the exported string.
    var exported = manager.exportEntities([originalEntity], { asString: false, includeMetadata: false });
    // extract the entity from the export
    var type = originalEntity.entityType;
    var copy = exported.entityGroupMap[type.name].entities[0];
    // remove the entityAspect
    delete copy.entityAspect;
    // remove the key properties
    type.keyProperties.forEach(function (p) { delete copy[p.name]; });

    // NoRyb's change:
    recursiveFixEntity(copy);

    // the "copy" provides the initial values for the create
    return manager.createEntity(type, copy);
}

Also I don't parse the JSON from exportEntities myself instead I used the option (I guess this was added later).