Preface: this post is part of the Write Your First Advanced Trigger series.
WARNING: Do not attempt this trigger unless you’re comfortable with Chapter 1 through Chapter 6!
You may not realize it, but you’re about to reach the most important milestone in your coding career!
Frodo Baggins, as he prepares to destroy the Ring and fulfill his life’s destiny!
You’re one step away from knowing how to write an advanced trigger. After destroying this trigger, no other will be impossible for you to slay. All triggers follow the same general pattern!
Here’s this chapter’s trigger. The major differences between this and our pseudo-code is that this code is bulkified and it’s production ready:
// Share an Account with every member of its Territory trigger ShareTerr on Account (after insert, before update) { // Step 1: Create a set of all Zip Codes to evaluateSet<String> zips = new Set<String>();if (Trigger.isInsert) { // For inserted Accounts, all Zip Codes are needed for (Account a : Trigger.new) {zips.add(a.BillingPostalCode);} } else if (Trigger.IsUpdate) { // For updated Accounts, only evaluate changed Zip CodesSet<Id> changedAccs = new Set<Id>();for (Account a : Trigger.new) {String oldZip = Trigger.oldMap.get(a.Id).BillingPostalCode; String newZip = a.BillingPostalCode;if (newZip != oldZip) {zips.add(newZip); changedAccs.add(a.Id);} } // Step 1a: Delete sharing records from old Territories List<AccountShare> shares = [SELECT Id FROM AccountShareWHERE AccountId IN :changedAccs AND RowCause = 'Manual'];delete shares; } // Step 2: Find the matching Territories and Map themMap<String, Territory__c> terrMap = new Map<String, Territory__c>();List<Territory__c> terrs = [SELECT Id, Zip_Code__c,(SELECT Id, User__c FROM Territory_Members__r)FROM Territory__cWHERE Zip_Code__c IN :zips];for (Territory__c terr : terrs) {terrMap.put(terr.Zip_Code__c, terr);} // Step 3: Create AccountShares for all Territory MembersList<AccountShare> shares = new List<AccountShare>();for (Account a : Trigger.new) {Territory__c terr = terrMap.get(a.BillingPostalCode);if (terr != null) {for (Territory_Member__c tm : terr.Territory_Members__r) {// We won't create one if they're already the owner if (tm.User__c != a.OwnerId) { AccountShare aShare = new AccountShare(); aShare.AccountId = a.Id; aShare.UserOrGroupId = tm.User__c; aShare.AccountAccessLevel = 'Edit'; aShare.OpportunityAccessLevel = 'Edit';shares.add(aShare);} } } } insert shares; }
Folks, that’s all there is to it! It’s not a lot of code if you approach it step-by-step. If you’ve been following the Sfdc99 tutorials in order, you’ve already written similar code to each individual piece of this trigger. This is simply the first time it has all been in one place.
You’ll know this trigger works by clicking on the Sharing button on any Account page to see who the record is shared with and why!
Hi David
Looking at your code – it seems that *updates* of records where the account.zip is not edited would cause the creation of duplicate sharing records (as the records are created for all accounts in the trigger). I suggest that the following code would be better:
// Step 3: Create AccountShares for all Territory Members
List accountsToCreateSharesFor = new List;
if Trigger.isUpdate{
accountsToCreateSharesFor = changedAccs;
} else{
accountsToCreateSharesFor = Trigger.new;
}
List shares = new List();
for (Account a : accountsToCreateSharesFor) {
Territory__c terr = terrMap.get(a.BillingPostalCode);
if (terr != null) {
for (Territory_Member__c tm : terr.Territory_Members__r) {
// We won’t create one if they’re already the owner
if (tm.User__c != a.OwnerId) {
AccountShare aShare = new AccountShare();
aShare.AccountId = a.Id;
aShare.UserOrGroupId = tm.User__c;
aShare.AccountAccessLevel = ‘Edit’;
aShare.OpportunityAccessLevel = ‘Edit’;
shares.add(aShare);
}
}
}
}
insert shares;
}
Please let me know if I am missing something!
Hi David,
Could you please upload the test class for this code.
It will be really helpful
Read on and you’ll find it!
Hi David, I am new to Salesforce. Just started learning 5days before. Now after i found this site, i am able to code in APEX..This is a great site. Thanks a lot David. Its time to learn VISUALFORCE.. Are you planning to put integeration in your site? Please respond. I am waiting to here from you.
Thanks Srikanth =) Lots in the works but can’t talk about it yet!
Does this part of the code- Set changedAccs = new Set(); has to be outside the if statement ( for update)? I am just trying to understand if it has anything to do with the singleton pattern…
Hi David,
Please move on, I am not planning to waste your time today. Others, would you please help me with this..
I am asking this because I started writing the trigger, without looking at solutions and I got lost somewhere. ( this trigger is still screaming at me on my 3rd day)
Even when I had asked my room mates ( who are also coders-but not in SFDC ) for an algorithm or pseudo code to resolve the issue, they also didn’t suggest me to group the users first.
So, here goes my questions,
Case 1. How would you write the same trigger if you don’t have a custom object to group users?
Case 2. How would you add users to territory at run time? ie. populate territory records at run time based on the Zip Code of accounts being inserted/updated. [ This is how I was trying to solve the problem… and Yes… I know… it is a stupid idea coz every time when trigger runs, if creates unnecessary SOQL calls affecting performance of org -or are there more disadvantages?- )
Case 3: Don’t we need another trigger to populate records under territory when a new user record is created.
Thanks
Hareesh
Please dont follow the trigger code writter above, it is not at all bulkified.
The trigger that you have written above territories and account share one, it is not at all bulkified one. It would be better to use map instead of for inside for loop.
So many people have been following your not so bulkified code till now. Damn!
Hi David,
for this program am getting the following error in first Line;1 ” Entity is not org-accessible ”
I have verified the code i have given the extensions like __C correctly for every custom object, Can you please help me out
Hey David,
This trigger wouldn’t correctly work if we do a bulk upsert using the Data Loader since it only handles either Insert or Update in a single transaction. Am I understanding it right?
(You can remove replace my previous question with this one).
Hey David,
This trigger wouldn’t correctly work if we do a bulk upsert using the Data Loader since it only handles either Insert or Update in a single transaction. Am I understanding it right? So let’s say if we have an upsert file that has 150 insert type records and 100 update type records, will your code be able to handle it?
Great question!
I believe two separate batches are created, one for update and one for insert. Don’t know this for certain but either way one of the paths will work for sure!
(Ie there is no Trigger.isUpsert)
https://www.salesforce.com/us/developer/docs/apexcode/Content/apex_triggers_context_variables.htm
Do you think that simply putting the ‘if’ and ‘if else’ statements inside the for loop will take care of an upsert operation?(I don’t like posting code for you to review but this has been pestering me for 2 days now and I want to get it off my head haha. Sorry David!)
My code(trying to handle upsert):
for (Account a : Trigger.new) {
if (Trigger.isInsert) {
zips.add(a.BillingPostalCode);
}
else if (Trigger.IsUpdate) {
String oldZip = Trigger.oldMap.get(a.Id).BillingPostalCode;
String newZip = a.BillingPostalCode;
if (newZip != oldZip) {
zips.add(newZip);
changedAccs.add(a.Id);
}
}
}
List shares = [SELECT Id FROM AccountShare
WHERE AccountId IN :changedAccs
AND RowCause = ‘Manual’];
delete shares;
//Rest of the code follows
You bring up a very good and interesting question that I can’t answer with 100% certainty!
I think you should attempt solving this one the scientific way… live checking!
Let us know how it goes Mayank!
David
It worked!! I did an upsert with 300 records and all were appropriately updated =) Thank you for making me do this David. I sure learnt a lot of things while customizing the code in accordance with the Enterprise Territory Management.
For those who are using the new Territory Management, I had to make some changes to David’s code to make it compatible with the Enterprise Territory Management on my sandbox org. (For Eg: Territory__c has to replaced by Territory2, Territory_Members__r and Territory_Members__c has to be replaced by ‘UserTerritory2Associations’ and User_c had to be replaced by UserId.
Go go Mayank!!! ha ha ha
Hi David,
Could you pls explain the “Step 1a: Delete sharing records from old Territories “. The reason for deleting the Account share record. Thank you much for your time.
If an account changes territories 50 times, this makes sure that the 49 old territories aren’t associated with it!
Yep.:) Thanks David
Hi David
I have a new requirement to work on. If I go on a vacation I am able to setup the delegated user and he should be able to edit the quotes that I own. How can I achieve this functionality? Please let me know.
Thanks,
Sweety
Try a criteria based sharing rule =) There might be a better way but that’ll work!
Good luck Sweety!
David,
Here is the clear explanation.
If A and B are 2 users and User A goes on vacation and sets a User B as a delegate to update the quotes in his absence. There is a field on the User Object to set this delegate for Quotes. ‘A’ sets a delegate user ‘B’ using that field and after coming back he should take him out from his User Profile so his access to update the Quotes will be removed. Can this happen with Criteria based sharing rules? Please help!
Thanks!!
Oh are you trying to automate this? In that case, definitely check out the OpportunityShare object!
Is there any other way to do this ? I mean by using the sharing rules?
Try playing around with this!
https://help.salesforce.com/HTViewHelpDoc?id=security_sharing_cbs_about.htm&language=en_US
David,
Using Criteria based rules, Can we share the record access to a single user? Please suggest.
Hi,
I need your help mate….
I want to populate a field on OPP object based on USER. I tried using USERINFO.getUserID(), But my requirement is to get the User information from the USER OBJECT. and have to find a manager who is in my custom setting.
I am able to code the UPDATE but i also need this trigger for INSERT as well.
Plz help me…..
Try the forums!
Also, two other things:
1. Try posting the code you have so far
2. Provide more info on exactly what you need!
https://www.sfdc99.com/forums/forum/beginning-apex/
My requirement is I am introducing two new fields which ihave to populate the values into these based on the owner of the Opportunity. L1 and L2
now if Opp owner belogns to salesteamTeam called 1 then L1 shud populate and if its 2 then L2
here i have to check the user hierarchy and populate the values to field by checking whether they contain anywhere in the LIST L2heads below.
And sometimes I have parent and child Opps in this case i have two owners at this now here i will have L1 and L2 now they both shud get populated
I have written the below logic
Trigger mytriiger on Opportunity(before insert, before update)
{
if (isinsert){
classname.methodname(Trigger.new, trigger.oldmap, true);
}
if(isupdate){
classname.methodname(Trigger.new, trigger.oldmap, true);
}
}
class classname{
public void methodname(list, Map(id,Opportunity> oldmap, boolean isinsert)
{
Set L1heads = new Set{‘A’, ‘B Singh’, ‘S’, ‘Nikon’, ‘C’, ‘Satya’};
List L2heads= new List([select id, Name, Sales_Team_C__c, Profile.Name From User where RM1__c IN:L1heads]);
Map L2_Profile= new Map();
for(User u:L2heads){
system.debug(u);
if(!L2_Profile.containsKey(u.Name))
{
L2_Profile.put(u.Name, u.Sales_Team_C__c);
}
// L2_Profile.get(u.Name).add(u);
}
Set setownerIds = new Set();
if(isinsert){
for(Opportunity o:op){
setownerIds.add(o.OwnerId);
}
else{ if(mapoldop!=null && op!=null){
for(Opportunity o:op){
if(mapoldop.containsKey(o.Id) && o.ownerId != mapoldop.get(o.Id).OwnerId){
setownerIds.add(o.OwnerId);
}
}
}}
Map u= new Map([SELECT Id, Email,User_Hierarchy__c FROM User WHERE Id IN (select ownerid from Opportunity where Id=:setownerIds)]);
for(Opportunity oOpportunity:op){
User owner=u.get(oOpportunity.OwnerId); ERROR—- BEFORE UPDATE ATTEMPT to reference NULL object.
//string oOwner=
string U_Hierarchy=owner.User_Hierarchy__c;
Set userhierarchy= new Set();
// here i am adding all my logic to get the values populated to respective fields………. which is working fine. because i tried writing individual triggers on same object. But i want to reduce the redundant code. so started creating a method and depending on the action now i wnat to call this method before insert or before update.but it is giving an error while executing.
}
Pls help me….. And i also want how acheive same using parent child as well…..
Too much for me to debug but I noticed you don’t use System.debug at all!
Needs a lot more!!
https://www.sfdc99.com/2014/02/22/debug-your-code-with-system-debug/
Actually it looks like you have plenty of System.debug! Sorry =P
Hi David,
I created the custom objects of Territory and Territory Member and my trigger got created and saved successfully. When I tired to update the Account getting the below error.
ShareTerr: execution of BeforeUpdate caused by: System.DmlException: Insert failed. First exception on row 0; first error: INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY, insufficient access rights on cross-reference id: []: Trigger.ShareTerr: line 61, column 1
Can you please help me. I tried to get the resolution but not succeeded
Double check that those objects are set to private in your sharing settings!
Thanks David,
I set the sharing setting as Public Read Only earlier. It was worked. Basically, I had to tweak the trigger as it was not completely in line with yours.
Appreciate your time for replying me
No problem! Can you share the fixes in case others are experiencing the same issue?
Hi David,
First of all, I had to make the Account object as Public Read Only. Secondly, in my code, I was initially comparing Account Id with Territory Member User Id, which was typo/mistake instead of Account owner id.
Excellent and Marvelous. I can say with your chapters. I read the apex workbooks and practiced them, but was going mechanically, until I hit your website. Your site gives the clarity and clear picture of Apex development process. Glad I found you.
Hi David
I was experimenting on your code and m stuck
I am receiving an error while saving it says “error: Compile Error: Field is not writeable: AccountShare.AccountId ”
Is there something to be done in Sharing setting on Account object.
Please help!
Looks like you’re missing permissions! Try manually sharing the same account using point and click to see if that’s the case!
If not, post your code!
David
Hi David,
Though I made all the custom object and its relationship as you explained. But still I am getting below mentioned error while saving the trigger. whats wrong in this SOQL ? I can provide more info if required.
List terr = [SELECT Id, Zip_Code__c, (SELECT Id, User__c FROM Territory_Member__r)
FROM Territory__c
WHERE Zip_Code__c IN :zipCode];
ERROR : “Didn’t understand relationship ‘Territory_Member__r’ in FROM part of query call. If you are attempting to use a custom relationship, be sure to append the ‘__r’ after the custom relationship name. Please reference your WSDL or the describe call for the appropriate names.”
I’ll take a stab at this since I’ve always found that I seem to learn better if I can help someone else! I believe you’re missing the bit between List and terr where you state which object you are doing a list against.
List terr…
So try:
List terr
I think of this as assigning the List Object to the sObject of terr which this has the results of the query against that object.
Hey Virginia… Appreciate your help. But I did typecasted the List to the sObject of Territory__c which is erased while pasting due to some limitation of this page.
Virginia you’re probably right but my site reads comments as HTML unfortunately so your comment was cut off! Looking into a fix!
If you navigate to the Territory Member object and go to the Territory field, you’ll see the exact Child Relationship Name!
In my org it’s actually Territory_Members__r (don’t forget the ‘s’)!
Thank you very much David I now understand where I was wrong ‘s’ was the issue…… You are awesome Man!!!!! :-)
try changing ur child relationship name” Territory_Members__r” it should work
Hello David,
Can you please clarify the following for me? I was wondering if snippet B would still work without the downwards traversal in snippet A
//Snippet A from step 2
List terrs = [SELECT Id, Zip_Code__c, (SELECT Id, User__c FROM Territory_Members__r)
FROM Territory__c WHERE Zip_Code__c IN :zips];
//Snippet B from step 3
for (Territory_Member__c tm : terr.Territory_Members__r)
My question is – would [terr.Territory_Members__r] in step 3, not be a valid dot notation reference if we did not include [(SELECT Id, User__c FROM Territory_Members__r)] in the SOQL query in step 2?
And if it will still be valid, then is there any other reason for having the downward traversal in the SOQL query?
Thanks.
PS: I hope my question is not too dumb… :)
Snippet B will work! Without querying for it, the value of terr.Territory_Members__r will likely be null for all members. So in short, you must query for it, but everything will still save fine if you don’t (but it’ll break when you run)!
Ok, thank you.
Hi David, as always, wonderful material!
I’m new to apex and have been trying to design a trigger that would populate the Account Number field with a value starting at 10000, when the Opportunity Probability moves to 85%. Before I added my ‘if’ statements, it worked fine, but after that nothing. I even tried writing the trigger on Opportunity and Account objects.
I created a custom object accNumber__c and custom field Ones__c, where the generated number would be stored.
Here is what I have so far:
trigger AccountNumberUpdate on Opportunity(before insert, before update) {
accNumber__c value = new accNumber__c(Name=’1′,Ones__c=10000);
for(accNumber__c record:[SELECT Id,Name,Ones__c FROM accNumber__c WHERE Name=’1′ FOR UPDATE]) {
value = record;
}
for(Opportunity opps:Trigger.new) {
if((opps.Probability>=85)&&(opps.Account.Region__c==’Americas’)&&(opps.AccountNumber==null)) {
opps.Account.AccountNumber =’0′.repeat(math.max(0,0-String.valueOf(value.Ones__c).length()))+String.valueOf(value.Ones__c);
value.Ones__c+=1;
}
}
update value;
}
The Account object is a lookup on the Opportunity object. Is there another way I can approach this problem? Any help is appreciated.
hahahaha since the Probability is a percentage field it’s probably 0.85 instead of 85!
Also, don’t forget to query for opps.Account.Region__c since it’s on a different record!
Hi David,
I’m getting an interesting Problem message when saving this trigger. The developer console returns: “DML operation DELETE not allowed on AccountShare”. I’ve checked the supported calls for the AccountShare sObject, and delete is indeed listed (also, I’m pretty sure you know what you’re doing). Is there a setting or something that I’m missing?
I did see this error message once before when running through your examples, but I can’t recall which sObject it applied to…
Thanks David!
Dave – make sure you’re a system admin in your org!
You can login to my org to see all the code live – so it definitely works!
https://www.sfdc99.com/login-to-sfdc99/
Dave:
I think you have to go to Setup | Security Controls | Sharing Settings. Change the Default Internal Access for Account from “Public Read/Write” to “Public Read Only” and then save. I ran into the same problem as you, and figured out that since the default for Account is “Public Read/Write” sharing of a record wasn’t possible when I was looking at an Account. Changing it to Public Read Only makes the object eligible for Sharing. I then was able to save my trigger!
Virginia
Thank you Virginia!!! =)
For this to work, the Account object should be Private in OWD. Go to Setup > Sharing settings > Edit > Make account as Private.
Also, after editing the Sharing settings, check if the “Manual User Record Sharing” is set to active.
Hi David,
Thank you so much for your great posts on here.
My client wants to track the size of each account based on the number of contacts on the account. I’ve created a Contact Size picklist on the Account object with the values ‘Cold’,’Warm’,’Hot’. I’m trying to create a trigger that runs before insert,update as follows:
1) 2 or fewer contacts update to ‘Cold’
2) 3-7 contacts update to ‘Warm’
1) 8 or more contacts update to ‘Hot’
What is confusing is the Account object is a lookup on Contacts. How would I start this off?
This is a pretty common ask!
You have two options:
1. Download a free app that does this:
https://appexchange.salesforce.com/listingDetail?listingId=a0N30000009i3UpEAI
2. Read up to Chapter 5, then write a trigger that uses a downward SOQL query!
https://www.sfdc99.com/2013/06/24/example-how-to-write-a-cross-object-soql-query-part-2/
Definitely will be able to do #2 based on the info on this site, give it your best shot!
David
Thanks David!
Reading through those 5 chapters really helped. I was able to write the trigger with a downward SOQL query!
Rock On!
Holy cow you learn quickly hahahaha. Wow still impressed!!
David, Its always a best practice to take count of size before any a soql is triggered. Section 1.a onwards should be within put in a condition if(changedAccs.size()>0). It will not allow to break the trigger.
he he he actually I thought so too until relatively recently – Apex was changed so that DML on empty lists no longer throws an error!
So now we can save a few lines of code =)
(I just tested this on a brand new custom object and can confirm it’s not necessary!)
Either way I appreciate you keeping an eye out for me Sunil – don’t hesitate if you find anything else that needs to be changed!
David
David,
I am good with insert trigger. The issue is for update trigger only. I guess in case of update trigger, we should run the loop on changedAccs list while creating new AccountShare records.
Please make sure if I am getting it wrong.
For update triggers we’ll clear all AccountShares for changed Accounts and start from scratch – only adding in AccountShares from the new Territory!
For insert triggers, this step isn’t necessary since new Account don’t have any existing AccountShares!
Hopefully this answers your questions, if not I am sorry for misunderstanding!! Let me know!
David
Hi David,
I have a query regarding this trigger. In case of update you are deleting all the Account Share records if the zip code of the corresponding Account has been changed.
But while creating new Account Share records, you are not checking if the zip code has been changed or not. The for loop is on Trigger.new, that means it will create Account Share records for all the Accounts updated regardless the zip code is changed or not.
I guess it will try to create duplicate Account Share records for the Account which are updated by some other field (say BillingCity) and not zip code. (In case it has a duplicate zip code)
Please clear my doubt.
Thanks
Raghvendra
Hey Raghvendra,
The reason we don’t check for Zip Code changes on Insert triggers is because there is no “old” value on Insert – every value is always new!
https://www.salesforce.com/us/developer/docs/apexcode/Content/apex_triggers_context_variables.htm
IE the “old” value is always null. There is no Trigger.oldMap on insert!
Hope this explains it!
David
Hi David
Thanks and appreciate your efforts for these Great Tutorials.
One request is that to add few more exercises at the end of each chapter if possible for better practicing.