The main thing to watch out for is that you avoid anti-patterns like querying in a loop. The way you do this is by using collections such as Set
and Map
. In this case, you would want your set elements and map keys to be email addresses. Because this logic will perform cross-object updates (or inserts), you should perform the logic after insert
and after update
.
There are also good reasons to use a handler pattern. I tend to use a slightly simpler approach as outlined below. I also keep all of the "how" questions out of the handler so I don't have to test it directly, just the trigger and the service.
Trigger
trigger Account on Account (after insert, after update)
{
AccountTriggerHandler handle = new AccountTriggerHandler(trigger.new, trigger.oldMap);
if (trigger.isAfter)
{
if (trigger.isInsert) handle.afterInsert();
if (trigger.isUpdate) handle.afterUpdate();
}
}
Handler
public with sharing class AccountTriggerHandler
{
final List<Account> newRecords;
final Map<Id, Account> oldMap;
public AccountTriggerHandler(List<Account> newRecords, Map<Id, Account> oldMap)
{
this.newRecords = newRecords;
this.oldMap = oldMap;
}
public void afterInsert()
{
AccountServices.associateContacts(newRecords);
}
public void afterUpdate()
{
AccountServices.associateContacts(AccountServices.hasEmailChanged(newRecords, oldMap));
}
}
Service
public with sharing class AccountServices
{
public static List<Account> hasEmailChanged
(List<Account> newRecords, Map<Id, Account> oldMap)
{
List<Account> changed = new List<Account>();
for (Account account : newRecords)
{
if (account.Custom_Email__c != oldMap.get(account.Id).Custom_Email__c)
changed.add(account);
}
return changed;
}
public static void associateContacts(List<Account> accounts)
{
Set<String> addresses = new Set<String>();
for (Account account : accounts) addresses.add(account.Custom_Email__c);
// now you know all the email addresses
// you can use this collection to get every Contact in one query
Map<String, Contact> emailToContact = new Map<String, Contact>();
for (Contact contact : [
SELECT Custom_Email__c FROM Contact WHERE Custom_Email__c IN :addresses
])
{
emailToContact.put(contact.Custom_Email__c, contact);
}
//now you can find a Contact by its email address
for (Account account : accounts)
{
String address = account.Custom_Email__c;
Contact contact = buildContact(account, emailToContact.get(address));
emailToContact.put(address, contact);
}
upsert emailToContact.values();
// you should really wrap this in a try/catch
// I'll leave that code up to you
}
static Contact buildContact(Account account, Contact existing)
{
Contact contact = new Contact(
AccountId = account.Id,
Custom_Email__c = account.Custom_Email__c
);
if (existing != null) contact.Id = existing.Id;
return contact;
}
}
As for how to handle any DmlException
thrown by your update
call, you can see how @sfdcfox would approach it in this thread. He is the most prolific poster in this community and the approach he outlines is canonical. You can also see an official introduction to the problem here.