6

One of my fellow developers was working on a class that converted feedback from a REST callout into SObjects. Part of the class was storing the feedback in a map, and then attempted to upsert the map values. The code looked similar to the snippet below:

Map<Id, Feedback__c> feedbackMap = new Map<Id, Feedback__c>();

// Fill map and perform other functions....

List<Feedback__c> updatedFeedback = (List<Feedback__c>) feedbackMap.values();
upsert updatedFeedback;

The upsert resulted in the below error:

System.TypeException: DML on generic List only allowed for insert, update or delete

I'd remembered getting this before when attempting to perform DML on something that wasn't an SObject or List<SObject>, like Set<SObject>, so I recommended he try adding the map values to a list instead of just casting the map values:

List<Feedback__c> updatedFeedback = new List<Feedback__c>();
updatedFeedback.addAll(feedbackMap.values());
upsert updatedFeedback;

This worked no problem. I'm glad that my solution worked, but I want to know exactly why it worked. Was there something that was causing the cast map values to still be typed as List<Object>? Why did adding the values work instead of casting?

3
  • 2
    Are you sure that the map being used in the code that was failing was declared with a concrete SObject type instead of being a Map<Id, SObject>?
    – Derek F
    Commented Apr 9, 2019 at 20:09
  • So it was declared as Map<Id, SObject>. Map.values() should still return as least as List<SObject>, which should upsert fine, right? And why did explicitly casting it as a list of a concrete SObject not work as we expected? Commented Apr 9, 2019 at 20:48
  • The error you got rules out upsert on List<SObject>, but I'm not sure why casting to a collection of a concrete SObject type first didn't work. I may play around with that a bit tomorrow.
    – Derek F
    Commented Apr 9, 2019 at 21:44

1 Answer 1

4

I coudn't replicate the issue from provided example. If you could elaborate I may be able to give you more precise answer, why addAll() worked for you.


The key here is that the declarion has been with Map<Id, SObject>. And upsert does not work on SObject collections (there is a ways arround it).

I was able to reproduce the same behaviour with following code. (I belive you could have something similar at the beginning)

String response = '[{"attributes": {"type": "Account"}, "id": "00100000000000001", "name":"Test"}]';
List<SObject> objs = (List<SObject>)JSON.deserialize(response, List<SObject>.class);

Map<Id, SObject> sObjectMap = new Map<Id, SObject>(objs);
List<Account> acountList = (List<Account>) sObjectMap.values();

upsert acountList;
// Runtime exception: System.TypeException: DML on generic List<SObject> only allowed for insert, update or delete

Simplified example, everyone can try:

List<Account> accounts = (List<Account>) new List<SObject>{
    new Account(Name = 'Doe'),
    new Account(Name = 'Jackson')
};
  • I've created collection variable of type List that should hold Account objects.
  • I've actually initialized it as List<SObject>.
  • But my List<SObject> holds Account instances only.
  • Apex will let me deploy (and build) the class.
  • Remember accountList variable actually points to List<SObject>.

So far so good, you could be doing insert, update and delete DML statements on accountList.

And if I try upsert

upsert accountList;
// Runtime exception: System.TypeException: DML on generic List<SObject> only allowed for insert, update or delete

Why? Becuase casting List collections actually does nothing. Runtime initialized accountList as List<SObject> and does not care that variable pointing to the object says it should be List<Account>.


Now I will try to explain the confusion behind this.

  • Casting isn't magic, you are only telling the compiler that Object A is more of a type Object B (reference).

  • Apex is based on JVM. In Java you can't cast collections (without generics you can't and we don't have generics in Apex).

  • Not related: In Java you can cast arrays. I don't how Apex List is implemented, but I believe it's rather the ArrayList than simple old Array

  • What is very confusing is that Apex compiler will let you cast List collections but no Map and Set.


List<Account> accounts = (List<Account>) new List<SObject>();
// Will compile, no runtime exceptions
Map<Id, Account> accountMap = (Map<Id, Account>) new Map<Id, SObject>();
// Will compile, but runtime exception:
// FATAL_ERROR System.TypeException: Invalid conversion from runtime type Map to Map
Set<Account> accountSet = (Set<Account>) new Set<SObject>();
// Won't compile with message:
// Incompatible types since an instance of Set<SObject> is never an instance of Set<Account>
2

You must log in to answer this question.

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