0

So I'm a little confused on how to do this. I'm trying to make a scheduled apex job to do some cleanup.

  • A) On Account I am looking for Accounts with Type != "Type1" and Type != "Type2"
  • B) Based on A) I want to find Contacts with a field "Lead_Type__c" of "ABC" that are in the accounts from A

Then I want to set Accounts.Type="Type1" for the unique account ids found in B.

Then another piece of code for all the Contacts with field "Lead_Type__c" of "DEF" that are in the accounts from A minus any account we just processed.

Then set those remaining accounts to "Type2" from the remaining unique account ids we just located.

I'm just quite confused on how to accomplish this keeping clear of my potential governor limits.

Based on I have a field on contact "Lead_Type__c" that is either "ABC" or "DEF".

1 Answer 1

0

All right (assuming you're not asking this for a homework assignment), let's give this a go!

Account[] Accs   = [SELECT Id FROM Account WHERE Type != 'Type1' AND Type != 'Type2' AND Ignore_Type_Update__c = false AND CreatedDate > :Date.today().addDays(-30) ORDER BY LastModifiedDate DESC LIMIT 50];
Set<Id>   AccIds = (new Map<Id, Account>(Accs)).keySet();
Set<Id>   IgnIds = new Set<Id>(AccIds);
Contact[] Cons   = [SELECT AccountId, Lead_Type__c FROM Contact WHERE (Lead_Type__c = 'ABC' OR Lead_Type__c = 'DEF') AND AccountId IN :AccIds];
Contact[] Rems   = new List<Contact>();
Map<Id, Account> UpdateAccounts = new Map<Id, Account>();

// Find the Type1 Accounts
for (Contact Con : Cons) {
    Id AccId = Con.AccountId;
    if (Con.Lead_Type__c == 'ABC') {
        if (!UpdateAccounts.containsKey(AccId)) UpdateAccounts.put(AccId, new Account( Id = AccId, Type = 'Type1' ));
    } else {
        Rems.add(Con); // all these will have 'DEF'
    }
}

// Find the Type2 Accounts
for (Contact Con : Rems) {
    Id AccId = Con.AccountId;
    if (!UpdateAccounts.containsKey(AccId)) UpdateAccounts.put(AccId, new Account( Id = AccId, Type = 'Type2' ));
}

// Ignore Remaining Accounts
IgnIds.removeAll(UpdateAccounts.keySet());
for (Id IgnoreId : IgnIds) UpdateAccounts.put(IgnoreId, new Account( Id = IgnoreId, Ignore_Type_Update__c = true );

if (!UpdateAccounts.isEmpty()) Database.update(UpdateAccounts.values(), false); // don't fail all accounts if only one has an error

So I think the above meets your criteria. I accomplished this with 2 SOQL queries (originally had 3, but was able to condense the 2 contact queries you mentioned into one). The key to avoiding governor limits is to do all of your queries and DML (database updates) outside of loops and recursion. That basically means that you have to become comfortable with the three major collections that Salesforce provides -- lists, sets, and maps.

Lists contain an ordered list of data (typically SObject records) that you can iterate through. Sets are similar in that they can be used in for loops, but they are unordered, and all elements are guaranteed to be unique. So if you add a duplicate value, the set doesn't change. The primary benefit of sets is efficient lookup -- you don't have to loop through a whole list to find if it contains a value, you can just use the set's "contains" method to check. Maps are also used for efficient lookup, but they differ from sets in that they "map" one value to another. So you provide a "key" (like an id), and map it to a "value" (like a record). To get a record associated with a particular id, you use the map's "get" method. This saves you from having to loop through an entire list of records to find something.

So in the logic above, we query all relevant accounts, and load their ids into a set. You can do this with a for loop, but I used a shortcut that comes with first converting the list to a map, then getting the set of keys. The account query is the greatest point of risk for your scheduled job, because it will become less performant, the more invalid data you have. For example, if you have a Type1 or Type2 account which does not have any contacts (for whatever reason, perhaps a dataload rather than a lead), then your account query may get stuck querying those same invalid accounts over and over again, blocking the real ones from being processed. So I added a field to the Account object to denote that it was already processed, and can now be ignored. Additionally, it only looks at accounts created in the last 30 days, because CreatedDate is an indexed field (meaning it's very efficient for the database to query accounts when this field is part of the conditions). For scheduled jobs, it's a good idea to base the initial query on an indexed field, so that the query doesn't time out. Once you're working with a limited set of ids, that isn't usually a problem (since id fields are also indexed). Lastly, I ordered by LastModifiedDate to help ensure that the most recently modified accounts are the ones that are processed. That way, if any invalid accounts do hang up the process, they should slowly slip to the back of the line.

Once we have our account ids, I create a duplicate set of ids to contain all the accounts we'll ignore. Later we'll remove matched accounts from this set. Next we query contacts with the two lead types in question, and define another list of contacts for those who are remaining after the first pass. Final piece of the initialization code, we define a map of accounts so that we can keep track of what accounts have already been matched.

Then we run the first pass over the contacts. If a contact does not match 'ABC', then it's skipped, and added to the list of remaining contacts (for the second pass). If it does match, then it checks to see if the account has already been flagged for updating, and if not, flags it to update the Type to Type1.

Then we perform a second pass over the remaining contacts. Because the query only included 'ABC' and 'DEF', we know these are all 'DEF'. So every first occurrence of an account id flags the account to update to 'Type2'.

Finally, we determine which account ids were pulled, which did not find a matching contact. These need to be ignored in the next pass of the scheduled job, so these are flagged to set the Ignore_Type_Update__c to true. Then, assuming any accounts were found by the job, it updates them.

2
  • Lol no not for homework. So I can't seme to get it to run. "Method does not exist or incorrect signature: void has(Id) from the type Map&lt;Id,Account&gt;" "for (Contact Con : Rems) { Id AccId = Con.AccountId; if (!UpdateAccounts.has(AccId)) UpdateAccounts.put(AccId, new Account( Id = AccId, Type = 'Partner' )); }"
    – Justin
    Apr 2, 2021 at 13:12
  • Mixing up my languages lol (.has is JavaScript). Updated answer to use "containsKey". There may be other compilation issues; I typed that without recreating in my orgs Apr 2, 2021 at 14:08

You must log in to answer this question.

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