ガバナ制限は怖くない（デバッグ方法と減らし方）

## 目次

- [1.デバッグ方法](#1デバッグ方法)
  - [SOQL（発行数・レコード数）、DML（発行数・レコード数）](#soql発行数レコード数dml発行数レコード数)
    - [デバッグログ(LIMIT_USAGE)（2022/03/28追記）](#デバッグログlimit_usage20220328追記)
    - [開発者コンソール（Limitsタブ）](#開発者コンソールlimitsタブ)
    - [Limitsクラスでのデバッグ文出力](#limitsクラスでのデバッグ文出力)
  - [CPU時間（2022/03/28追記）](#cpu時間のデバッグ方法20220328追記)
    - [開発者コンソール（Timelineタブ）](#開発者コンソールtimelineタブ)
    - [デバッグログ(CUMULATIVE_LIMIT_USAGE)](#デバッグログcumulative_limit_usage)
  - [ヒープサイズ（2022/03/28追記）](#ヒープサイズのデバッグ方法20220328追記)
- [2.ガバナ消費の減らし方](#2ガバナ消費の減らし方)
  - [SOQL発行数](#soql発行数)
  - [SOQLレコード数](#soqlレコード数)
  - [DML発行数](#dml発行数)
  - [DMLレコード数](#dmlレコード数)
  - [CPU時間](#cpu時間)
    - [補足](#補足)
  - [ヒープサイズ](#ヒープサイズ)
  - [ビューステート](#ビューステート)
- [3.バッチについて](#3バッチについて)
  - [バッチはスケジューリングしかできないわけじゃない](#バッチはスケジューリングしかできないわけじゃない)
  - [バッチでも落ちる場合](#バッチでも落ちる場合)
- [まとめ](#まとめ)
- [参考文献](#参考文献)

## 1.デバッグ方法

※ビューステートに関しては、デバッグ方法が少し特殊で、
分量が多いので[別記事](https://qiita.com/xQIEBo4zFWKZ0ig/items/a990a21a3969dfadb33c)にしています。
<br>
### SOQL（発行数・レコード数）、DML（発行数・レコード数）

#### デバッグログ(LIMIT_USAGE)（2022/03/28追記）

デバッグログを```LIMIT_USAGE```という単語で検索すると、各ガバナ消費数が見れます。

※デバッグレベル```Apexプロファイリング```が```INFO```以上の必要があります。（2022/04/02　追記）

#### 開発者コンソール（Limitsタブ）

1. Debugをクリック
1. View Log Panelsをクリック
1. Execution Overviewにチェック
1. Limitsタブをクリック

※Limitsタブでガバナ消費を表示するには、デバッグレベルをデフォルトの設定より高くする必要があります。

【ガバナ消費が見れるデバッグレベルの設定例】

|項目|デバッグレベル|
|----|--------------|
|データベース|FINEST|
|ワークフロー|FINER|
|入力規則|INFO|
|コールアウト|FINER|
|Apexコード|FINEST||
|Apexプロファイリング|FINEST|
|Visualforce|INFO|
|システム|DEBUG|
|Wave|FINER|
|NextBestAction|INFO|

#### [Limitsクラス](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_limits.htm)でのデバッグ文出力

デバッグだけじゃなく、ガバナ消費状況に合わせてSOQL・DMLを発行するのに使ったりもできます。

例）
```
// SOQLの発行数/SOQLの発行数上限
System.debug(String.valueOf(Limits.getQueries()) + '/' + String.valueOf(Limits.getLimitQueries()));
```
<br>

### CPU時間（2022/03/28追記）

#### 開発者コンソール（Timelineタブ）

1. Debugをクリック
2. View Log Panelsをクリック
3. Execution Overviewにチェック
4. Timelineタブをクリック

- CPU時間に含まれるもの
  - APEX_CODE
  - WORKFLOW
  - VISUALFORCE
- CPU時間に含まれないもの
  - DB
- CPU時間に含まれるか分からないもの（分かる方がいたら教えていただけると嬉しいです）
  - VALIDATION
  - CALLOUT
  - CALLOUT

#### デバッグログ(CUMULATIVE_LIMIT_USAGE)

こちらは[公式のヘルプ](https://help.salesforce.com/s/articleView?id=000337320&type=1)に記載されていたのですが、
私の環境だと出力されずでした。
<br>

### ヒープサイズ（2022/03/28追記）

[Limitsクラス](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_limits.htm)でのデバッグ文出力
<br>

## 2.ガバナ消費の減らし方

### SOQL発行数

- ループ内での発行を止める
例）IN句で一括検索する
```
List<Account> acctMap = [SELECT Id FROM Account WHERE Id IN :acctIdList];
```

- [JavaScript Remoting](https://developer.salesforce.com/docs/atlas.ja-jp.pages.meta/pages/pages_js_remoting_example.htm?search_text=JavaScript%20Remoting)を検討する
- カスタム設定を使う
    - [カスタム設定](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_customsettings.htm)はSOQLを使わないでセレクトできる簡易オブジェクトみたいなもの
    - ただ動的具合は低いので、複雑度が低めな処理のときに使う印象
    - [こちらの記事](https://gawatari.com/custom-setting/)がカスタム設定の可能性を模索していて勉強になりました
- バッチ・ future メソッド化を検討する

### SOQLレコード数

- 余分なレコードを取っているときはwhereやlimit句で絞り込む
- SOQLを分割する
- VisualforceページのreadOnly属性をtrueにする
    - ※そのページでの更新処理はできなくなる
- ApexメソッドにReadOnlyアノテーションをつける
    - ```@RemoteAction```アノテーションをつけるか、Webservice（外部サービスへの公開用キーワード）をつける必要がある
    - ```@RemoteAction```アノテーションの使い方については[こちらの記事](https://www.terrasky.co.jp/blog/2013/131023_001312.php)が分かりやすかったです
- バッチ化を検討する
    - 非同期で制限は緩和されないが、バッチだとスコープ・ジョブ・スケジュール単位でトランザクションを分割しやすい

### DML発行数

- ループ内での発行を止める
例）コレクションに対してDMLを発行する
```
insert acctList;
```
- ```@RemoteAction```アノテーションをつけてトランザクションを分割する
- バッチ化を検討する
    - 非同期で制限は緩和されないが、バッチだとスコープ・ジョブ・スケジュール単位でトランザクションを分割しやすい

### DMLレコード数

- DML処理が不要なレコードが処理に混じっているときは排除する
- DMLを分割する
- バッチ化を検討する
    - 非同期で制限は緩和されないが、バッチだとスコープ・ジョブ・スケジュール単位でトランザクションを分割しやすい

### CPU時間

- バッチ・ future メソッド化を検討する
- [マップベースクエリ](https://help.salesforce.com/s/articleView?id=000339361&type=1)を検討する
- 2層以上のループ内でListの突き合わせをしている場合は、マップを使ってループの階層を浅くする

例）
```
List<Account> acctList = [select Name from Account];
List<Contact> conList = [select AccountId from Contact];

// これを作りたい
Map<String, Contact> acctNameConObjMap = new Map<String, Contact>();

for(Account acct : acctList){
    for(Contact con : conList){
        if(acct.Id == con.AccountId){
            acctNameConObjMap.put(acct.Name, con);
        }
    }
}

// ↓マップを使うと、、、

Map<Id, Account> acctMap = new Map<Id, Account>(acctList);
Map<Id, Contact> acctIdconObjMap = new Map<Id, Contact>();
for (Contact con : conList) {
    acctIdconObjMap.put(con.AccountId, con);
}

for(Id acctId : acctMap.keySet()){
    if(acctIdconObjMap.containsKey(acctId)) acctNameConObjMap.put(acctMap.get(acctId).Name, acctIdconObjMap.get(acctId));
}
```

処理の内容に意味はないので頭に入りづらいかもしれませんが、
仮にacctListが10個・conListが10個だった場合、ループ内の処理が動く回数は以下のようになります。
- Listの突き合わせ：100回
- Map：20回

#### 補足

CPUガバナには、DB処理時間は含まれません。

※一応アプリケーションサーバ上でのDB処理時間はガバナを食うんですが、
メインの時間はDBサーバ上の処理が占めてるので、結果DB処理は気にしなくていいという感じです。

### ヒープサイズ

- sObjectのセレクト項目数を減らす
- SOQL For ループを使う
- 使わない変数のメモリは解放する（nullを代入する）
- バッチ・ future メソッド化を検討する

### ビューステート

分量が多いのでデバッグ方法・減らし方ともに[別記事](https://qiita.com/xQIEBo4zFWKZ0ig/items/a990a21a3969dfadb33c)にしています。

## 3.バッチについて

### バッチはスケジューリングしかできないわけじゃない

どうしても画面から容量大きめな処理をしたい！ってときは、画面からバッチを動かすこともできます。

例）
```
// 画面から呼ぶ関数
public void btnAction(){
    Batch_Class batchClass = new Batch_Class();
    Database.executeBatch(batchClass, BATCH_SIZE);
}
```

バッチが動いてる間はボタンを非活性にして、
「実行中…」みたいなテキストを出してあげると親切ですね。

バッチの実行状況はApexジョブオブジェクトから取れるので、
それを使うと実行状況を把握した処理が作れます。

### バッチでも落ちる場合

バッチ（非同期）も万能なわけではありません。
数万〜数千万レコードあるオブジェクトを扱うと落ちるときもあります。

そんなときは以下を検討してみると良いです。
- スコープを減らす
    - スコープを減らすとトランザクションが分割され、ガバナ消費がリセットされます
- ジョブを分割する
    - 同じスケジュールクラス内でもジョブを分割できます（最大100ジョブまで）
- バッチを分割する


## まとめ
個人的にはガバナ制限は嫌いじゃないです。
適切な設計に導かれるので。

ガバナに引っかかるときは、以下のように設計かコードに改善点があることが多い気がします。
- モジュールの責務が大きすぎる
- 冗長な処理の書き方をしている

他のプラットフォームだと無茶して実装することができるのですが、
ガバナ制限があると改善せざるを得ないので、ある程度ソフトウェアの品質向上につながるな〜と思っています。

ガバナ制限以外にもApexの設計にはいくつか注意点があるので、どこかで記事にするつもりです。

読んでいただきありがとうございました。

## 参考文献

- [Apex ガバナ制限](https://developer.salesforce.com/docs/atlas.ja-jp.salesforce_app_limits_cheatsheet.meta/salesforce_app_limits_cheatsheet/salesforce_app_limits_platform_apexgov.htm)
- [Salesforceガバナ制限を回避するためにやってきたこと](https://qiita.com/comefigo/items/48f05d8fe4a3959911f4)
- [Visualforceページから@ReadOnlyメソッドを実行する - TECH BLOG | 株式会社テラスカイ](https://www.terrasky.co.jp/blog/2013/131023_001312.php)