I have a trigger on lead after insert that query all accounts and all contacts created the past 30 days to compare 4 fields (external id and legal number on accounts / mobile and email on contacts) and automatically convert the lead if a match is found.
I'm a beginner with apex and, even if I have the expected result, I think the way I wrote my code is bad since I get "Apex CPU time limit exceeded" error triggered sometimes on lead insert (not always).
trigger LeadDeduplicate on Lead (after insert) {
List<Account> accounts = [SELECT ID, Account_ID__c, Siret__c, OwnerId FROM Account WHERE Account_ID__c != NULL AND CreatedDate > :System.Today() - 30];
List<Contact> contacts = [SELECT ID, Email, MobilePhone, AccountId, OwnerId FROM Contact WHERE CreatedDate > :System.Today() - 30];
Boolean leadIsConverted = false;
for(Lead lead : trigger.new) {
// Trigger only if the lead comes from external
if(lead.IsExternal__c) {
for(Account account : accounts) {
if(!leadIsConverted) {
if(Utils.hasTheSameAccountId(lead, account) || Utils.hasTheSameSiret(lead, account)) {
Utils.mergeLeadWithAccount(lead, account);
leadIsConverted = true;
}
}
}
if(!leadIsConverted && !contacts.isEmpty()) {
for(Contact contact : contacts) {
if(!leadIsConverted) {
if(Utils.hasTheSameEmail(lead, contact) || Utils.hasTheSameMobile(lead, contact)) {
Utils.mergeLeadWithContact(lead, contact);
leadIsConverted = true;
}
}
}
}
}
leadIsConverted = false;
}
}
Utils
being the apex class used for matching and conversion.
Anyone out there know a workaround to avoid this error ?
Any help would be appreciated.
UPDATE : Thank you both for your first answers, I'll dig into this and come back if I find a solution. I also add my Utils class to provide more context.
public class Utils {
public static boolean hasTheSameAccountId(Lead lead, Account account) {
return acccountIdIsNotNull(lead, account) && account.Account_ID__c == lead.Account_ID__c;
}
public static boolean hasTheSameSiret(Lead lead, Account account) {
return siretIsNotNull(lead, account) && account.Siret__c == lead.Siret__c;
}
public static boolean acccountIdIsNotNull(Lead lead, Account account) {
return account.Account_ID__c != null && lead.Account_ID__c != null;
}
public static boolean siretIsNotNull(Lead lead, Account account) {
return account.Siret__c != null && lead.Siret__c != null;
}
public static boolean hasTheSameEmail(Lead lead, Contact contact) {
return emailIsNotNull(lead, contact) && lead.Email == contact.Email;
}
public static boolean hasTheSameMobile(Lead lead, Contact contact) {
return mobileIsNotNull(lead, contact) && Utils.mobilePhoneWithoutCodeCountry(lead.MobilePhone) == Utils.mobilePhoneWithoutCodeCountry(contact.MobilePhone);
}
public static String mobilePhoneWithoutCodeCountry(String phoneNumber) {
return phoneNumber.substring(phoneNumber.length() - 9, phoneNumber.length());
}
public static boolean emailIsNotNull(Lead lead, Contact contact) {
return lead.Email != null && contact.Email != null;
}
public static boolean mobileIsNotNull(Lead lead, Contact contact) {
return lead.MobilePhone != null && contact.MobilePhone != null;
}
public static void mergeLeadWithAccount(Lead lead, Account account) {
Database.LeadConvert lc = new Database.LeadConvert();
lc.setLeadId(lead.Id);
lc.setConvertedStatus('Qualifié');
lc.setDoNotCreateOpportunity(true);
lc.setAccountId(account.Id);
lc.setOwnerId(account.OwnerId);
try {
Database.LeadConvertResult lcr = Database.convertLead(lc);
System.assert(lcr.isSuccess());
} catch (DmlException e) {
System.debug('The following exception has occurred: ' + e.getMessage());
}
}
public static void mergeLeadWithContact(Lead lead, Contact contact) {
Database.LeadConvert lc = new Database.LeadConvert();
lc.setLeadId(lead.Id);
if(contact.AccountId != null) {
lc.setConvertedStatus('Qualifié');
lc.setDoNotCreateOpportunity(true);
lc.setAccountId(contact.AccountId);
lc.setOwnerId(contact.OwnerId);
lc.setContactId(contact.Id);
try {
Database.LeadConvertResult lcr = Database.convertLead(lc);
System.assert(lcr.isSuccess());
} catch (DmlException e) {
System.debug('The following exception has occurred: ' + e.getMessage());
}
}
}
}
Best Answer
Debugging a CPU time issue is probably one of the more frustrating things to try to track down and resolve.
One thing to keep in mind here is that the line of code that your error points to isn't necessarily the problem, it's just where you happened to go over the CPU limit.
Measuring CPU time usage is more of a pain than, say, Query Rows (since each query that gets run tells you how many rows were returned). You can look at the USAGE_LIMITS statements in your debug log to get a rough idea of what's taking time to run, but beyond that I think you just need to add some
System.debug(Limits.getCPUTime());
to track down where your issue lies.It might be important to see your
Utils
class (at least the code and appropriate context for the methods that you're using) too, but I get the feeling that the issue is in how you're looping over records.Code that looks like the following can be a red flag:
If you have 200 records in trigger.new, and 400 records from the other query, you end up iterating 200 * 400 = 80,000 times. Even if the work you're doing in each iteration is small/fast, when you get into tens of thousands of iterations, it starts to add up.
The normal recommendation to get around that is to make use of collections, and iterating over the object you're trying to compare against in a loop outside of the loop over your other records.
With this approach, you only end up looping 200 + 400 = 600 times.
For your checks against
Account
, you can pretty much use the second code snippet as-is (make yourself aMap<Id, Account>
, loop over Account records to populate that map). The call of your Utils class to check for the same Id could be removed. You can get the specific Account to pass to your other Utils method call through the map (if it exists).The check against
Contact
records needs a little creativity to make the pattern we used for theAccount
checks work, but only just a little creativity.While it's generally not recommended to use SObjects as the key in a Map or Set, doing just that is very helpful if you're trying to match multiple fields on an SObject (i.e. making something similar to a composite key).
The general idea is this:
Yeah, there's some extra hoops to jump through there, but the important point is that this allows you to avoid doing a bunch of extraneous looping when you don't have a unique identifier to use in a comparison (as is the case with your check against
Contact
records)Bonus tips
There's an (arguably) easier way to get records from a specific period of time using Date literals.
Specifically,
WHERE CreatedDate >= N_DAYS_AGO:30
N_DAYS_AGO
might not be documented, but it is available. The closest documented date literal to that would beLAST_N_DAYS
You also might want to check your
leadIsConverted = false
towards the end of your code. It might undo the work you're trying to do (it would set all your leads to leadIsConverted = false)