A generic pattern for the sort of logic I think you are aiming to create is this:

for (CampaignMember cm : [
        SELECT Id, CampaignId, AccountID_Formula__c
        FROM CampaignMember
        WHERE AccountID_Formula__c = :accIds
        ORDER BY Name
        ]){
    List<CampaignMember> l = accountCampaignMap.get(cm.AccountID_Formula__c);
    if (l == null) {
        l = new List<CampaignMember>();
        accountCampaignMap.put(cm.AccountID_Formula__c, l);
    }
    l.add(cm);
}

This will result in each list being in the order of the CampaignMember.Name.

Answer from Keith C on Stack Exchange
๐ŸŒ
Akhil Kulkarni
salesforce.fun โ€บ 2022 โ€บ 08 โ€บ 22 โ€บ apex-soql-to-map-of-sobject
APEX: SOQL to MAP of sObject โ€“ Akhil Kulkarni
August 22, 2022 - Map<Id, Contact> m_Contacts = [Select Id, Name, Email, Phone From Contact Where AccountId IN: l_Account_Ids] Above query will create a MAP of sObjects with key as ID of that record and value as a record of sObject.
Discussions

salesforce - Apex - Retrieving Records from a type of Map - Stack Overflow
Map> leadMap = Map>(); Then in your query where you build the map you ensure that your Lead also includes the Account__c field. Any of these options should work, it all depends on how this code snippet in being executed and where. ... Find the answer to your question by asking. Ask question ... See similar questions with these tags. ... 0 Create a method that convert a List More on stackoverflow.com
๐ŸŒ stackoverflow.com
Cast a Map <Id, sObject> to Map <Id, Opportunity>
I'm trying to implement the trigger framework outlined here: The triggerhandler methods take lists or maps of sObjects as their inputs. In his example he casts list as list More on salesforce.stackexchange.com
๐ŸŒ salesforce.stackexchange.com
January 14, 2016
Cast map<id, sObject> to map<id, opportunity>
Cast the SObjects to Opportunities. e.g. private static processPotatoOpportunities(Map newItemById, Map oldItemById) { List newOpportunities = (List) newItemById.values(); for (Opportunity record : newOpportunities) { Opportunity oldRecord = (Opportunity) oldItemById.get(record.Id); //stuff } } Also, I cba at markdown More on reddit.com
๐ŸŒ r/salesforce
5
2
December 10, 2015
salesforce - Map of Sobject in SOQL query - Stack Overflow
When working with SOQL queries, maps can be populated from the results returned by the SOQL query. The map key should be declared with an ID or String data type, and the map value should be declared as an sObject data type. More on stackoverflow.com
๐ŸŒ stackoverflow.com
๐ŸŒ
Stack Overflow
stackoverflow.com โ€บ questions โ€บ 35866260 โ€บ apex-retrieving-records-from-a-type-of-mapsobject-listsobject
salesforce - Apex - Retrieving Records from a type of Map - Stack Overflow
Map<ID, List<Leads>> leadMap = Map<ID, List<Leads>>(); Then in your query where you build the map you ensure that your Lead also includes the Account__c field. Any of these options should work, it all depends on how this code snippet in being ...
๐ŸŒ
Reddit
reddit.com โ€บ r/salesforce โ€บ cast map to map
r/salesforce on Reddit: Cast map<id, sObject> to map<id, opportunity>
December 10, 2015 -

Hi all,

I am working on an after update trigger for opportunities. I would like logic to only execute on opportunities after a specific field has been changed. I'm using a trigger framework that uses generic sObject maps, so my logic is initiated as:

public static void oppTrigger (Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {}

What I would normally do if I had two maps of opportunities is:

map<id, opportunity> updatedOpps = new map<id, opportunity>();
for (opportunity o: newItems) {
    if (o.value != oldItems.get(o.id).value) {
        updatedOpps.put(o.id, o);
    }

Any advice on how to do this with generic sObject maps?

Find elsewhere
Top answer
1 of 2
1

I duplicated this (V58)

  • my IDE (Illuminated Cloud) says ParentId is an invalid symbol but Apex compiler accepts it;
  • at runtime, if list size is one, returns the 0th element.ParentId;
  • if list size is > 1, returns runtime error System.QueryException: List has more than 1 row for assignment to SObject

In the code example you showed, it would only work if you knew that values() always returned exactly one row, never two or more. This is not great practice as one should be thinking about Map class method values() as always returning a list of size 0,1,2, ... These elements could be primitives, apextypes, or sobjects depending on the Map's declaration.

  • I'd be more explicit about it by doing:

    // only care about 1st value
    params.put('cCaseId', caseIdToFeedItemMap.values()[0].ParentId); 
    
2 of 2
3

What you've run into is a feature that's documented as Using SOQL Queries That Return One Record. Basically, there are some cases where you can assign a list that has a single item in it to a scalar value. Despite what the documentation says, you can use this in a variety of ways that are not specifically called out in the documentation, notably that this is a feature baked into the language itself, and not specifically to SOQL.

To understand what's going behind the scenes, there is an object that looks like a QueryResult. Apex uses this object transparently in order to facilitate the ability to behave the way it is documented. You'll see this in times when you JSON.serialize a query directly, and you'll find it has some specific behaviors. This object tends to not be null, and has a language-specific feature that allows you to access the first element if, and only if, exactly one item is in the list. It is also responsible for facilitating query cursors when there are too many results to fit in memory all at once.

As a simple example, let us write the following code:

Contact c = (Contact)JSON.deserialize(
    '{"Cases":{"done": false, "totalSize": 0, "records": []}}',
    Contact.class
);
Case[] cases = c.Cases;

When this code runs, we get the exception:

System.QueryException: Aggregate query has too many rows for direct assignment, use FOR loop

This happened because the underlying QueryResult object detected that the list was incomplete ("done": false).

Similarly, if we have exactly one entry, we can use the "Using SOQL Queries That Return One Record" behavior.

Contact c = (Contact)JSON.deserialize(
    '{"Cases":{"done": true, "totalSize": 1, "records": [{"Subject":"Need Help"}]}}',
    Contact.class
);
Case theCase = c.Cases;
System.debug(theCase.Subject); // Need Help

This code does not crash, as there is exactly one record to return. As long as the internal QueryResult meets the conditions that (1) done is true, (2) totalSize is 1, and (3) records contains exactly one record, you can use this behavior.

Interestingly, Map.values() appears to return a QueryResult object internally. This is inconsistent with the documentation for Map. However, I think this was documented this way intentionally, since we aren't necessarily meant to "know" what's going on in the backend. However, we know that this must be true, because the following code also throws the QueryException from above:

Map<Id, Case> cases = new Map<Id, Case>();
Case theCaseValues = cases.values();

If Map.values() did not return this QueryResult, then the language would not work according to its documented behavior. After all, we can't assign a normal list this way:

Case[] cases = new Case[0];
// Compiler error: Illegal assignment from List<Case> to Case
Case theCase = cases;

It is a shame that we only have the one page in the documentation that suggests this is possible. While it is not strictly "undocumented," I would say that it is poorly documented. Apex is working in the manner that it was documented in. Now that you know this feature exists, and has literally existed since at least the first public release of Apex, you can understand why it works.

I'd like to add that the Apex Language Server does not throw an error in VS Code when you write code that looks like this. The bug here is that any IDE that does not use the official Apex Language Server may be inconsistent with the actual implementation of Apex. Illuminated Cloud simply has a detail wrong about the underlying implementation, and I wouldn't blame them, as this is a rather niche construct.

Also, unless it is very explicitly proven to be a bug, which I don't think this is, this is more of a documentation omission. Salesforce R&D typically maintain that the behavior produced by the server is the correct behavior, and that the documentation is incorrect, instead.

That said, if you're going to use the behavior intentionally, you should leave a comment about how there's only ever one record returned, etc. You usually want to be more specific by selecting the first index, if that is your intent (someValues[0]).

๐ŸŒ
JanBask Training
janbasktraining.com โ€บ community โ€บ salesforce โ€บ how-to-get-my-desired-map-id-list-sobject-salesforce
How to get my desired Map id list sobject salesforce? | JanBask Training Community
April 17, 2023 - you can use below code to get your desired Map id list sobject salesforce: - for(Account_Territory_Loader_vod__c atl: atlList) { if(accATLMap.containsKey(atl.account_vod__c) && accATLMap.get(atl.account_vod__c) != null) { List lst_terr = accATLMap.get(atl.account_vod__c); lst_terr.add(atl); accATLMap.put(atl.account_vod__c,lst_terr); } else { accATLMap.put(atl.account_vod__c, new List {atl}); } } I hope it solves your purpose.
Top answer
1 of 3
4

If you open up the details of the error message, it probably says Invalid conversion from runtime type Map<Id, SObject> to Map<Id, Segment__c> Class. You can't convert Maps with a different type specialization, even if the changed types are convertible. In the particular case of converting a Map<Id, sObject> to a Map<Id, Specific_SObject__c> where the key is the Id of the sObject value, you could do this by taking advantage of the List constructor for Map:

Map<Id, Segment__c> noLockedRecordsMap = new Map<Id, Segment__c>(
           (List<Segment__c>)new LockedRecordHandler().removeLockedRecords(newMap).values()
       );

In all other cases, you should loop through the map and put its key-value pairs in a new map:

Map<Id, Segment__c> noLockedRecordsMap = new Map<Id, Segment__c>();
Map<Id, SObject> lockedRecordsSObject = new LockedRecordHandler().removeLockedRecords(newMap);
for(Id key : lockedRecordsSObject.keySet())
{
    noLockedRecordsMap.put(key, (Segment__c)lockedRecordsSObject.get(key));
}

This could probably also be done by doing round-trip JSON, but the only advantage to that is saving lines of code, most likely it won't be faster. Hopefully in the future Apex will support methods with generic arguments so that it can return a map of the correct type and this won't be necessary.

2 of 3
2

You should not try to put a generic type in to a concrete type. This can cause bugs. The ideal situation is to create a copy so you retain the concrete type:

public Map<Id, SObject> removeLockedRecords(Map<Id, SObject> newMap) {
     // Make a copy //
     Map<Id, SObject> returnMap = newMap.clone();
     // Clear out existing key/values in copy (not original) //
     returnMap.clear();
     for (SObject s : newMap.values()) {
         if (!(Boolean)s.get('Locked__c')) {
             returnMap.put((Id)s.get('Id'), s);
        }
     }
     return returnMap;
}

By using clone, you preserve the original type that was passed in, and then your cast won't fail in the end.

Top answer
1 of 2
5

The special map constructor only handles the case of creating a map where the key is the ID and the value is the SObject.

To do what I think you want to do requires a loop:

Map<Id, List<Custom__c>> m = new  Map<Id, List<Custom__c>>();
for (Custom__c c : [
        SELECT Lookup_To_Custom__c, Id, Name
        FROM Custom__c
        WHERE Lookup_To_Custom__c != null
        ORDER BY Name
        ]) {
    List<Custom__c> l = m.get(c.Lookup_To_Custom__c);
    if (l == null) {
        l = new List<Custom__c>();
        m.put(c.Lookup_To_Custom__c, l);
    }
    l.add(c);
}

Note that using the foreign key field Lookup_To_Custom__c is a little more direct than going through the reference and taking the ID Lookup_To_Custom__r.Id.

2 of 2
4

This is certainly possible. You just need to build the map yourself instead of trying to create it form a a single query.

As an example lets say we have a custom lookup on the contact record to another contact, say 'Emergency Contact'. Its possible that a person is listed as the emergency contact for more 1 person, so we could create a map where the key is the contact id, and the value is actually a list of contacts in which they are the emergency contact for.

Obviously this is a bit different from your case, but the same idea and you should easily be able to adjust the code for your scenario.

Map<Id, List<Contact>> conMap = new Map<Id, List<Contact>>();
for(Contact parent : [Select Id, Name, (Select Id, Name From EmergencyContacts__r) From Contact]) {
    conMap.put(parent.Id, new list<Contact>());
    for(Contact child : parent.EmergencyContacts__r){
         conMap.get(parent.Id).add(child);
    } 
}
system.debug('My Map: ' + conMap);

You now have a map where a contact Id is the key, and a list of contacts that person is the emergency contact for is the value.

Hope that helps

EDIT

Thinking about it more, you could likely make trim this down a bit more and just do this as well.

Map<Id, List<Contact>> conMap = new Map<Id, List<Contact>>();
for(Contact parent : [Select Id, Name, (Select Id, Name From EmergencyContacts) From Contact]) {
    conMap.put(parent.Id, parent.EmergencyContacts__r);
}

system.debug('My Map: ' + conMap);
๐ŸŒ
GitHub
gist.github.com โ€บ emoran โ€บ 4ea945ec3db32cc8c06b
Adding list values to apex map<String,List<Sobject>>
Map<Id, List<Id>> userRoleToUsers = new Map<Id, List<Id>>(); for(User newUser : [SELECT UserRoleId FROM User LIMIT 50000]) { if(userRoleToUsers.containsKey(newUser.UserRoleId)) { userRoleToUsers.get(newUser.UserRoleId).add(newUser.Id); } else { userRoleToUsers.put(newUser.UserRoleId, new List<Id> { newUser.Id }); } }
๐ŸŒ
Salesforce Developers
developer.salesforce.com โ€บ forums
Discussion Forums Migration FAQs | Salesforce Developers
February 11, 2010 - On December 4, 2023, Salesforce Developers discussion forums joined the Trailblazer Community. Learn more with our FAQs!