Apex開發注意事項

Apex開發注意事項

*https://developer.salesforce.com/page/Apex_Code_Best_Practices

Best Practice #1: 在Trigger裏成塊兒的處理數據集

Bad Sample

trigger accountTestTrggr on Account (before insert, before update) {
   Account acct = Trigger.new[0];
   List<Contact> contacts = [select id, salutation, firstname, lastname, email 
              from Contact where accountId = :acct.Id];
   
}

Good Sample

trigger accountTestTrggr on Account (before insert, before update) {
   List<String> accountNames = new List<String>{};
   for(Account a: Trigger.new){
      a.Description = a.Name + ':' + a.BillingState
   }
}

知識點:Trigger的使用場景有兩種
一種是畫面單條記錄更新,新建的時候會觸發相應動作的Trigger,這個時候上邊兒兩個例子處理是一樣的都沒有問題。
另外一種情況是使用Dataloader活着其他的工作批量的導入數據時候也會觸發相應的Tirgger,這時候Bad Sample的處理只能處理第一條(Trigger.new[0]),其他的數據就會丟失(準確的說是每200條,或者一個批處理的第一條),所以正確的做法是用處理數組的方法處理Trigger的邏輯。

Best Practice #2: 不要把SOQL語句寫到循環裏

Bad Sample

trigger accountTestTrggr on Account (before insert, before update) {
   for(Account a: Trigger.new) {
      List<Contact> contacts = [select id, salutation, firstname, lastname, email  from Contact where accountId = :a.Id];
 		  
      for(Contact c: contacts) {
         System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '], LastName[' + c.lastname +']');
         c.Description=c.salutation + ' ' + c.firstName + ' ' + c.lastname;  
         update c;
      }    	  
   }
}

Good Sample

trigger accountTestTrggr on Account (before insert, before update) {
  List<Account> accountsWithContacts = [select id, name, (select id, salutation, description, firstname, lastname, email from Contacts) 
 from Account where Id IN :Trigger.newMap.keySet()];
  
  List<Contact> contactsToUpdate = new List<Contact>{};
  for(Account a: accountsWithContacts){
     for(Contact c: a.Contacts){
   	  System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '], LastName[' + c.lastname +']');
   	  c.Description=c.salutation + ' ' + c.firstName + ' ' + c.lastname; 
   	  contactsToUpdate.add(c);
     }    	  
   }
   update contactsToUpdate;
}

知識點:Salesforce的每個事物的執行都是有限制的尤其是SOQL操作有着明確的規定,所以爲了避免發生系統異常所以要把SOQL語句拿到循環外邊兒,靈活使用List,Map,Set等數組對象,確保在select ,update,Insert等數據庫操作語句裏只執行一次。
Execution Governors and Limits
Limits Class

Best Practice #3:批量處理數據

確保HelperClass裏的執行也是“批量處理數據”的原則來進行書寫。
原因是Salesforce裏的Limit是按照事物計算的不是按照文件來算的,所以無論是Apex還是Trigger,都應該儘量少的寫操作SOQL語句。

Best Practice #4: 儘量給For循環操作提供集合

Bad Sample

trigger accountTrigger on Account (before delete, before insert, before update) {
    List<Opportunity> opptysClosedLost = [select id, name, closedate, stagename
            from Opportunity where  
            accountId IN :Trigger.newMap.keySet() and StageName='Closed - Lost'];
    
    List<Opportunity> opptysClosedWon = [select id, name, closedate, stagename
            from Opportunity where  
            accountId IN :Trigger.newMap.keySet() and StageName='Closed - Won'];
    
    for(Account a : Trigger.new){
    	
        for(Opportunity o: opptysClosedLost){	
    		if(o.accountid == a.id)
    		   System.debug('Do more logic here...');
        }
        
        //Redundantly processes the List of Opportunity Won
        for(Opportunity o: opptysClosedWon){	
    		if(o.accountid == a.id)
    		   System.debug('Do more logic here...');
        }
    }
}
}

Good Sample

trigger accountTrigger on Account (before delete, before insert, before update) {
 
    List<Account> accountWithOpptys = [select id, name, (select id, name, closedate, 
         stagename  from Opportunities  where accountId IN :Trigger.newMap.keySet() 
         and  (StageName='Closed - Lost' or StageName = 'Closed - Won')) 
         from Account where Id IN :Trigger.newMap.keySet()];
    
    //Loop through Accounts only once
    for(Account a : accountWithOpptys){
    	 for(Opportunity o: a.Opportunities){
    	 	if(o.StageName == 'Closed - Won'){
    	 		System.debug('Opportunity Closed Won...do some more logic here...');
    	 	}else if(o.StageName =='Closed - Lost'){
    	 		System.debug('Opportunity Closed Lost...do some more logic here...');
    	 	}
    	 }
    
   }
}

知識點:
好壞相比較來看:
1:SOQL語句少了一個肯定提高效率。
2:嵌套的For循環也減少了一個,改成了If判斷的形式來實現了。
Salesforce之所以這麼算計效率原因就是因爲他是Online的產品,儘可能的減少資源浪費一直是他所推薦的。

Best Practice #5: 做好一個對象上多個Trigger 的排序

原則就是一個事物處理裏不要有重複查詢同一個數據集的操作。
一種辦法是在一個大的Trigger裏做好分支判斷,保證每個業務的處理邏輯清晰。
另外一種辦法是爲多個業務做多個Trigger,從而保證Trigger處理數據不被幹擾。

Best Practice #6: 使用For循環來查詢大數據

Bad Sample

//A runtime exception is thrown if this query returns enough records to exceed your heap limit.
Account[] accts = [SELECT id FROM account];

Good Sample

// Use this format for efficiency if you are executing DML statements 
// within the for loop.  Be careful not to exceed the 150 DML statement limit.
Account[] accts = new Account[];
for (List<Account> acct : [SELECT id, name FROM account
                            WHERE name LIKE 'Acme']) {
    // Your logic here
    accts.add(acct);
}
update accts;

知識點:Salesforce的Limit裏還有Heap的異常等等,所以在查詢大數據量的同時爲了避免出現系統異常建議採用For循環的形式來取得數據,並形成集合對象,最後在進行數據庫操作。
因爲For循環的處理是取200件處理一次,讓後在處理200件,這樣不會出現一次取數據過多導致系統異常的問題。

Best Practice #7: 靈活使用 Limits Class來判斷和處理Limits的問題

事例代碼:

trigger accountLimitExample on Account (after delete, after insert, after update) {

    System.debug('Total Number of SOQL Queries allowed in this Apex code context: ' +  Limits.getLimitQueries());
    System.debug('Total Number of records that can be queried  in this Apex code context: ' +  Limits.getLimitDmlRows());
    System.debug('Total Number of DML statements allowed in this Apex code context: ' +  Limits.getLimitDmlStatements() );
    System.debug('Total Number of CPU usage time (in ms) allowed in this Apex code context: ' +  Limits.getLimitCpuTime());
    
   // Query the Opportunity object 
    List<Opportunity> opptys = 
        [select id, description, name, accountid,  closedate, stagename from Opportunity where accountId IN: Trigger.newMap.keySet()];
   
    System.debug('1. Number of Queries used in this Apex code so far: ' + Limits.getQueries());
    System.debug('2. Number of rows queried in this Apex code so far: ' + Limits.getDmlRows());
    System.debug('3. Number of DML statements used so far: ' +  Limits.getDmlStatements());    
    System.debug('4. Amount of CPU time (in ms) used so far: ' + Limits.getCpuTime());
    if (opptys.size() + Limits.getDMLRows() > Limits.getLimitDMLRows()) {
            System.debug('Need to stop processing to avoid hitting a governor limit. Too many related Opportunities to update in this trigger');
            System.debug('Trying to update ' + opptys.size() + ' opportunities but governor limits will only allow ' + Limits.getLimitDMLRows());
            for (Account a : Trigger.new) {
                a.addError('You are attempting to update the addresses of too many accounts at once. Please try again with fewer accounts.');
            }
    }
    else{
        System.debug('Continue processing. Not going to hit DML governor limits');
        System.debug('Going to update ' + opptys.size() + ' opportunities and governor limits will allow ' + Limits.getLimitDMLRows());
        for(Account a : Trigger.new){
            System.debug('Number of DML statements used so far: ' +  Limits.getDmlStatements());
            for(Opportunity o: opptys){ 
                if (o.accountid == a.id)
                   o.description = 'testing';
            }
        }
        update opptys;
        System.debug('Final number of DML statements used so far: ' +  Limits.getDmlStatements());
        System.debug('Final heap size: ' +  Limits.getHeapSize());
    }
}

DEBUG|Total Number of SOQL Queries allowed in this Apex code context: 100
DEBUG|Total Number of records that can be queried in this Apex code context: 10000
DEBUG|Total Number of DML statements allowed in this Apex code context: 150
DEBUG|Total Number of CPU usage time (in ms) allowed in this Apex code context: 10000
DEBUG|1. Number of Queries used in this Apex code so far: 1
DEBUG|2. Number of rows queried in this Apex code so far: 0
DEBUG|3. Number of DML statements used so far: 0
DEBUG|4. Amount of CPU time (in ms) used so far: 9
DEBUG|Continue processing. Not going to hit DML governor limits
DEBUG|Going to update 3 opportunities and governor limits will allow 10000
DEBUG|Number of DML statements used so far: 0
DEBUG|Final number of DML statements used so far: 1
DEBUG|Final heap size: 1819

Best Practice #8: 適時使用@future 功能

代碼示例:

trigger accountAsyncTrigger on Account (after insert, after update) {
    //By passing the @future method a set of Ids, it only needs to be
    //invoked once to handle all of the data. 
    asyncApex.processAccount(Trigger.newMap.keySet());
}
global class asyncApex {
 
  @future 
  public static void processAccount(Set<Id> accountIds) {
       List<Contact> contacts = [select id, salutation, firstname, lastname, email from Contact where accountId IN :accountIds];
       for(Contact c: contacts){
 	 System.debug('Contact Id[' + c.Id + '], FirstName[' + c.firstname + '], LastName[' + c.lastname +']');
 	                        c.Description=c.salutation + ' ' + c.firstName + ' ' + c.lastname;
        }
        update contacts;
  }
}

知識點:@future 是異步調用的標記,帶這個標記的方法被調用的時候不是馬上執行,而是salesforce通過判斷服務器端情況適時調用,這麼做雖然不可預期執行時間,但是可以提交系統使用效率,避免觸及salesforce的Limit。
注意點:如果異步方法和同步方法有可能同時更新同一條數據或者造成死鎖的可能的話,需要引用鎖的機制來解決。另外需要注意要批量處理數據。
異步方法調用也有Limit:
No more than 10 method calls per Apex invocation
No more than 200 method calls per Salesforce license per 24 hours
The parameters specified must be primitive dataypes, arrays of primitive datatypes, or collections of primitive datatypes.
Methods with the future annotation cannot take sObjects or objects as arguments.
Methods with the future annotation cannot be used in Visualforce controllers in either getMethodName or setMethodName methods, nor in the constructor.
It’s important to make sure that the asynchronous method

Best Practice #9: 如果有大量數據的處理邏輯,需要在測試Class也進行大數據量的測試

代碼示例:

public class sampleTestMethodCls {

	static testMethod void testAccountTrigger(){
		
		//First, prepare 200 contacts for the test data
		Account acct = new Account(name='test account');
		insert acct;
		
		Contact[] contactsToCreate = new Contact[]{};
		for(Integer x=0; x<200;x++){
		    Contact ct = new Contact(AccountId=acct.Id,lastname='test');
		    contactsToCreate.add(ct);
		}
		
		//Now insert data causing an contact trigger to fire. 
		Test.startTest();
		insert contactsToCreate;
		Test.stopTest();	
	}	
}

知識點:
1:在測試Class的創建測試數據階段,適時的創建大於200件數據來測試多批次處理邏輯是否有問題
2:測試一個事物裏的執行可以使用Test.startTest();和Test.stopTest();來包圍測試對象方法,從而準確的看出結果。

Best Practices #10:停止在代碼裏使用Hard Code

Bad Sample

for(Account a: Trigger.new){
     	 
   //Error - hardcoded the record type id
   if(a.RecordTypeId=='012500000009WAr'){     	  	
      //do some logic here.....
   }else if(a.RecordTypeId=='0123000000095Km'){
      //do some logic here for a different record type...
   }
    	 
}

Good Sample

//Query for the Account record types
     List<RecordType> rtypes = [Select Name, Id From RecordType 
                  where sObjectType='Account' and isActive=true];
     
     //Create a map between the Record Type Name and Id for easy retrieval
     Map<String,String> accountRecordTypes = new Map<String,String>{};
     for(RecordType rt: rtypes)
        accountRecordTypes.put(rt.Name,rt.Id);
     
      for(Account a: Trigger.new){
     	 
     	  //Use the Map collection to dynamically retrieve the Record Type Id
     	  //Avoid hardcoding Ids in the Apex code
     	  if(a.RecordTypeId==accountRecordTypes.get('Healthcare')){     	  	
     	  	 //do some logic here.....
     	  }else if(a.RecordTypeId==accountRecordTypes.get('High Tech')){
     	  	 //do some logic here for a different record type...
     	  }
     	 
     } 

知識點:Recordtype,Profile,user,queue,Group等都有單獨的系統表存儲數據,可以通過Select語句查詢到,所以代碼裏需要這些數據的時候可以到系統表裏查詢,
原則上salesforce是不建議固定ID的,因爲開發環境和生產環境的ID是不一樣的,這樣的Hardcode在開發環境上好用,在生產環境上是會出問題的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章