Salesforceガバナ制限を回避するためにやってきたこと

# 概要

　Salesforceプラットフォーム上での開発経験をもとに今までSalesforceガバナ制限を回避するためにやってきたことについてまとめました  
なお、今後のSalesforceのアップデートによってガバナ制限の対象外や緩和になることもありますので、最新のリファレンスをご確認して頂くか、修正依頼して頂ければ修正させて頂きます

[Salesforce Summer'16 ガバナ制限リファレンス](https://developer.salesforce.com/docs/atlas.ja-jp.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_apexgov.htm)

# Salesforceガバナ制限とは

 Salesforceはマルチテナントのプラットフォームであるため、特定ユーザがサーバリソースを占有することによる他ユーザへの影響を最小限に抑えるためにリソース利用に関する様々な制限のこと


# 各種回避方法

下記のガバナ制限項目の非同期とはApexの一括処理(バッチ)とfutureメソッドを指します  
例：非同期200回と記載されている場合は、Apex一括処理またはFutureメソッド内で200回までコールできる

## 1. データ取得編

#### 1トランザクション内で発行されるSOQLクエリの合計数(同期100回/非同期200回)

##### **回避案**
 - 複数の取得処理をまとめて処理することでクエリ発行数を抑える  

```sfdc:Apex
public with sharing class SelectController{
  //NGパターン
  public String selectRecord_NG() {
    //リレーションをたどればこんな処理は必要ないですが、NG例のためにあえてこうしています
    //1,000レコードが取得されるとする
    //for文内でデータ取得した場合は、容易に制限を超えます
    for (Account acc: [Select Id, Name From Account Limit 1000]) {
      List<Contact> cons = [Select Name From Contact Where AccountId = :acc.Id];
    }
    return 'ng';
  }

  //OKパターン
  public String selectRecord_OK() {
    //1,000レコードが取得されるとする
    List<Account> accs = [Select Name From Account Limit 1000];
    Map<Id, Account> accsMap = new Map<Id, Account>(accs);
    //List化されたIDでまとめて取得する
    List<Contact> cons = [Select Name From Contact Where AccountId = :accsMap.keySet()];
    return 'ok';
  }
}
```

 - 非同期によるクエリの発行(トランザクションを分割)  
ビュー側から[RemoteActionアノテーション](https://developer.salesforce.com/docs/atlas.ja-jp.202.0.apexcode.meta/apexcode/apex_classes_annotation_RemoteAction.htm)メソッドによるデータ取得  
RemoteActionを使用することでAPIコール数制限に抵触しないため、トランザクションの分割には有効な手段  
ただし、[そのほか制限](https://developer.salesforce.com/docs/atlas.ja-jp.pages.meta/pages/pages_js_remoting_limits.htm?search_text=JavaScript%20Remoting)もありますので、実装時にご確認ください  
  - [公式ドキュメントの実装例](https://developer.salesforce.com/docs/atlas.ja-jp.pages.meta/pages/pages_js_remoting_example.htm?search_text=JavaScript%20Remoting)
  - [Javascript Remotingのパラメータ設定](https://developer.salesforce.com/docs/atlas.ja-jp.pages.meta/pages/pages_js_remoting_configuring_request.htm?search_text=JavaScript%20Remoting)
  - [Javascript Remotingの制限](https://developer.salesforce.com/docs/atlas.ja-jp.206.0.pages.meta/pages/pages_js_remoting_limits.htm)

```sfdc:Visualforce
<script type="text/javascript">
  var param1 = '';
  Visualforce.remoting.Manager.invokeAction(
    //文字列で'Apexコントローラ名.メソッド名'も可能
    //ただし、パッケージなどの名前空間は解決してくれないので独自実装が必要
    '{!$RemoteAction.RemoteActionController.getRecord}',
    param1,
    function(result, e){
      //取得後の処理
    },
    {escape :false});
</script>
```

```sfdc:Apex
public with sharing class RemoteActionController{
  @RemoteAction
  public static String getRecord(String strParam) {
    String response = 'xxxxxxxxxx';
    return response;
  }
}
```

 - カスタム設定の利用  
更新頻度が低いデータをカスタム設定で持つことで、SOQLの発行回数を抑えることができます  
[カスタム設定リファレンス](https://help.salesforce.com/articleView?id=cs_accessing.htm&language=ja&type=0)

#### 1トランザクション内で発行されたSOQLクエリによって取得されるレコードの合計数((非)同期50,000レコード)  

##### **回避案**
 - Limit句でのレコードの取得  
Limit句を付与することで明確に50,000レコード以下で取得できる
また、Limitクラスと組み合わせることで動的にLimit値を指定することができます

```sfdc:Apex
//50,000レコード以上があると仮定
[SELECT COUNT() FROM Lead Limit 50000];

//with Limit Class
//取得可能レコード数 = 最大取得可能レコード数 - 消費済みレコード数
Integer canExecuteRowsLimit = Limits.getLimitQueryRows() - Limits.getQueryRows();
[SELECT COUNT() FROM Lead Limit :canExecuteRowsLimit];
```

 - VisualforceのreadOnly属性をtrue  
Visualforceから要求したクエリが最大1,000,000行を取得できるようになる  
また、繰り返し表示用のコンポーネント(\<apex:dataTable>、\<apex:dataList>、\<apex:repeat>)が扱える項目数が1,000→10,000になる    
ただし、ページ全体が読み取り専用になるためデータ更新処理はできなくなります  
  - [公式ドキュメントの実装例](https://developer.salesforce.com/docs/atlas.ja-jp.pages.meta/pages/pages_controller_readonly_context_pagelevel.htm?search_text=readonly)
  - [apex\:pageリファレンス](https://developer.salesforce.com/docs/atlas.ja-jp.pages.meta/pages/pages_compref_page.htm?search_text=readonly)

```sfdc:Visualforce
<apex:page controller="LargeRecordController" readOnly="true">
    <p>record size: {!largeRecordSize}</p>
</apex:page>
```

```sfdc:Apex
public with sharing class LargeRecordController{
  public Integer getLargeRecordSize() {
    //50,000件を超えるレコードを取得できると仮定
    Integer recordSize = [SELECT COUNT() FROM Lead];
    return recordSize;
  }
}
```

 - ApexコントローラのReadOnlyアノテーションメソッドによるデータ取得  
VisualforceでreadOnly属性を付与した場合と同様にApexで取得できるレコード数が最大1,000,000行になる  
ただし、ReadOnlyアノテーションが付与されているメソッド内で発生したトランザクションのみに適用される  
また、同メソッド内でのデータ更新処理はできない  
Visualforceが読み取り専用になるわけではないので、データ取得とデータ更新処理を異なるApexメソッドで実装が可能  

```sfdc:Visualforce
<apex:page controller="ReadOnlyController">
  Visualforce.remoting.Manager.invokeAction(
    '{!$RemoteAction.ReadOnlyController.getRecord}',
    function(result, e){
      //取得後の処理
    },
    {escape :false});
</apex:page>
```
```sfdc:Apex
public with sharing class ReadOnlyController{
  @ReadOnly
  @RemoteAction
  public String getRecord() {
    //50,000件を超えるレコードを取得できると仮定
    List<Lead> leads = [SELECT Id, Name FROM Lead];
    return 'ok';
  }
}
```

 - 非同期によるクエリの発行(トランザクションを分割)  
ビュー側からRemoteActionによるデータ取得


## 2. データ更新編

#### 1トランザクション内で発行されたDMLステートメントの合計数((非)同期150回)
#### 1トランザクション内で発行されたDMLステートメント、Approval.process、database.emptyRecycleBin の結果として処理されるレコードの合計数((非)同期10,000レコード)

##### **回避案**
 - 複数の更新処理をまとめて処理することでDML発行数を抑える
   - [公式ドキュメントの実装例](https://developer.salesforce.com/docs/atlas.ja-jp.202.0.apexcode.meta/apexcode/langCon_apex_loops_for_SOQL.htm)

```sfdc:Apex
public with sharing class UpdateController{
  //NGパターン
  public String updateRecord_NG() {
    //1,000レコードが取得されると仮定
    //for文内でのデータ更新をした場合は、容易に制限を超えます
    for (Account acc: [Select Name From Account Limit 1000]) {
      acc.Name = 'new name';
      update acc;
    }
    return 'ng';
  }

  //OKパターン
  public String updateRecord_OK() {
    //1,000レコードが取得されると仮定
    List<Account> accs = [Select Name From Account Limit 1000];
    //for文内でオブジェクト内のデータを更新し、最後にリスト全体を更新することで消費量を抑えられる
    for (Account acc: accs) {
      acc.Name = 'new name';
    }
    update accs;
    return 'ok';
  }
}
```
 - RemoteActionによる非同期でのDML発行(トランザクションを分割)  
上記のRemoteActionによるデータ取得と同様にトランザクションを分割して、DMLを発行することで回避が可能になる  
<br>
 - Apexバッチによる非同期処理  
Salesforceプラットフォームのスケジューリング実行になるため、毎回一定時間内に完了する保証がない  
そのため、処理を待つ必要がない夜間の大量データ更新などに適しています 
ただし、バッチ処理の実行回数に制限がありますので、下記の制限事項をご確認ください 
  - [公式ドキュメントの実装例](https://developer.salesforce.com/docs/atlas.ja-jp.202.0.apexcode.meta/apexcode/apex_batch_interface.htm)  
  - [Apex の一括処理のガバナ制限](https://developer.salesforce.com/docs/atlas.ja-jp.202.0.apexcode.meta/apexcode/apex_batch_interface.htm#BatchApexLimitsSection)
<br>
 - Futureアノテーションメソッドによる非同期でのDML発行  
DML発行数をある程度Futureメソッドに分散することでDML発行数およびDMLレコード数を増やすことが可能  
ただし、Futureメソッドの呼び出し回数に制限があるので、下記の制限事項をご確認ください
  - [Futureアノテーションの実装例](https://developer.salesforce.com/docs/atlas.ja-jp.202.0.apexcode.meta/apexcode/apex_invoking_future_methods.htm)
  - [ガバナ制限 - Force.comプラットフォームのApex制限](https://developer.salesforce.com/docs/atlas.ja-jp.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_apexgov.htm)



## 3. そのほか処理編

#### ヒープの合計サイズ(同期6MB/非同期12MB)

##### **回避案**
 - 使わなくなった変数を初期化
 - SOQL for Loopの利用でメモリの利用が抑えられる  
ただし、上記のようにLoop内でのDMLやSOQL発行回数の制限にはご注意ください
 - Apexバッチによる分割処理でヒープサイズを抑える  
また、バッチ実行によりヒープ上限値が12MBになるので、制限回避に期待ができる

#### Salesforceサーバの最大CPU時間(同期10,000ms/非同期60,000ms)

##### **回避案**
 - Mapの利用  
利用シーンによりますが、例えばfor文の入れ子状態でデータの突合せ処理をしている場合、  
その入れ子状態を解消するために親for文の変数をMap化して、キー値でうまく突合せられるような仕組みにするだけで劇的に時間が短縮することができます  
  - [Map Classリファレンス](https://developer.salesforce.com/docs/atlas.ja-jp.202.0.apexcode.meta/apexcode/apex_methods_system_map.htm)

```sfdc:Apex
public with sharing class CpuLimitController{
  //NGパターン
  public String cpu_NG() {
    //リレーションをたどればこんな処理は必要ないですが、NG例のためにあえてこうしています
    List<Account> accs = [Select Id, Name From Account Limit 10000];
    List<Contact> cons = [Select Name, AccountId From Contact Limit 10000];
    for (Account acc: accs) {
      for (Contact con: cons) {
        if(acc.Id == con.AccountId) {
          //なにか処理
        }
      }
    }
    return 'ng';
  }

  //OKパターン
  public String cpu_OK() {
    List<Account> accs = [Select Id, Name From Account Limit 10000];
    List<Contact> cons = [Select Name, AccountId From Contact Limit 10000];
    //Map化する
    Map<Id, Account> accsMap = new Map<Id, Account>(accs);
    for (Contact con: cons) {
      //Mapのキー値と一致している場合
      if(accsMap.containsKey(con.AccountId)) {
        //なにか処理
      }
    }
    return 'ok';
  }
}
```


## 4. まとめ

上記以外にも様々な工夫を施せば、ガバナ制限回避につながると思います
基本的に非同期処理(一括処理、Future、RemoteActionなど)であればある程度の制限は回避できると思います
また、Limit Classの活用によりガバナ制限による処理の強制終了を事前に防ぐことも効果的でしょう  
ただし、それでも回避できない場合は、外部にデータを転送して、外部で処理後に再度Salesforceに戻すのであれば、かなりの心配事はなくなるでしょう(実装コストが高くなりますが････)


以上、皆さんのお役に立てれば幸いです