【Apex】ガバナ制限を知るにはまずはエラーを起こしてみようという発想

Salesforce開発には切っても切り離せない存在、ガバナ制限。
エラーが出ては修正する。といった経験をした開発者の方も多いと思います。

改めてガバナ制限について向き合おうと思ったときに、
まずは実際にガバナ制限に引っかかりそうなロジックを書いてみようと思ったのが当記事投稿のきっかけです。

今回は**トランザクション単位の 同期 Apex の制限**のみ取り扱います。

# ガバナ制限とは

そもそもガバナ制限とはなにか？
[Apexガバナ制限](https://developer.salesforce.com/docs/atlas.ja-jp.236.0.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_apexgov.htm)のページには下記のように記載されています。

>Apex はマルチテナント環境で実行するため、Apex ランタイムエンジンは、回避 Apex コードまたはプロセスが共有リソースを独占しないよう制限事項を強制します。

つまり、マルチテナントを維持するために一個の組織がリソースを使いすぎないようにかけている制限 ということですね。

# トランザクション
ガバナ制限を意識するうえで確認する必要があるのが**トランザクション**の単位です。

[Apex のトランザクション](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_transaction.htm)のページには

>トランザクションの境界は、トリガ、クラスメソッド、匿名のコードブロック、Visualforce ページ、カスタム Web サービスメソッドのいずれかにすることができます。

とあります。通常のApexトリガだと200レコードで1トランザクションですね。
Database.executeBatch()やデータローダではバッチサイズ（1トランザクションあたりのレコード数）を指定することができます。

# 匿名実行でLimitException祭り
今回は開発者コンソールのDebug → Open Execute Anonymous Window にガバナ制限に引っかかるコードを書いて実行します。
基本的にはfor文で規定回数回して落とすやり方です。落とすだけのコードだと少し味気が無いので、無駄に変数増やしてadd()とかしています。
**LimitException（ガバナ制限のエラー）** がちゃんと出るかの検証と考察を書いていきます。

### 発行される SOQL クエリの合計数：100
```apex:
List<Account> allAccountList = new List<Account>();
for(Integer i = 0; i < 101 ; i++){
    String accName = '取引先' + i;
    List<Account> accountList = [SELECT Id FROM Account WHERE Name = :accName];
    allAccountList.addAll(accountList);
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1461666/eb8370d0-2bfc-435d-ed8e-a4f8ff9fc898.png)
`System.LimitException: Too many SOQL queries: 101`

`i < 100`では落ちませんが、`i < 101`ではちゃんとエラーになりました。（非同期処理だと200まで）
トリガ処理の途中にfor文があって、その中で呼んでいる別クラスのメソッドでクエリしていました。なんてソースはたまに見かけますね。
そうでなくとも、複雑なロジックを持つ組織では結構起こりそうな気がします。

### SOQL クエリによって取得されるレコードの合計数：50000
```apex:
List<Lead> allLeadList = new List<Lead>();
for(Integer i = 0; i < 50 ; i++){
    List<Lead> leadList = [SELECT Id FROM Lead];
    allLeadList.addAll(allLeadList);
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1461666/4f26050e-2ecb-4549-0832-e5b03b5ba411.png)
`System.LimitException: Too many query rows: 50001`
検証した環境はDeveloperエディションのためデータストレージ上限の関係上、1001件のリードデータしか用意しませんでした。
（データが少なすぎると前述のクエリ100回の制限にかかるので、501件以上はあったほうが検証しやすいです。）
1001件×50回ループでエラーを発生させました。
50000件以上あるオブジェクトであれば条件次第では1回で超えそうな制限ですね。
取得したレコードの使い道にもよりますが、不要なデータまで取らないようにWHERE句をしっかり記述することを心がけようと思いました。


### Database.getQueryLocator によって取得されるレコードの合計数：10000
```apex:
List<Lead> allLeadList = new List<Lead>();
for(Integer i = 0; i < 10 ; i++){
    Database.QueryLocator q = Database.getQueryLocator([SELECT Id FROM Lead]);
    Database.QueryLocatorIterator it =  q.iterator();
    while (it.hasNext()){
        allLeadList.add((Lead)it.next());
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1461666/71e1557b-64d1-62b8-f2a8-e654604034f3.png)
`System.LimitException: Too many query locator rows: 10001`
一個前のやつに似ていますが、Database.getQueryLocator()でのレコード取得になります。
1001件のリードデータなので、ループ10回で取得したレコードの合計数が10000を超えてエラーになりました。

### 発行される SOSL クエリの合計数：20
```apex:
List<Account> allAccountList = new List<Account>();
List<Lead> allLeadList = new List<Lead>();
for(Integer i = 0; i < 21 ; i++){
	List<List<SObject>> results = [FIND 'sample' IN NAME FIELDS RETURNING Account, Lead];
    allAccountList = (List<Account>)results[0];
    allLeadList = (List<Lead>)results[1];
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1461666/4981f614-c6c4-e552-2086-b183b176749e.png)
`System.LimitException: Too many SOSL queries: 21`
SOSLクエリを21回行ったら発生しました。
1トランザクションあたり20回と少ないですが、そんなに頻繁に使うイメージがないですね。使うとこはよく使うのかしら。
**1 つの SOSL クエリによって取得されるレコードの合計数：2000**
今回はデータストレージの関係上検証していませんが、SOSLについては↑レコード数の制限のほうを注意したほうがいいかもしれません。

### 発行される DML ステートメントの合計数：150
```apex:
List<Lead> leadList = [SELECT Id FROM Lead LIMIT 151];
for(Lead lead:leadList){
    lead.FirstName = '';
    update lead;
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1461666/5e61a1ce-23e1-9522-61f3-b4f195d14be0.png)
`System.LimitException: Too many DML statements: 151`
これもSOQLクエリと同じように、for文内で呼んでいる別メソッドでdmlしていました。みたいなことを見かけることがあります。
とくに子レコードのinsertとか多いですね。気を付けましょう。

### トランザクション内のコールアウト (HTTP 要求または Web サービスコール) の合計数：100
```apex:
List<Object> allAnimals = new List<Object>();
for(Integer i = 0; i < 101 ; i++){
    Http http = new Http();
	HttpRequest request = new HttpRequest();
	request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
	request.setMethod('GET');
	HttpResponse response = http.send(request);
	if(response.getStatusCode() == 200) {
	    Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
		List<Object> animals = (List<Object>) results.get('animals');
        allAnimals.addAll(animals);
	}
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1461666/0e4d5ac7-eb4b-b732-48bc-980d9a2743bb.png)
`System.LimitException: Too many callouts: 101`
API連携なんかしているシステムでは、結構頻繁にコールアウトが発生しますね。
1レコードずつ処理なんかしていると簡単に引っかかりそうです。
**トランザクション内のすべてのコールアウト (HTTP 要求または Web サービスコール) のタイムアウトの最大累積値：120 秒**
こちらも要注意ですね。

### 許可される sendEmail メソッドの合計数：10
```apex:
for(Integer i = 0; i < 11 ; i++){
    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
   	mail.setToAddresses(new String[]{'pie@pie.com'});
	mail.setSubject('件名');
	mail.setPlainTextBody('本文');
	Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1461666/72f14217-2660-a34f-e898-903c76165842.png)
`System.LimitException: Too many Email Invocations: 11`
これも10回と少ないので、メール送信の処理をまとめてるロジックだとかかりやすそうです。
メール関連で言及するとガバナ制限以外に、1日に送信できるメール数の上限などもあるのでそちらも注意したいですね。


# おわりに
今回は簡単にこしらえることができたものだけ記載しました、
データ諸々揃って、気が向いたら他の制限にも引っかかって追記する予定です。

>親子リレーションのサブクエリを使用する SOQL クエリでは、各親子リレーションは追加クエリとみなされます。これらのクエリタイプは、最上位クエリ数の 3 倍に制限されています。

なんて楽しそうなことも書いてるので、これは別記事にまとめようと思います。

いったんはここまで～～