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.
You can't cast ANY
to a more specific type, so you have to actually obtain the type that you want to return. Without proper reflection, you have to tell the function what type you'd like to return. There's several ways you can do that, but here's one way:
public class Parser {
public static List<List<Object>> splitList(List<Object> items, Integer splitSize, Type destType) {
// Take our destination type and cast to generic
List<List<Object>> result = (List<List<Object>>)destType.newInstance();
// Get a copy of source list to obtain list type.
List<Object> protoList = items.clone();
protoList.clear();
// This is the list that will actually be added to result
List<Object> tempList = protoList.clone();
// A for loop with two counters.
Integer index = 0, count = 0, size = items.size();
while(index < size) {
tempList.add(items[index++]);
count++;
// Split size reached, add to result and make new list
if(count == splitSize) {
result.add(tempList);
tempList = protoList.clone();
count = 0;
}
}
// Add left-over partial
if(!tempList.isEmpty()) {
result.add(protoList);
}
return result;
}
}
You use the code like this:
List<List<String>> v =
(List<List<String>>)Parser.splitList(
myStringList,
5, List<List<String>>.class
);
It's a little messy, as you have to give it a primitive data type and then cast it back, but it works.
As an alternative, you might try the "out-param" approach:
public class Parser {
public static void splitList(List<Object> items, Integer splitSize, List<List<Object>> result) {
List<Object> protoList = items.clone();
protoList.clear();
List<Object> tempList = protoList.clone();
Integer index = 0, count = 0, size = items.size();
while(index < size) {
tempList.add(items[index++]);
count++;
if(count == splitSize) {
result.add(tempList);
tempList = protoList.clone();
count = 0;
}
}
if(!tempList.isEmpty()) {
result.add(tempList);
}
}
}
This one can't incur the run-time exception if destType isn't compatible with List<List<Object>>
and saves you a bit of casting. You can use it like this:
List<List<String>> v = new List<List<String>>();
Parser.splitList(myStringList, 5, v);
The callee now becomes responsible for initializing the correct object type, but avoids the extra casting.
Best Answer
You can't add store an Object in an Account, nor a List of Object in a List of Account.
If, and only if, you're sure it's the correct type, you can "cast" to override:
If the type is invalid at runtime, you'll get a TypeException.
You note that sometimes your method will return a Boolean, so this would be unacceptable for a List of Account. You'd have to check first: