Apex Cast sObject list dynamically to a specific sObject Type

  • The upsert() DML operation requires a specific List based on a specific sObject type, for instance:List<Account> lstAccount = new List<Account>();

    However, we are trying to generate a generic method which allows us to pass by an sObject list containing all data records to be upserted for a specific sObject. An insert() operation allows us to use a List<sObject> to be inserted but the upsert() function is rejecting it:

    List<sObject> listWRecords = new List<sObject>();
    listWRecords.add(new Foo__c(Name="Record1"));
    listWRecords.add(new Foo__c(Name="Record2"));
    
    upsert(listWRecords, 'ExternalIdFieldFrom_Foo__c', false);
    

    Which will result in an error saying that the upsert must be performed on a concrete entity type.

    Dynamically casting an sObject list is also not possible; I've seen solutions to 'hard code' or create Wrapper classes for each sObject on your org. But we would like to create a generic option here.

    Another possibility would be to create some logic mimicking an upsert by splitting it into an update() and an insert() operation. Keeping in mind Salesforce limits, I do not see an option to go this way.

    So yes we have same problem. We essentially have a metadata engine on top of the SFDC engine as we are a OEM/ISV. To do proper Apex SOC and Enterprise Design Patterns without additional overhead of creating our own UPSERT operation is frustrating. The fact we need to consume additional SOQL calls / governor limits, create additional unit test coverage scenarios, and other engineering effort is frustrating.

  • I know this approach is strange because you are still working with a List<SObject>, but when you assign it you can make it more specific (e.g. List<Account>) by using Type.forName and Type.newInstance methods.

    public static void dynamicUpsert(List<SObject> records)
    {
        Schema.SObjectType sObjectType = records.getSObjectType();
        if (sObjectType != null)
        {
            String listType = 'List<' + sObjectType + '>';
            List<SObject> castRecords = (List<SObject>)Type.forName(listType).newInstance();
            castRecords.addAll(records);
            upsert castRecords;
        }
    }
    

    The getSObjectType call may not be completely reliable, especially since some of these records are being inserted and hence won't have ids. For that reason, it is probably better to accept sObjectType as an additional parameter instead of trying to determine it on the fly.

    public static void dynamicUpsert(List<SObject> records, SObjectType sObjectType)
    {
        String listType = 'List<' + sObjectType + '>';
        List<SObject> castRecords = (List<SObject>)Type.forName(listType).newInstance();
        castRecords.addAll(records);
        upsert castRecords;
    }
    

    Update

    Some bad news, as discovered here (actually earlier than your post), the above methodology does not always work. Specifically, if I want to perform a partial upsert and also specify an external Id field, I get a compile fail.

    Database.upsert(castRecords, externalIdField); // compiles, throws TypeException
    Database.upsert(castRecords, /*allOrNone*/ false); // compiles, throws TypeException
    Database.upsert(castRecords, externalIdField, /*allOrNone*/ false); // compile fail 
    

    Additional Update

    I have a case open with support and it has been escalated to Tier 3. Currently the responses I'm getting are all along the lines of "will update you tomorrow," but I will post here if I get any resolution.

    Additional Update

    Support claims the three parameter signature failure is WAD. They said if I want them to actually fix it, I need to post on an Idea, so here it is, vote for it!

    Tier 3 ... performed some testing and it looks like that we cannot do upsert with List regardless of which of the 3 method signatures you use.

    The only difference in behavior seems to be that we block using the 3-arg one at compile time and the 2-arg and 0-arg ones fail at run- time.

    Basically for the 3-arg we validate at compile time that you are not passing in a list parameterized with SObject. But for the other 2 we compile it and we allow it to run and check in the call whether the list is generic.

    We could make the 3-arg work but it seems like a feature request and based on the behavior of the 0-arg and 2-arg versions we would presumably still enforce that the underlying list isn't generic.

    Hi Adrian, thanks for the reply! We have finished our developments but, when I have some time available the coming days, I will definitely play around with your solution in our scope. I'll let you know the outcome. Thanks again!

    @RobinWijnen Did it work?

    @FernandoGavinho I have successfully implemented this in a `DML` library and it does work.

    very good work here.....

    Thanks @AdrianLarson for talking to support! Voted for the idea

    Did you perchance try creating your externalIdField as a field token, e.g. `Schema.SObjectField externalIdField = yourType.getDescribe().fields.getMap().get('Ext_Id__c');` seems to work from what i'm seeing...

    @PhilHawthorn Yes the library I work only uses tokens. If it's working now...maybe they fixed it. Are you specifying all three parameters? That is the only broken signature. It seems to still be broken in my sandbox.

    Ah, ok - I misunderstood, thought it was just that an external id was passed, I'm just passing the two as thats all I need.

    +10 @AdrianLarson Awesome....

    @TomSnyder Have you actually tried it? Because you cannot upsert a generic `List`. It has to be concretely typed.

    @AdrianLarson, In my testing I was passing a concrete list to method(LIST sos), and that worked without a type cast as long as you didnt supply all 3 args, but confimed creating a list like question does not work without type casting.

License under CC-BY-SA with attribution


Content dated before 7/24/2021 11:53 AM