I'm stuck on the syntax (and maybe the logic) of writing this Salesforce.com Trigger. I want the trigger to check to see if a primary contact is listed in the ContactRoles on the Opportunity. If there's a primary listed, I need to lookup the LeadSource from the corresponding contact and insert that value in the the Lead Source of the Opportunity.
Any hints or tips are greatly appreciated!
trigger UpdateContactLeadSource on Opportunity (after insert, after update) {
//Declare the Lead Source Variable which will hold the contact's lead source
string leadsource;
// See if there is a primary contact listed on the Opportunity
for (Opportunity o : Trigger.new) {
OpportunityContactRole[] contactRoleArray =
[select ContactID, isPrimary from OpportunityContactRole where OpportunityId = :o.id ORDER BY isPrimary DESC, createdDate];
// If the there is a primary contact, then...
if (contactRoleArray.size() > 0) {
// Lookup ContactID on the Contacts table to find the lead source
for (Contact contact : [SELECT LeadSource FROM Contact WHERE Contact.Id = :OpportunityContactRole.ContactId LIMIT 1])
// Store the actual lead source in the leadsource variable
{ Contact.LeadSource = leadsource;}
update Opportunity.LeadSource = leadsource; }
}
}
There are couple of seriously bad things in your code. Hope you won't feel offended, it's a nice requirement and a learning opportunity too...
after insert doesn't make any sense here. By very definition if you've just finished inserting this Opportunity it won't have any contact roles in it yet.*
after update is OK-ish. before update would be nicer because you'd just fill in the value and you'll get the save to database for free.
You might have to replicate this logic to similar trigger on OpportunityContactRole I think? Instead of duplicating code - use some class that could be called from both places?
I'm assuming you'd want to fill it in only if LeadSource on Opportunity would be null?
You have selects in a loop, that's a very bad practice ;) Your trigger isn't "bulk", it might fail when somebody updates 10 opportunities in one transaction (for example by using Data Loader) because we have a limit of max 20 queries in triggers.
You don't need ORDER BY, LIMIT 1 etc - Salesforce will protect you and allow only 1 contact to be primary. Even if you'd want to load them with such mistake with Data Loader.
It seems like you'd benefit from reading about how relationships work in Salesforce. The "dot notation" and subqueries look a bit weird compared to normal SQL languages but you can use them to greatly simplify your code. Will help if you did some object oriented programming in the past. Here's some guide and here are official docs
TL;DR
trigger fillLeadSource on Opportunity (before update) {
/* I'm assuming you want to fill LeadSource in only if it was blank.
If that's not the case - just delete this first part of code and in the query instead of ":oppsToFill" bind to ":trigger.new" or
":trigger.newMap.keyset()".
(I know they look weird but you can do it, bind objects/collections in queries that expect Ids / collections of Ids)
*/
Set<Id> oppsToFill = new Set<Id>();
for(Opportunity o : trigger.new){
if(o.LeadSource == null) {
oppsToFill.add(o.Id);
}
}
// Now we'll select all possible contacts wasting only 1 query.
if(!oppsToFill.isEmpty()){
List<OpportunityContactRole> roles = [SELECT OpportunityId, Contact.Name, Contact.LeadSource
FROM OpportunityContactRole
WHERE isPrimary = true AND Contact.LeadSource != null AND OpportunityId IN :oppsToFill];
if(!roles.isEmpty()){
for(OpportunityContactRole ocr : roles){
Opportunity oppToBeFilled = trigger.newMap.get(ocr.OpportunityId);
System.debug('Changing lead source on ' + oppToBeFilled.Name + ' from ' + oppToBeFilled.LeadSource + ' to ' + ocr.Contact.LeadSource + ' (thx to ' + ocr.Contact.Name + ' being the Primary Contact).');
oppToBeFilled.LeadSource = ocr.Contact.LeadSource;
}
}
}
// That's it. If there was a primary contact with Lead source, data will be copied over.
// If there was no primary contact or he didn't have the source filled in - tough luck, we did our best.
// Since it's before update, you get save to database for free.
}
Fine print to #1, for advanced scenarios ;) You could have Contact Roles right after saving if you have another after insert trigger that'd be adding them somewhere... But that'd be very messy (depending on which trigger fired first I'd say you'd get a really interesting random behaviour, good luck debugging). If you do have more than one trigger on given object with same firing keyword in it it's advised to have only 1 trigger and be in full control of the order of actions. See http://www.embracingthecloud.com/2010/07/08/ASimpleTriggerTemplateForSalesforce.aspx for more.
EDIT to answer question from comment re #3
You'd need similar but not identical code in a new trigger (in this case it doesn't matter much whether it's before or after - we need to explicitly update Opportunities). It's a bit worse here also because the fields you want to look at aren't directly available - you have access to OpportunityId, ContactId but not Contact.LeadSource. Something like this should do the trick:
trigger ContactRoleRollup on OpportunityContactRole(after insert, after update){
Map<Id,Id> oppToContactMap = new Map<Id, Id>();
for(OpportunityContactRole ocr : trigger.new){
if(ocr.isPrimary){
oppToContactMap.put(ocr.OpportunityId, ocr.ContactId);
}
}
if(!oppToContactMap.isEmpty()){
List<Opportunity> oppsToUpdate = [SELECT Id FROM Opportunity WHERE LeadSource = null AND Id IN :oppToContactMap.keyset()];
Map<Id, Contact> contacts = [SELECT Id, LeadSource FROM Contact WHERE LeadSource != null AND Id IN :oppToContactMap.values()];
for(Opportunity o : oppsToUpdate){
Id contactId = oppToContactMap.get(o.Id);
Contact c = contacts.get(contactId);
if(c != null){
o.LeadSource = c.LeadSource;
}
}
update oppsToUpdate;
}
}
It gets interesting here because this update will fire our old trigger on opportunities. It should be fine if you have left my "skip if leadSource is filled in" but still you might want to explore 2 things:
Use some helper static flag in a class, call it "skipTriggerOnOpps" you'd be setting this flag in trigger on OpportunityContactRoles and wrapping up whole code of Opportunity trigger in it so it won't execute if we already took care of lead source sync.
Theoretically you could just "touch" the Opportunities without changing anything (treat the old trigger as a benefit, not unwanted side effect in this case). For me it'd look a bit too magical but if it's well commented what's going on here it might lead to less code, less logic duplication, less unit tests... It will work as long as it's after trigger so the query for contact roles we've just modified will see new values. It'd have to look like that
trigger ContactRoleRollup on OpportunityContactRole(after insert, after update){
Set<Id> oppIds = new Set<Id>();
for(OpportunityContactRole ocr : trigger.new){
if(ocr.isPrimary){
oppIds.add(ocr.OpportunityId);
}
}
if(!oppIds.isEmpty()){
update [SELECT Id FROM Opportunity WHERE LeadSource = null AND Id IN :oppIds];
// That's it. Call update directly on returned list without changing anything, let the other trigger worry about the logic
}
}
eyescream's opportunity trigger is most helpful and is solving a problem for me. It's worth pointing out, though, that the OpportunityContactRole triggers he hypothesises are unfortunately not yet supported by SFDC. As this idea (https://success.salesforce.com/ideaview?id=08730000000BrdvAAC) notes, triggers on OCR are not possible at this time.
Related
I was trying out a simple trigger on Case Object.There is a field Hiring_Manager__c (looks up to User) in Case .On update or insert of a case, this field has to be populated with the Case Owner's Manager.I created the trigger as follows.It is not bulkified as I was just trying out for a single record.
I could see the value getting populated correctly in debug statements.But it is not updated on the record.
trigger HiringManagerupdate_case on Case (before insert,before update) {
Case updatedCaseRec= [select id,ownerId,Hiring_Manager__c,CaseNumber from case where id in :Trigger.new];
system.debug('Case Number'+updatedCaseRec.CaseNumber);
system.debug('Manager before updation'+updatedCaseRec.Hiring_Manager__c);
try{
User caseOwner = [Select Id,Name, ManagerId, Manager.Email From User Where Id = :updatedCaseRec.ownerId];
updatedCaseRec.Hiring_Manager__c=caseOwner.ManagerId;
}
Catch(DMLException de){
system.debug('Could not find Manager');
}
system.debug('Manager'+updatedCaseRec.Hiring_Manager__c);
}
I got a reply for this in developer's forum with a code that works fine.
trigger HiringManagerupdate_case on Case (before insert,before update) {
for(Case cas :Trigger.new){
try{
User caseOwner = [Select Id,Name, ManagerId, Manager.Email From User Where Id = :cas.ownerId];
cas.Hiring_Manager__c=caseOwner.ManagerId;
}
Catch(DMLException de){
system.debug('Could not find Manager');
}
system.debug('Manager'+cas.Hiring_Manager__c);
}
I would like to know
1)Difference in logic in both the approaches if I am trying to update only a single record?Mine is populating the value in debug statement but not on screen.
2)Will the select statement inside the loop (of second approach)affect governor limit?
3)Do we need to explicitly call update for before triggers? My understanding is the save and commit happens after the field update for before triggers and hence the changes will be saved automatically without calling update statement.?
Thank in advance.
Nisha
1) If you are trying to access the record(s) for the object which fired a before trigger, you must iterate over Trigger.new. You cannot query for it because the record(s) may not be in the database yet.
2) Yes it could. The query executes for every Case record being inserted or updated. If you have 1,000 such Cases, the query executes a 1,000 times.
3) No, we do not need to call update for before triggers. Your understanding is correct.
A user wants to invite a friend but I want to do a check first. For example:
SELECT friends_email from invites where friends_email = $1 limit 1;
If that finds one then I want to return a message such as "This friend already invited."
If that does not find one then I want to do an insert
INSERT INTO invites etc...
but then I need to return the primary user's region_id
SELECT region_id from users where user_id = $2
What's the best way to do this?
Thanks.
EDIT --------------------------------------------------------------
After many hours below is what I ended up with in 'plpgsql'.
IF EXISTS (SELECT * FROM invitations WHERE email = friends_email) THEN
return 'Already Invited';
END IF;
INSERT INTO invitations (email) VALUES (friends_email);
return 'Invited';
I undestand that there are probably dozens of better ways but this worked for me.
Without writing the exact code snippet for you...
Consider solving this problem by shaping your data to conform to your business rules. If you can only invite someone once, then you should have an "invites" table that reflects this by a UNIQUE rule across whatever columns define a unique invite. If it is just an email address, then declare the "invites.email" as a unique column.
Then do an INSERT. Write the insert so that it takes advantage of Postgres' RETURNING clause to give an answer on success. If the INSERT fails (because you already have that email address -- which was the point of the check you wanted to do), then catch the failure in your application code, and return the appropriate response.
Psuedocode:
Application:
try
invite.insert(NewGuy)
catch error.UniqueFail
return "He's already been invited"
# ...do other stuff
Postgres:
INSERT INTO invites
(data fields + SELECT region thingy)
VALUES
(some arrangement of data that includes "region_id")
RETURNING region_id
If that's hard to make work the first time you try it, phrasing the insert target as a CTE may be helpful. If all else fails, write it procedurally in plpgsql for the time being, making sure the external interface accepts a normal INSERT (so you don't have to change application code later) and sort it out once you know whether or not performance is an issue.
The basic idea here is to let the relational shape of your data obviate the need for any procedural checking wherever you can. That's at the heart of relational data modeling ...somewhat of a lost art these days.
You can create SQL stored procedure for implement functionality like described above.
But it is wrong form architecture point of view. See: Direct database manipulation an anti-pattern?
DB have scope of responsibility: store data.
You have to put business logic into your business layer.
I'm receiving an exception when I try to use data loader for this apex trigger. It says there's limit of 100 records to be updated at a time. Here is the code that explains a trigger on Account Object. All the comments are much appreciated
trigger MaintainPrimaryOverriding on Account (before insert, before update) {
if (TriggerUpdateController.getPrimaryBranchOverriding()){
TriggerAffiliationControl.setLock();
for(Account s : Trigger.new)
{
if (Trigger.isUpdate){
Id ownerId = Trigger.oldMap.get(s.Id).OwnerId;
if (s.OwnerId != ownerId){
//Use Branch Associated with owner ID
TriggerUpdateController.UpdatePrimaryBranchOfficeForAccountOwnerChange(s);
TriggerUpdateController.UpdateAffiliation(s);
}
}
else if(Trigger.isInsert){
TriggerUpdateController.UpdatePrimaryBranchOfficeForAccountOwnerChange(s);
}
}
TriggerAffiliationControl.setUnLock();
}
}
Thanks!
This error Too many SOQL queries is due to the fact, you are hitting on governor limit. Look at Best Practice: Avoid SOQL Queries Inside FOR Loops
Looks like you are calling below methods in your code for each update/insert record
"TriggerUpdateController.UpdatePrimaryBranchOfficeForAccountOwnerChange(s)"
"TriggerUpdateController.UpdateAffiliation(s)"
Looks like you are issuing SOQL statement in those above statements, which causing the governing limit error.
two modifications you have to do here.
1. Capture the list of updated or inserted accounts in a list and pass them to above two methods.
2. make above two methods to accept the list of accounts created in step1 and do the processing.
3. If you want fetch details from some other object, get those details in a SOQL.
If you could post the code for "TriggerUpdateController", i would be in a better position to suggest you correct code.
Regards,
Pundareekam Kudikala
I am a beginner on this apex endeavours. I can interpret code but I struggle on creating it.
So I just need to understand how to create the subject explained trigger.
NOTE: there is a condition that states for this to happen if the field is different from 0, but it is not necessary.. as long as it updates it with the value on the fieldb on object b is enough.
If you can provide comments on all the steps for me to understand the why, that would be great
I came up with this, but it is obvious that it is not working:
trigger PopulateOrderValue on ObjectA__c (after insert) {
FieldA__c A = Trigger.new
ObjectB__c.FieldB__c != 0
{
A = ObjectB__c.FieldB__c
update A;
}
}
Firstly, there has to be some sort of relationship between the objects.
Then, you can capture that value and use it to form a mapping to update the fields on the other object.
Hello fellow code lovers, I am attempting to learn apex code and stuck on a problem. Since most of you guys here are avid code lovers and excited about problem solving, I figured I could ran by problem by.
I am attempting to Create a Trigger on an object called Book that does the following:
On Delete, all associated Chapters are also deleted
There is also an object named Chapter that is has a lookup to book.
Here is my attempt. This is my first ever attempt at apex so please be patient. Is anyone willing to dabble in this piece of code?
trigger DeleteChaptersTrigger on Book__c (before delete) {
List<Book__c> book = Trigger.old;
List<Chapter__c> chapters = new List<Chapter__c>();
Set set = new Set();
for (Book__c b :books){
if ()
}
}
You need to write all trigger with consideration that the trigger might be processing many records at any one time so you need to bulkify your trigger code.
Here are the variables that available on the trigger object.
You want to get all the record ids that will be deleted. Use the keyset method on the oldmap to get this info without looping and creating your own collection. Then you can just delete the records returned from the query.
trigger DeleteChaptersTrigger on Book__c (before delete) {
Set<string> bookids = Trigger.oldMap.keyset();
delete [SELECT Id FROM Chapter__c WHERE Book__c IN :bookids];
}
Apex code is unlike other languages where it gets confused with reuse of used words, like set as a variable name. Apex is also case insensitive.
Since you have full control, I recommend changing the way your custom objects are related to each other.
A chapter has no meaning/value without a book so we want to change the relationship between the two.
Remove the lookup on the Chapter object and replace it with a master-detail. When a master record gets deleted, Salesforce automatically deletes the detail related records. This is what you want, and without coding.