Preface: this post is part of the Bulkify Your Code series.
Here’s a situation you’ll find yourself in almost every properly written trigger:
Here’s the same situation illustrated in code – it’s also where we left off in this chapter’s trigger:
// One giant SOQL query with all potential Case creators (users) List<User> potentialUsers = // SELECT Id, Email FROM User... for (Case newCase : Trigger.new) {// Match the case to the correct User from potentialUsers // Match when newCase.SuppliedEmail equals the User's email}
The incorrect way to solve this: Loops.
Don’t use loops because they’re inefficient and may introduce other Governor Limits!
// One giant SOQL query with all potential Case creators (users) List<User> potentialUsers = // SELECT Id, Email FROM User... for (Case newCase : Trigger.new) {// Don't do this! Using Loops to find a match is inefficient for (User u : potentialUsers) { if (newCase.SuppliedEmail.equals(u.Email)) { newCase.CreatedById = u.Id; } }}
If your SOQL query returned 10,000 records and you had 200 records in your trigger – Apex would have to run 2,000,000 statements in the background! You’d either hit some governor limit or your code would slow down your org.
The correct way to solve this: Use Maps!
// One giant SOQL query with all potential Case creators (users) List<User> potentialUsers = // SELECT Id, Email FROM User...// Create a Map that lets you search for Users by their Email Map<String, User> emailToUserMap = new Map<String, User>(); for (User u : potentialUsers) { emailToUserMap.put(u.Email, u); }// Get the matching user by Email in just one line of code! for (Case newCase : Trigger.new) { User creator = emailToUserMap.get(newCase.SuppliedEmail); newCase.CreatedById = creator.Id; }
And that’s all there is to it folks! You now know how to bulkify your code and beat the #1 most important Governor Limit!
Just for your reference, here’s what the final trigger looks like:
// Email-to-Case always defaults the creator - this fixes that! trigger FixCreator on Case (before insert) { // Step 1: Create a set of all emails of Users to query Set<String> allEmails = new Set<String>(); for (Case newCase : Trigger.new) { if (newCase.SuppliedEmail != null) { allEmails.add(newCase.SuppliedEmail); } } // Step 2: Query for all the Users in Step 1 List<User> potentialUsers = [SELECT Id, Email FROM User
WHERE Email IN :allEmails];// Step 3: Make a Map that lets you search for Users by Email Map<String, User> emailToUserMap = new Map<String, User>(); for (User u : potentialUsers) { emailToUserMap.put(u.Email, u); }// Step 4: Get the matching user in the Map by Email! for (Case newCase : Trigger.new) { if (newCase.SuppliedEmail != null) { User creator = emailToUserMap.get(newCase.SuppliedEmail); if (creator != null) { newCase.CreatedById = creator.Id; } } }}
Next post: When to use before vs after triggers!
But I am getting an error here “Field is not writeable: Case.CreatedById”.
So shall i use Soql on Case object for that ?
Check if you have create audit fields permission
Heyoo!
I need to send a get request and retrieve information via this site: https://www.impots.gouv.fr/verifavis/
and update fields of my lead with the value retrieved from this site
someone already had this case?
Hi David,
I am getting some not writable compile time error for the field Case.CreatedById in my case for above code.
Could you please tell me what’s wrong in that.
Hey, I think you may be able to do the same with 1 less loop… Check it out :) :
// Accept passed in Trigger.new list…
public static void emailtoCaseFieldPopulation (List newList) {
//Declare Map of emails to Cases
Map emailToCase = new Map();
//Construct the Map
for(Case c : newList) {
if (c.SuppliedEmail != null) {
emailToCase.put(c.SuppliedEmail,c);
}
}
//For users that match the email key in the map, set the case c equal to //the case value received from the provided email and then set the case //values equal to the User record in the loop
for (User u : [SELECT Id, Title, Enterprise_User_Type__c, Email FROM User WHERE Email IN :emailToCase.keyset()]) {
Case c =emailToCase.get(u.email);
if (c != null) {
c.Sales_Channel__c = u.Enterprise_User_Type__c;
c.Title__c = u.Title;
}
}
}
Hi David,
:) Seriously I am Die hurt fans of yours <3
HI David,
Can you share any scenario like above trigger for sales cloud ? So i can bulkify and work on it, i understood the concept but not familiar with service cloud. just an scenario….
If a Stage Field on Opportunity object is “Closed Won”
Then send an automated “welcoming mail” to account owner.
H David. I am getting an error — case.createdbyID is not writable. I have changed the sharing rules to read/write. Please help.
See this post! The ability to edit that field is turned off by default:
https://www.sfdc99.com/2013/11/23/the-three-most-common-governor-limits-and-why-youre-getting-them/
I’m confused as to what this trigger accomplishes? I can see what it does, but I don’t understand why it’s doing it. Or maybe that’s not important/relevant to the example and I should just focus on the code and theory aspects? Sorry if it’s not, I just tend to have a hard time understanding things without understanding the purpose in addition to how and why they work.
Check out this earlier post!
https://www.sfdc99.com/2013/11/23/the-three-most-common-governor-limits-and-why-youre-getting-them/
I’m having a hard time getting this trigger “outside the loop’, can you someone help?
trigger triggerAccountContact on Opportunity (before insert, before update) {
List b;
b = [select name, AccountId from Contact];
Map m = new Map();
for(Contact b1: b){
m.put(b1.id, b1);
}
for(Opportunity a1: Trigger.new){
if(a1.SalesProfessional__c!=null){
a1.accountid=m.get(a1.SalesProfessional__c).accountid;
}
}
}
Hi Chris
in reply to your question
I’m having a hard time getting this trigger “outside the loop’, can you someone help?
trigger triggerAccountContact on Opportunity (before insert, before update) {
List b;
b = [select name, AccountId from Contact];
Map m = new Map();
for(Contact b1: b){
m.put(b1.id, b1);
}
for(Opportunity a1: Trigger.new){
if(a1.SalesProfessional__c!=null){
a1.accountid=m.get(a1.SalesProfessional__c).accountid;
}
}
}
In my understanding you have a custom field containing a contact id on the opportunity
I wonder why Contacts in your case are the Sales professionals.
You general select name , accountId from Account into a list will also pull duplicates and ALL contacts
this is not best practice, What are you doing if you have 5 mill contacts in your database?
Then you iterate through that list and put them into a map still including duplicates.
After you have sorted that you iterate through the trigger to change the by default mandatory Account on the opportunity.
Anyway , I would need to learn your customisation in full before i make an assessment on this structure
You r main issue in that trigger is the global select on all contacts
you should select only those where the salesproffesional__c is not Null
trigger triggerAccountContact on Opportunity (before insert, before update) {
map m = new map() // String, opportunity ( only those where the salesprof is not null
for(opportunity O :trigger.new)
{
if(o.SalesProfessional__c != null){m.put(o.SalesProfessional__c,O)}
}
Now gather the contact info
map c= new map([select id,accountId from Contact Where Id In: m.keyset()]) // String, contact fetching only effected ids
using both maps to change the account ids
for(opportunity o1 : m.values()) //iterates the opps in the map
{
o1.accountid = c.get(o1.SalesProfessional__c).AccountId;
// since we have only those where salesprof is not null simply change the account id
}
}
this would need to be tested but from a principal it should work except you hit the 50k gov limit which that is why i said belong into a trigger handler where you can divert the process easy into a batch if you are hitting 50k opportunities in one go.
Hi David
After reading about using Sets and Maps for writing trigger to handle bulk insert, I tried following code for a custom object. Since I am getting Too many SOQL queries : 101. So I have removed most of the code. Here I am only posting the query with all Ids collected in data type Set. I am still getting Too many SOQL queries : 101 error on SELECT Query. I am not able to understand whats wrong. Could you please help?
trigger CreateFtnsClsSchedule on Fitness_Class__c (before insert) {
Set allIds = new Set();
for (Fitness_Class__c newClass : Trigger.new) {
allIds.add(newClass.Id);
}
List ftClassList = [SELECT Id FROM Fitness_Class__c
WHERE Id IN :allIds];
}
List ftClassList = [SELECT Id FROM Fitness_Class__c WHERE Id IN :allIds];— in this line I missed type casting. I am using below line in the code
List ftClassList = [SELECT Id FROM Fitness_Class__c WHERE Id IN :allIds];
Finish this chapter and you’ll see!
List ftClassList = [SELECT Id FROM Fitness_Class__c
WHERE Id =:allIds];
Hi David, I’m struggling to implement the the following logic : I delete the records from the junction table OpportunityProject . The table contains Project Id and Opportunity Id. Project might be associated with many Opportunity Ids in the junction table. One of the Opportunity ids is stored as a primary opty id in the Project table. Here is what I’m trying to achieve on after deleted – create a old map of the deleted from junction table records and then query Project table by oldmap.projectid to find out if the old. opporunityid is a primary. If the statement is true, I need to add the record to map2 ( project id, opportunity id). Then i will need to update primary opportunity based on the latest logic.
Hopefully a reader can help you out here!!
David,
Thank you so very much. I struggled to create my own mapping trigger for hours yesterday. Following your example with explanations and duplicating the logic has worked perfectly.
Cheers
Craig
Well done Craig!
Can anyone tell me what will happen if we execute this trigger?
Hi David –
I tested the one trigger both with list + combined query and map.
With List + Combined Query, it taking less time compared to Map. I think list operation is faster than Map. What do you think?
It might depend on how you use the map!
Lists in general are worse because:
1. They potentially hit multiple governor limits
2. They are more unpredictable in their speed
So maybe it’s quicker in one test, but in a different test with different parameters (ie more records) a map would be faster.
David
I am complete lost on bulkifing this code. We are trying to remove the RSM and BDM from the opportunity record and just pull them from the Account record where they should live. I have tried maps, list, everything seems to get me a step closer but never quiet works. Any suggestions? The original code up to that point is
public with sharing class ChatterPost {
public void OppsToPost(list opps){
//String to hold the body of the Chatter Post//
String postBody;
system.debug(‘OPPS PASSED TO POST SIZE:’ + opps.size());
for(Opportunity opp : opps){
postBody = ‘\n\nOpportunity: ‘ + opp.Name + ‘ has been set to ‘ + opp.StageName + ‘.’ + ‘\n\nStage Reason: ‘ +
opp.Stage_Reason__c + ‘\n\nNotes/Next Steps: ‘ + opp.NextStep + ‘\n\nEstimated Annualized Volume (lbs): ‘ +
opp.Annualized_Volume_lbs__c;
string RSMID = opp.RSM_Sales_Manager__c;
string BDMID = opp.BDM__c;
SFDC recommended putting a SOQL inside the for loop but after I said NO WAY I’ll hit the limit they haven’t answered me about it,
Try asking on the forums here, there’s a guy there I suspect would know how to do this =)
First time caller, long time listener hoping you could help. I’m trying to learn apex and trying to write a trigger on a Contact field title “Last Phone Call.” I want the value to return Last Activity (minus future dates) for the completed Tasks.
I keep getting the error : Variable does not exist: t.whoId at line 25 column 54
trigger UpdateInTouchDate on Contact (before insert, before update) {
Set whoIds = new Set();
for (Contact c : Trigger.new)
{
whoIds.add(c.Id);
}
List tasks = [SELECT ActivityDate, whoId FROM Task
WHERE ActivityDate <= Today AND Id =: whoIds
ORDER BY ActivityDate DESC NULLS FIRST LIMIT 1];
Map taskMap = new Map();
for (Task t : Tasks)
{
taskMap.put(t.whoId, t.activitydate);
}
for (Contact newContact : Trigger.new)
{
if (newContact.lastactivity != null)
{
newContact.Last_Phone_Call__c = taskMap.get(t.whoId).ActivityDate; //magic ain’t happ’nin
}
}
}
Perfect documentation of your error!
You gave me the exact error message plus added a nice little comment to help me find the line. Thousands of comments on this site and you are the first to make it so easy for me, ha ha ha. THANK YOU!
The variable “t” only exists within your loop “(for t : Tasks)…” The “t” is a temporary variable only to be used in that loop, and once the loop is done, so is “t”.
So what you were probably actually trying to do was search taskMap for newContact.Id instead of t.whoId. Try it out =)
David
BTW “first time caller, long time listener” LOL!!!! Bravo!!!!
Hi David. I am new to Maps and Apex. Can you help me figure out why the code doesn’t get the account id and the account name from the map? It’s throwing the exception but showing the tracking number and the Oracle Order. I’m not too clear how how maps work when a small file with 2 columns is uploaded where the data is then stored in a List.
public class importDataFromCSVController {
public Blob csvFileBody{get;set;}
public string csvAsString{get;set;}
public String[] csvFileLines{get;set;}
public List trackList;
public List accToUpdate{get;set;}
List accOrderList;
//Controller
public importDataFromCSVController(){
//creates sojbect for csvFileLines which is an array of strings
csvFileLines = new String[]{};
//creates sobject for list of accounts
accToUpdate = new List();
}
public void importCSVFile(){
try{
csvAsString = csvFileBody.toString();
csvFileLines = csvAsString.split(‘\n’);
trackList = new List();
// List existingAccts = new List();
accOrderList = new List();
for(Integer i=1;i<csvFileLines.size();i++){
Account oneAcc = new Account() ;
string[] csvRecordData = csvFileLines[i].split(',');
oneAcc.Oracle_Order__c = csvRecordData[0];
oneAcc.tracking_number__c = csvRecordData[1];
accOrderList.add(oneAcc.Oracle_Order__c);
trackList.add(oneAcc);
}
List existingAccts = [SELECT Id,name,Oracle_Order__c,tracking_number__c FROM Account WHERE Oracle_Order__c IN :accOrderList];
Map orderNoToAccMap = new Map();
for (Account ao : existingAccts )
{
orderNoToAccMap.put(ao.Oracle_Order__c, ao);
}
for (Account cr: trackList)
{
if(orderNoToAccMap.containskey(cr.Oracle_Order__c))
{
Account id = orderNoToAccMap.get(cr.id);
Account name = orderNoToAccMap.get(cr.name);
Account Oracle_Order = orderNoToAccMap.get(cr.Oracle_Order__c);
Account tracking_number = orderNoToAccMap.get(cr.tracking_number__c);
}
accToUpdate.add(cr);
}
update accToUpdate;
}
catch (Exception e)
{
ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR,’An error has occurred while importing the data. Please make sure input csv file is correct.’);
ApexPages.addMessage(errorMessage);
}
}
}
That’s a lot of code to go through!
Try using this:
https://www.sfdc99.com/2014/02/22/debug-your-code-with-system-debug/
Thanks David! It was very helpful to help me debug the code. I made changes to it but looks like the update fails because I need the record id. I thought that by invoking orderNoToAccMap.values() it would add all the values from the query. Can you see what I am doing wrong?
List existingAccts = [SELECT Id,name,Oracle_Order__c,tracking_number__c FROM Account WHERE Oracle_Order__c IN :accOrderList];
Map orderNoToAccMap = new Map();
for (Account a : existingAccts )
{
if (a.Oracle_Order__c!=null)
{
orderNoToAccMap.put(a.Oracle_Order__c, a);
}
accToUpdate = orderNoToAccMap.values();
}
for (Account at: trackNumbsList)
{
if(orderNoToAccMap.containskey(at.Oracle_Order__c))
{
Account accid = orderNoToAccMap.get(at.Oracle_Order__c);
at.id= accid.id;
}
accToUpdate.add(at);
}
update accToUpdate;
I think what’s happening is you’re adding the same account to the list twice, and that is generating an error!
For example, at and accid are technically the same account, and you’re potentially adding it in the top and the bottom! That would definitely create an error in the code!
Try fixing that part, and if you do get another error message, post the exact results!
Hi David, I’m back so I was able to get it to work in my own DE because Order_Number__c was not read only but when I wrote the apex class in our dev sandbox I’m getting this error: “Error: Compile Error: Field is not writeable: Order__c.Order_Number__c at line 40 column 16”. How do assign the order number from the csv to Order_Number__c field.
public Void importCSVFile(){
try{
csvAsString = csvFileBody.toString();
csvFileLines = csvAsString.split(‘\n’);
for(Integer i=1;i<csvFileLines.size();i++){
string[] csvRecordData = csvFileLines[i].split(',');
Order__c singleOrder = new Order__c();
singleOrder.Order_Number__c = csvRecordData[0];
singleOrder.tracking_Number__c = csvRecordData[1];
singleOrder.Status__c = 'Complete';
trackingNumList.add(singleOrder);
}
for (Order__c ot : trackingNumList){
Map orderToTrackMap = new Map();
orderToTrackMap.put(ot.Order_Number__c,ot);
List queryOrder = [Select Id,Name,Tracking_number__c,Order_Number__c,Status__c FROM Order__c WHERE Order_Number__c IN :orderToTrackMap.keyset()];
more code………………….
Most likely it’s because you’re trying to update a field that’s on an record related to your variable. You can only update fields that are directly on your variable!
So instead of updating a.b.c, you need to update b.c
Hope this makes sense!
David, I modified the datatype to a list of String that allowed me to add the field to a list. Now I have a new error when I loop through the list of string (orderData). Please help. Error: Initial term of field expression must be a concrete SObject: String at line 55
Here’s the updated code:
csvAsString = csvFileBody.toString();
csvFileLines = csvAsString.split(‘\n’);
for(Integer i=1;i<csvFileLines.size();i++){
string[] csvRecordData = csvFileLines[i].split(',');
orderNumber = csvRecordData[0];
orderData.add(orderNumber);
trackingNumber = csvRecordData[1];
orderData.add(trackingNumber);
status = 'Complete';
orderData.add(status);
}
Map<String,List> orderToTrackMap = new Map<String,List>();
for (String ot : orderData){
orderToTrackMap.put(ot.orderNumber,ot); /*line 55 here*/
List queryOrder = [Select Id,Name,Tracking_number__c,Order_Number__c,Status__c FROM Order__c WHERE Order_Number__c IN :orderToTrackMap.keyset()];
for (Order__c qo : queryOrder){
qo.Tracking_number__c = ot.trackingNumber;
qo.Status__c = ot.status;
updatedRecords.add(qo);
System.debug(‘list of orders to update:’ + updatedRecords);
}
}
ahhhh where is line 55!
I commented the code where line 55 lies. See a closer look below.
for (String ot : orderData){
/* line 55 here*/ orderToTrackMap.put(ot.orderNumber,ot);
Ahhh – ot is a String not an object!
Correct because OrderData is a list of strings which consist of status, order number, and tracking number. If I loop though a list of strings, doesn’t my variable ot need to be of the same type? When I iterate i create a map and I need to set the order number in the list of strings as my key and then the other 2 strings in the list as the values. Does the map look correct?
Hmmm I recommend bringing this one up to the official or SFDC99 forums actually – I am totally exhausted and they can help you out much better!!
Sorry Star!!
Thanks for getting back to me =)
Hi David,
This is my test class to the FixCreator Trigger. Its giving 100% coverage. But pls let me know, any advice to make it better.
@isTest
public class emailtocase{
static testMethod void EmailtocaseMethd()
{
// created a user
User creator = new User();
creator.Username = ‘sukumari@gmail.com’;
creator.Email = ‘sukumari@gmail.com’;
creator.FirstName = ‘suku’;
creator.LastName = ‘Jyothi’;
creator.Alias = ‘sukus’;
creator.CommunityNickname = ‘petals’;
creator.ProfileId = ’00e90000001b52F’;
creator.TimeZoneSidKey = ‘GMT’;
creator.LocaleSidKey = ‘en_US’;
creator.EmailEncodingKey = ‘ISO-8859-1’;
creator.LanguageLocaleKey = ‘en_US’;
insert creator;
// created a cse with supplied email
Case case1= new Case();
case1.Status= ‘New’;
case1.Origin=’Email’;
case1.SuppliedEmail=’sukumari@gmail.com’;
insert case1;
// Everything is working??
List cases= [SELECT Id, CaseNumber FROM Case];
for (case c : cases) {
system.assertEquals(owner.Name,case1.Created_By_Id__c);
}
}
}
Hi David,
One question. As you said, there is no direct link between trigger and the test class. But after a “Run Test ” in the Test Class , the Code coverage of the expected trigger is getting changed. Could you explain this . Sorry for a stupid question , if it is ;)
You’re indirectly calling the trigger in your test class – but nowhere are you saying “run trigger xyz”!
Looks good to me =) Well done!
Dear David,
If I am having same email to multiple users, then I think this trigger will cause problem,
In step 4,
User creator = emailToUserMap.get(newCase.SuppliedEmail); // this will get multiple Users
Regards,
Swapnil
Hi Swapnil,
that will not cause any problem as in step 3 any duplicate is ignored, a key in a map does not allow identical values (duplicates)
to surround multiple emails with the same value you could extend the map by another string( casenumber) but then the whole logic needs to be fitted around
Map <string,new map>
map string, new map string,user
You can use that to search on username as well =)
Hi,
Can somebody explain me the this trigger which is wrote in this chapter.
Like for what functionality we have implemented.
Is it fine for the last line of the trigger need to replaced by
if(creator != NULL){
newCase.OwnerId = creator.Id;
}
Definitely OK – play it safe, I like it!
Hi,
I’m having some issues with my before insert trigger. First trigger with a map :-) I only want it to fire when the check box equals true but it’s firing when it is and is not. I’m not sure what I’m missing.
trigger PreventOrderTaskCreationonOrderItem on Order_Task__c (before insert) {
List orderPhaseIds = new List();
for (order_Task__c o: trigger.new){
orderPhaseIds.add(o.Order_Phase__c);
}
Map opMap = new Map();
if (orderPhaseIds.size() > 0) {
for (Order_Phase__c op : [select Order__Item__r.Order_Project__c from Order_Phase__c where Id in:orderPhaseIds]) {
opMap.put(op.Id, op);
}
}
for (order_Task__c o: trigger.new){
Order_Phase__c op = opMap.get(o.Order_Phase__c);
if(op != null && op.Order__Item__r.Order_Project__c == FALSE) {
o.addError(‘You cannot add an order project category to this item.’);
}
}
}
Hi Lisa,
Good progress. My suggestions is to change your if statement so that instead of using greater than to use isempty(). So this is what it would look like:
Curent:
if(orderPhaseIds.size()>0){}
New:
if(orderPhaseIds.isempty()==false{}
In other words, if the list is not empty (false) you want to do your operations. Hopefully this helps, but just another newie in case it doesn’t :)
Salaad
Hi Salaad,
You can shorten that
New:
if(orderPhaseIds.isempty()==false{}
NEW new
New:
if(!orderPhaseIds.isempty()){}
the exception mark in front negates the boolean result same as in the normal formula NOT()
You also missed one )
But you made a good point
Lisa I wouldn’t close the loop
if (orderPhaseIds.size() > 0) {
right after your filled map
i would close that at the end of your code
so if the map is empty the code will not be processed as it is not necessary
so like that
Map opMap = new Map();
if (!orderPhaseIds.IsEmpty()) {
for (Order_Phase__c op : [select Order__Item__r.Order_Project__c from Order_Phase__c where Id in:orderPhaseIds]) {
opMap.put(op.Id, op);
}
for (order_Task__c o: trigger.new){
Order_Phase__c op = opMap.get(o.Order_Phase__c);
if(op != null && op.Order__Item__r.Order_Project__c == FALSE) {
o.addError(‘You cannot add an order project category to this item.’);
}
}
}
}
Hi Lisa,
should this
for (Order_Phase__c op : [select Order__Item__r.Order_Project__c from Order_Phase__c where Id in:orderPhaseIds])
not more like
for (Order_Phase__c op : [select Id, Order__Item__r.Order_Project__c from Order_Phase__c where Id in:orderPhaseIds])
(to have the ID in the select? before put it into the map)
If I get you right you want to add the addError if the checkbox is false.
in the last part
for (order_Task__c o: trigger.new){
Order_Phase__c op = opMap.get(o.Order_Phase__c);
if(op != null && op.Order__Item__r.Order_Project__c == FALSE) {
o.addError(‘You cannot add an order project category to this item.’);
how about trying
for (order_Task__c o: trigger.new){
Boolean cb = opMap.get(o.Order_Phase__c).Order__Item__r.Order_Project__c;
If(cb == null || cb == false){
o.addError(‘You cannot add an order project category to this item.’);
}
}
Thanks for the help Salaad & Andreas.
I need to make sure that a value is not filled in on the order_task__c also. Would I do something like this in the last part?
for (order_Task__c o: trigger.new){
Order_Phase__c op = opMap.get(o.Order_Phase__c);
if(op != null && op.Order__Item__r.Order_Project__c == FALSE && o.finance_category__c != null) {
o.addError(‘You cannot add an order project category to this item.’);
}
Yes Lisa, you could do that, but as this is a criteria on the trigger record itself it would make more sense
to have this on top of the trigger before you handle the rest of the code.
Because if you are not allowed to add an order to something which has a filled finance_category__c field, than why going further and retrieve the order_phase__c record, if the order_task__c record is already disqualified?
so sticking with your original code it could look like
trigger PreventOrderTaskCreationonOrderItem on Order_Task__c (before insert) {
List orderPhaseIds = new List();
for (order_Task__c o: trigger.new){
if(o.finance_category__c != null){
o.addError(‘You cannot add an order project category to this item.’);
}else{
orderPhaseIds.add(o.Order_Phase__c);
}
}
Map opMap = new Map();
if (orderPhaseIds.size() > 0) {
for (Order_Phase__c op : [select Order__Item__r.Order_Project__c from Order_Phase__c where Id in:orderPhaseIds]) {
opMap.put(op.Id, op);
}
for (order_Task__c o: trigger.new){
Order_Phase__c op = opMap.get(o.Order_Phase__c);
if(op != null && op.Order__Item__r.Order_Project__c == FALSE) {
o.addError(‘You cannot add an order project category to this item.’);
}
}
}
}
I am still not @ 100% happy with your code as you are iterating twice through the trigger may be another map where only
trigger records added which are involved in the order_phase__c gathering, this would also avoid any null values ( OP != null )
But I need to think about it.
I moved one } further down, so the last part of code is only executed if the orderphaseIds list is not empty
hope that helps
Hi David,
I think it would be beneficial if you covered the Map in the ‘final trigger looks’ like section above on a step by step basis like you have covered prior examples. It isn’t making sense for me how the mapping is happening and what can participate in a map. I think you have to specify string, but it looks like you are able to include objects in there, and not sure how you will then go above getting it out in the mapping (for example, if I pulled into custom object for zip code and had a list of zip codes that should be used in the code, how do I go about using the five digit zip code to get who should own the account – a field on my zip master). Sorry a little lost today, maybe going too fast. I have covered in two days what you probably expected to be done in few weeks :)
trigger setOwner2 on Account (before insert,before update) {
//define string and add batch process zip codes to string
Set allzipcodes = new Set();
for (Account zipcode : Trigger.new){
if (zipcode.BillingPostalCode!=null){
allzipcodes.add(zipcode.BillingPostalCode);
}
}
//query Zip Assignment table for batch of zip codes needed
List za=[SELECT id,zip_5__c,account_owner__r.id FROM zip_assignment__c WHERE zip_5__c IN : allzipcodes];
//Define Map to use for search
for (Account targetAcc: Trigger.new){
if (targetAcc.BillingPostalCode!=null){
string errorMessage=’Zip Assignment Master is missing!’;
if(za.size()==0){
targetAcc.addError(errorMessage);
}
else{
targetAcc.OwnerId=za[0].account_owner__r.id;
}
}
}
}
ha ha ha yes take it slow!
This post is probably the most important one in the chapter:
https://www.sfdc99.com/2014/01/12/introduction-maps/
Everything else in your trigger honestly looks strong =)
David
Thanks David, I have been able to finish. Man am I relieved, now I see the power of Map. My next step is to do the reverse. The code is currently looking for zip assignment and assigning owner that way. I will write a trigger that when zip assignment record is update will pull up all accounts and update owner for all related accounts. Thanks man.
trigger setOwner2 on Account (before insert,before update) {
//define string and add batch process zip codes to string
Set allzipcodes = new Set();
for (Account zipcode : Trigger.new){
if (zipcode.BillingPostalCode!=null){
allzipcodes.add(zipcode.BillingPostalCode.substring(0,5));
}
}
//query Zip Assignment table for batch of zip codes needed
List za=[SELECT id,zip_5__c,account_owner__r.id FROM zip_assignment__c WHERE zip_5__c IN : allzipcodes];
//Define Map to use for search result returned
MapmapZip=new Map();
for (Zip_assignment__c ziprecord: za){
mapZip.put(ziprecord.Zip_5__c,ziprecord);
}
//Get the match and let the magic happen
for (Account targetAcc: Trigger.new){
if (targetAcc.BillingPostalCode!=null){
string errorMessage=’Zip Assignment Master is missing!’;
Zip_assignment__c zdigit = mapZip.get(targetAcc.BillingPostalCode.substring(0,5));
if(zdigit==null){
targetAcc.addError(errorMessage);
}
else{
targetAcc.OwnerId=zdigit.Account_Owner__c;
targetAcc.Zip_assignment__c=zdigit.id;
}
}
}
}
Yey, successfully created a trigger to update all accounts when zip code is change. Now I have to figure how to write tests (which I have thus far been ignoring).
trigger MassUpdateAcc on Zip_Assignment__c (after update) {
//Map Zip Assignment Account Owner to be used in next steps for updating accounts
Map zipmapping=new Map();
for (Zip_Assignment__c zipcode: Trigger.new){
zipmapping.put(zipcode.Id,zipcode.account_owner__c);
}
//query accounts to be updated
List targetAccs=[Select ID,Zip_Assignment__c from Account where Zip_Assignment__c IN: zipmapping.keySet()];
//final step, update accounts: yahooooooo
for (Account a: targetAccs){
a.OwnerId=zipmapping.get(a.Zip_Assignment__c);
}
update targetAccs;
}
Tests are my favorite part and they aren’t too hard =) Give it a shot and let me know if you get stuck!
the maps above looking strange
they are
the first 2 maps are
(Id,Contact)
(Id,Lead)
the next 4 maps are
(string,Map(String,Id))
Hi David
I too am having trouble with this:
Error: Compile Error: Field is not writeable: Case.CreatedById at line….
I know you already answered this one, but I don’t understand the answer.
Thanks
Tony
No problem!
So the CreatedById field is only editable if you create a ticket with Salesforce and ask them to grant you that permission!
After they grant it to you, you’ll be able to edit that field only on record creation!
David
Ah! That all makes sense now! Thanks a lot!
Hi David,
thank you so much for your detailed post! You have really helped me a lot.
One thing I am struggling though is the fourth step with multiple “matching fields”.
trigger account_BIU_setFocusMarketLink on Account (before insert, before update) {
// Step 1: Create a set of all values to query
Set accountSector = new Set();
Set accountCountry = new Set();
for(Account a : Trigger.new) {
accountSector.add(a.Sector__c);
accountCountry.add(a.Country__c);
}
// Step 2: Query for all the records in Step 1
List fm = [SELECT Id FROM Focus_Market__c WHERE (Sector__c IN :accountSector AND Country__c in :accountCountry)];
// Create Map that lets one search for focus markets by their country/sector
Map<String, Map> emailToUserMap = new Map<String, Map>();
for (Focus_Market__c fm : potentialFMs) {
countrySectorToFMMap.put(fm.Country__c, fm.Sector__c, fm); // ### this line here
}
What I wanted to do: Select the corresponding focus market record for an account, matching it by the fields Sector and Country. I thought as those are two fields instead of one one as in your example, I would need a map in a map. And this is where things got too tricky for me.
How can I fill this map and how can I assign the query results to the records?
Would be awesome if you could give me a short reply!
Michael
Combine the two fields to make your Map “search term” =) Only one Map needed, popular technique! Also known as a compound key
Hey, thanks for your reply! That’s actually… a very good solution! Easy and flexible.
I am still struggling with the SOQL query though, as SOQL does not seem to suppor statements as [SELECT concat(A__c, ‘#’, B__c) c FROM obj WHERE c IN :list], but I hope I am going to find a solution.
You can combine them after the SOQL query or simple make a formula field that combines them and query that instead =)
use the + sign to concatenate in your PUT statement: countrySectorToFMMap.put(String.valueOf(fm.Country__c) + String.valueOf(fm.Sector__c, fm);
Hey David,
I grabbed your final trigger code to play with and, as it is written, I get this error when trying to save:
“Error: Compile Error: Field is not writeable: Case.CreatedById at line 26 column 9”
Are you able to get this to compile?
Thanks!
~J
@Conga_Jereriah
Love the experimenting!!!
CreatedById is a field that can only be changed if both of these apply:
1. Salesforce has turned on the “Audit System Fields” permission in your org
2. You’re changing that field when a record is being created!
Hope this helps!
David
Hi David,
From my understanding this will only work if the email is unique as maps and sets have to have unique keys. Is there a way to bulkify a trigger if there are different conditions that produce different lists of records?
For example if you have a trigger for an object that has to update different objects, records or simply different field values based on the value in a field on my object
eg: if field = ‘close account’ -> update set of fields/objects for close process
or if field = ‘update account’ -> update a different set of fields and objects etc
if I have 50 records with field = ‘close account’ and 50 records with field = ‘update account’ I can’t put them into a Map as that will leave me with 2 entries each having the last record for a ‘close account’ situation and an ‘update account’ situation.
How do you get around the uniqueness of a map and combine the SOQLs into one big one using ORs in the WHERE clause?
Definitely Laci!
In these cases you have two choices:
1. Use a unique value as your key
2. Use Lists instead
For #1, you almost always want to use some sort of ID (usually the record ID). A common trick if this won’t work in your scenario is to combine multiple fields and use them as a key also. For example you might use firstname + lastname + email as your key!
Lists are a lesser option (but still have its uses) – simply loop through the list instead of using a Map!
Hope this helps Laci!
David
Thanks David,
It hadn’t occurred to me to use a compound key!
Your lesson has however pointed out that I had a number of similar SOQL’s running in the trigger, each one the same except for a slightly different WHERE clause and each one adding their results to a separate list. I’ve now reduced that to a single SOQL and put the different Where clauses in as ORs filters so that the query is only run once.
Then iterate through the results using If…then..else (I don’t know how else to extract the records matching the different where clauses) to assign the records to their appropriate lists for further manipulation.
It has added some more processing lines in relation to the If…then…else, however I’ve removed 5 SOQL queries from the trigger. I figure that 5 ifs are better than 5 SOQLs.
Along these lines, when you have to pull out some records in a trigger that meet a criteria, is it better to iterate through trigger.new and do use an if statement to add a record to a list or to run a SOQL that includes that criteria in the filter?
eg
for(Object__c object : trigger.new)
{
if(object.field == ‘criteria’) listOfObjects.add(object);
}
vs
for(Object__c object : [Select Id, Name, field from Object__c WHERE Id in :trigger.new.keylist() AND field=’criteria’])
{
listOfObjects.add(object);
}
Thanks for the insight and help.
Five IFs are infinitely better than 5 SOQL queries – take that one every time! Excellent call!
You generally want to do the least amount of SOQL queries for the least amount of records total. If you have to choose between another SOQL query OR querying slightly more records (let’s say 30% more), go with the latter.
So the first example you provided is 100x better than the latter – no SOQL is ideal!
Thanks David,
I’ll go back and review my other triggers to see if I can do the same.
Daniel,
Great lesson. One question on the final trigger code…
Is there a specific reason why you first create a set and then query that set? Could you just combine steps 1 and 2 so you would have something like this?
List potentialUsers = [SELECT Id, Email FROM User
WHERE Email != Null];
1 other thing I just thought of…
In the map, you are putting “Email” and IDs but when you retrieve you are getting “SuppliedEmail”.
I thought these would have to be the same fields?
Our map is a string / user map, so we populate our map like this:
emailToUserMap.put(u.Email, u);
Note the u above is a user record
Then we search our map using newCase.SuppliedEmail, which will match an email we put in earlier and give us back a user!
Yes! The difference is in the post we queried the minimum number of records that would be useful during the trigger, while your example queries all possible records.
Here are two good reasons to only query the minimum amount:
1. There is a governor limit that doesn’t let you query more than 50,000 total records
2. Your code will run quicker
David
Hi David ,
Is there a way to surround the 50.000 mark?
I mean in regards of the chapter 4 de-dupe trigger, it can happen that the trigger fields ( user input / upload ) contains
null values those can not be queried, right?
I have written by now a “simple” de-dupe trigger which involve 3 Apex classes to keep it simple and readable
which I would have posted (not sure if including testclass 400 lines of code are possible within one reply) but
in that classes ( bulkified in terms of I tested it with 1,6 million records and canceled the upload after 12 thousand,deciding it works)
as a Sample when it comes to check if a lastname in combination with either a phone number or an email address is existing within contacts or Leads the class including constructor looks like
Public Class dedupeconinf{
Map cons = new Map([SELECT Id,LastName,Email,Phone,MailingStreet,MailingCity,MailingCountry,Account.Name FROM Contact]);
Map lds = new Map([SELECT Id,LastName,Email,Phone,Street,City,Country,Company FROM Lead]);
public boolean issi{get; private set;}
String FRU = URL.getSalesforceBaseUrl().toExternalForm() + ‘/’;
List triggerA;
Map<String,Map> con = new Map<String,Map>();//email
Map<String,Map> lea = new Map<String,Map>();//email
Map<String,Map> con1 = new Map<String,Map>();//phone
Map<String,Map> lea1 = new Map<String,Map>();//phone
public dedupeconinf(List TriggerlA){
triggerA = new List(TriggerlA);
for(Lead x: lds.values())
{
x.Lastname = x.Lastname.toLowerCase();
if(x.Email != null)
{
if(!lea.containsKey(x.LastName)){lea.put(x.LastName,New Map());}
if(!lea.get(x.LastName).containsKey(x.Email)){lea.get(x.LastName).put(x.Email,x.id);}
}
if(x.Phone != null)
{
if(!lea1.containsKey(x.LastName)){lea1.put(x.LastName,New Map());}
if(!lea1.get(x.LastName).containsKey(x.Email)){lea1.get(x.LastName).put(x.Phone,x.id);}
}
}
for(Contact x: cons.values())
{
x.Lastname = x.Lastname.toLowerCase();
if(x.Email != null)
{
if(!con.containsKey(x.LastName)){con.put(x.LastName,New Map());}
if(!con.get(x.LastName).containsKey(x.Email)){con.get(x.LastName).put(x.Email,x.id);}
}
if(x.Phone != null)
{
if(!con1.containsKey(x.LastName)){con1.put(x.LastName,New Map());}
if(!con1.get(x.LastName).containsKey(x.Phone)){con1.get(x.LastName).put(x.Phone,x.id);}
}
}
this.issi=false;
}
the origin trigger only calls 2 methods within this class, and only calls the 2nd method if the first didn’t found anything
(issi is the boolean which is responsible for that
the first method is
public boolean chechcon(){
String LN;
for(target__c t: triggerA)
{
if(t.Company__c == null && t.Lastname__c != null)
{
LN = t.LastName__c.toLowerCase();
if(con.containsKey(LN) && t.Email__c != null)
{
if(con.get(LN).containsKey(t.Email__c))
{
t.addError(‘Found Name ‘ + t.LastName__c + ‘ and Email ‘ + t.Email__c + ‘ in Contacts ‘ + FRU + con.get(LN).get(t.Email__c));
issi=true;
break;
}
}
if(con1.containsKey(LN) && t.Phone__c != null)
{
if(con1.get(LN).containsKey(t.Phone__c))
{
t.addError(‘Found Name ‘ + t.LastName__c + ‘ and Phone ‘ + t.Phone__c + ‘ in Contacts ‘ + FRU + con1.get(LN).get(t.Phone__c));
issi=true;
break;
}
}
}
}
return this.issi;
}
Here the 2nd method going through the leads
public boolean chechlea(){
String LN;
for(target__c t: triggerA)
{
if(t.Company__c == null && t.Lastname__c != null)
{
LN = t.LastName__c.toLowerCase();
if(lea.containsKey(LN) && t.Email__c != null)
{
if(lea.get(LN).containsKey(t.Email__c))
{
t.addError(‘Found Name ‘ + t.LastName__c + ‘ and Email ‘ + t.Email__c + ‘ in Leads ‘ + FRU + lea.get(LN).get(t.Email__c));
issi=true;
break;
}
}
if(lea1.containsKey(LN) && t.Phone__c != null)
{
if(lea1.get(LN).containsKey(t.Phone__c))
{
t.addError(‘Found Name ‘ + t.LastName__c + ‘ and Phone ‘ + t.Phone__c + ‘ in Leads ‘ + FRU + lea1.get(LN).get(t.Phone__c));
issi=true;
break;
}
}
}
}
return this.issi;
}
the trigger itself looks like
trigger dedupe on target__c (before insert, before update) {
if(!triggers__c.getValues(‘dedupe’).off__c){ // Custom setting to enable a switch on/off in production at anytime
boolean issi;
dedupeaccounts effe = new dedupeaccounts(Trigger.new);
issi = effe.chechAcc();
if(issi == false){issi = effe.chechlea();}
effe =null;
if(issi == false)
{
dedupeconacc iknowyou = new dedupeconacc(Trigger.new);
issi = iknowyou.chechcon();
if(issi == false){issi = iknowyou.chechlea();}
iknowyou = null;
}
if(issi == false)
{
dedupeconinf icheckyou = new dedupeconinf(Trigger.new);
issi = icheckyou.chechcon();
if(issi == false){issi = icheckyou.chechlea();}
icheckyou = null;
}
}
}
As you will have observed , I have set the initiated class after usage to null to avoid apex heapsize limit exceptions
Any comments are appreciated
especially about the 50k record limit in regards of the first 2 lines where those maps caching all existing records
the maps above looking strange
they are
the first 2 maps are
(Id,Contact)
(Id,Lead)
the next 4 maps are
(string,Map(String,Id))