2

We have some setup data that is presently expressed in Apex as arrays of SObjects - with a couple of levels of parent/child relationships - but need to generalise this to support many variations of this data. The number of records overall is about 100 in each variation.

Including multiple static resources in our product where each has that data expressed in the JSON output format of sfdx force:data:tree:export seems appealing: one static resource per variation of the full parent/child graph. But how to import that data from Apex given that e.g. force:data:tree:import can't be accessed?

Using the sObject Tree REST API from Apex would be one approach, but that leaves you needing to have remote site settings configured and needing to do that REST callout before any DML.

But the format looks simple to parse, so more direct Apex code that inserts layer by layer could be written. If you have used this approach, and are willing to share Apex code for it, do post that code here. (If I don't find any and end up writing it myself I'll post here.)

(On reflection, perhaps all that is needed is a JSON.deserializeUntyped followed by a walk through the records collections, taking the types from the attributes? See an example of the format here sObject Tree Request Body.)

3
  • 1
    Are you 100% sure that you must do that in Apex ? If not, Salesforce Data Move Utility makes a far better job than force:data:tree:import/export github.com/forcedotcom/SFDX-Data-Move-Utility Jul 11, 2021 at 10:29
  • Thanks for that reference @NicolasVuillamy; wasn't aware of that and good to know about it. For now Apex, as trying to solve a fairly simple problem.
    – Keith C
    Jul 11, 2021 at 15:13
  • If you are forced to do apex, i'm afraid you have to manually manage that object by object :/ linkedin.com/pulse/… Jul 11, 2021 at 19:23

1 Answer 1

0

Note this is only at the level of a proof of concept but it does work on the sObject Tree Request Body if the numberOfEmployees is left out.

But I now realise my problem is more complicated - see Joining up data from multiple sfdx force:data:tree:export outputs.

/**
 * Insert objects stored in JSON sObject tree format i.e. nested records.
 */
public inherited sharing class TreeJsonLoader {

    private static final Map<String, SObjectType> GD_CACHE = Schema.getGlobalDescribe();
    private static SObjectType typeFor(String type) {
        return GD_CACHE.get(type);
    }

    // Cache as referenced often
    private static final Map<SObjectType, Meta> META_CACHE = new Map<SObjectType, Meta>();
    private static Meta metaFor(SObjectType parentType, SObjectType type) {
        Meta meta = META_CACHE.get(type);
        if (meta == null) {
            meta = new Meta(parentType, type);
            META_CACHE.put(type, meta);
        }
        return meta;
    }

    /**
     * Metadata about an SObjectType and its fields.
     */
    private class Meta {

        SObjectType type;

        SObjectField parentField;
        Set<String> dataFields = new Set<String>();
        String[] childRelationshipNames = new String[] {};

        Meta(SObjectType parentType, SObjectType type) {

            this.type = type;

            // Child relationship names
            DescribeSObjectResult dor = type.getDescribe();
            for (Schema.ChildRelationship child : dor.getChildRelationships()) {
                childRelationshipNames.add(child.getRelationshipName());
            }

            // Fields
            for (SObjectField f : dor.fields.getMap().values()) {

                DescribeFieldResult dfr = f.getDescribe();

                // Non parent fields
                if (dfr.getReferenceTo().size() == 0) {
                    dataFields.add(String.valueOf(f).toLowerCase());
                } else {
                    if (parentType != null) {
                        for (SObjectType t : dfr.getReferenceTo()) {
                            // Parent field; assuming one only here which is not generally the case
                            if (t == parentType) {
                                parentField = f;
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Represents record parent/child nesting and holds the raw data and processed data.
     */
    private class Node {

        Node parent;
        Node[] children = new Node[] {};
        Map<String, Object> record;

        SObjectType type;
        SObject sob;

        Node(Node parent, Map<String, Object> record) {

            this.parent = parent;
            if (parent != null) parent.children.add(this);
            this.record = record;

            Map<String, Object> attributes = (Map<String, Object>) record.get('attributes');
            if (attributes != null) {
                String t = (String) attributes.get('type');
                if (t != null) {
                    this.type = typeFor(t);
                }
            }
        }

        /**
         * Must be called after parent layer has been inserted so parent Id is available.
         */
        SObject createSob() {

            Meta meta = metaFor(parent.type, type);

            sob = type.newSObject();
            for (String key : record.keySet()) {
                if (meta.dataFields.contains(key.toLowerCase())) {
                    Object value = record.get(key);
                    // TODO type conversion e.g. dates and maybe numbers too
                    if (value != null) sob.put(key, value);
                }
            }

            if (meta.parentField != null) {
                sob.put(meta.parentField, parent.sob.Id);
            }

            return sob;
        }
    }

    /**
     * Entry point that assumes the tree JSON comes from a static resource.
     */
    public void loadStaticResource(String ns, String name) {

        for (StaticResource sr : [
                select Body
                from StaticResource
                where NamespacePrefix = :ns
                and Name = :name
                ]) {
            loadJson(sr.Body.toString());
        }
    }

    private void loadJson(String jsonString) {

        Map<String, Object> holder = (Map<String, Object>) JSON.deserializeUntyped(jsonString);

        Node root = new Node(null, new Map<String, Object>());
        splitIntoNodes(root, holder);
        insertLayersOfNodes(root.children);
    }

    private void splitIntoNodes(Node parent, Map<String, Object> holder) {

        Object[] records = (Object[]) holder.get('records');

        for (Object r : records) {

            Map<String, Object> record = (Map<String, Object>) r;

            Node node = new Node(parent, record);
            Meta meta = metaFor(parent.type, node.type);

            // Add children
            for (String childRelationshipName : meta.childRelationshipNames) {
                Map<String, Object> childHolder = (Map<String, Object>) record.get(childRelationshipName);
                if (childHolder != null) {
                    // Recursive call
                    splitIntoNodes(node, childHolder);
                }
            }
        }
    }

    private void insertLayersOfNodes(Node[] nodes) {

        Node[] nextLayer = new Node[] {};

        // Allows multiple child types
        Map<SObjectType, SObject[]> sobsByType = new Map<SObjectType, SObject[]>();
        for (Node node : nodes) {

            SObject[] sobs = sobsByType.get(node.type);
            if (sobs == null) {
                sobs = new SObject[] {};
                sobsByType.put(node.type, sobs);
            }
            sobs.add(node.createSob());

            nextLayer.addAll(node.children);
        }

        for (SObjectType t : sobsByType.keySet()) {
            // TODO check insert rights
            insert sobsByType.get(t);
        }

        if (nextLayer.size() > 0) {
            // Recursive call
            insertLayersOfNodes(nextLayer);
        }
    }
}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .