開発環境では完璧に動いたコードが、本番で突然止まる。しかもリリース後、ユーザーがデータを入れ始めたタイミングで、です。Salesforceを触っていると、この経験はほぼ全員が一度は通ります。原因の多くがガバナ制限です。
先に結論を書きます。ガバナ制限に本番で当たったとき、やるべきは「コードの微調整」ではなく「設計のやり直し」であることが多い。だから工期にも予算にも響きます。制限は後から潰すものではなく、設計の段階で避けるものです。
ここでは一般的な制限値の解説は手早く済ませます。私が実際に設計を作り替えた一件を軸に、どこで何を間違えると何週間が吹き飛ぶのかを具体的に書きます。
そもそもガバナ制限とは何なのか
Salesforceはマルチテナント基盤です。1つの物理リソースを多数の組織が共有している。だから特定の組織が暴走するコードを動かすと、他の組織まで巻き添えになります。
それを防ぐために、1トランザクションあたりの実行回数や使用量に上限を課す。これがガバナ制限です。
厄介なのは、超過したときに「警告」ではなく「即座に例外を投げて処理を止める」挙動を取る点です。System.LimitException が飛んで、そのトランザクションは丸ごとロールバックされる。中途半端に途中まで保存される、ということがない。全部なかったことになります。
制限値は同期処理と非同期処理で違います。ここを知らないと設計を誤る。よく参照する範囲だけ表にしておきます。
| 制限項目 | 同期処理 | 非同期処理(Batch/Future等) | 数える単位 |
|---|---|---|---|
| SOQLクエリ発行回数 | 100回 | 200回 | クエリの実行「回数」(件数ではない) |
| DMLステートメント数 | 150回 | 150回 | insert/update等の「文」の数 |
| 外部へのコールアウト | 100回 | 100回 | HTTP呼び出しの回数 |
| CPU時間 | 10,000ms | 60,000ms | 処理に要した計算時間 |
| ヒープサイズ | 6MB | 12MB | メモリ使用量 |
非同期にすると枠が広がるのが分かります。この一点が、後で出てくる回避策の軸になります。値そのものはどこの技術ブログにも書いてあるので、ここから先は実際に踏んだ話に入ります。
私が設計をやり直した一件
いちばん記憶に残っているのは、ある中規模ユーザー環境での外部連携です。基幹システムから取引先と商談を夜間に一括取り込みし、レコード作成をトリガーに外部の在庫APIを叩いて初期値を取ってくる、という要件でした。最初の設計はシンプルで、取引先が1件できるたびにAPIを1回コールする、いわゆる「1レコード=1コール」です。開発環境では数十件で快調に動いていました。
崩れたのは本番移行のリハーサルです。実データに近い3,000件を一気に流した瞬間、コールアウト100回の上限に当たってSystem.LimitException。例のとおり全件ロールバックです。
部分的に入った、ならまだ復旧の相談ができる。全部なかったことになると、原因調査からやり直しになります。
問題はコードの一行ではなく設計そのものでした。トリガーの同期処理の中で1件ずつ外部を呼ぶ構造だった以上、件数が増えれば必ず踏む。微調整では逃げられません。結局、トリガーからはPlatform Eventを発行するだけにして、受け側のQueueable Apexで取り込みレコードを100件単位にまとめ、外部APIにはバルク受けのエンドポイントを新設してもらって1回のコールで送る形に組み替えました。
ここで効いたのが相手システム側の調整です。こちらのApexを直すだけなら数日ですが、連携先にバッチ受けを作ってもらう交渉と実装待ちで、工期は結局2週間ほど伸びました。レビューの段階で数千件のバルクテストを一度でも流していれば、設計レビューで止められた話です。あの2週間は、制限を設計前提に置かなかったツケでした。
で、ほかの現場ではどこで引っかかるのか
私のケースは外部連携でしたが、踏み方にはいくつか定番があります。怖いのは「気づかないうちに踏む」ことです。代表的なものを挙げます。
SOQLの制限は「件数」ではなく「回数」で死ぬ
一番多い事故がこれです。ループの中でクエリを書いてしまうパターン。たとえば取引先100件をループして、1件ずつ関連する商談をSOQLで取りに行く。
これだと100回クエリが走ります。レコードが増えた瞬間に制限超過です。
1回のクエリで取る件数には別の上限(累計5万件)がありますが、現場で先に当たるのはほぼ「回数」のほうです。クエリ1本で大量件数を返すより、ループで何百回も細かく叩くコードのほうが危ない。この感覚は覚えておいて損がない。
DMLの150回制限は、1件ずつ保存すると即死する
レコードを1件ずつinsertする実装も定番の地雷です。DMLは「文の数」を数えるので、150件を1件ずつ入れたら150回。あとひと押しで超えます。
リストにまとめて1回のinsertで入れれば、何千件でもDMLは1回で済みます。ここは実装者の癖がそのまま出る場所です。
CPU時間とヒープは、本番データでしか再現しない
売上集計や予測計算のような重いロジックは、CPU時間10秒で止まります。大量レコードをメモリに抱える処理はヒープ6MBを超える。この2つの厄介さは、開発環境の数十件では絶対に再現しないことです。
本番の数十万件で初めて牙をむく。だからレビューをすり抜けやすい。私のコールアウトの件と同じで、少件数のテストは何の保証にもならないのが骨身にしみます。
実装パターン別、どう逃げるか
踏み方が分かれば、逃げ方も決まってきます。ケースごとに整理します。
一括作成: ループ内でinsertしない。結果をリストに詰めて、ループの外で一度だけDMLを実行する。部分的な失敗を許容したいならDatabase.insert(list, false)でSaveResultを受け取り、成功分だけ通して失敗分をログに残す。全件ロールバックを避けたい業務でよく使う手です。
親子参照: 子を取るたびにクエリを投げるのをやめる。1本のSOQLでサブクエリを使って子ごと取得するか、親IDをキーにしたMapに子を詰めてからメモリ上で引き当てる。「ループに入る前に必要なデータを全部取っておく」が鉄則です。
外部API連携: 同期で呼ばず、Platform Eventsや@future(callout=true)、Queueable Apexで非同期に逃がす。あるいは相手システム側でバッチ受けに対応してもらい、まとめて1回のコールアウトで送る。設計を相手と握る必要があるので、これは早めに決めたほうがいい。
私が2週間溶かしたのは、まさにここを後から握ったからです。詳しい組み方は外部連携の設計パターンをまとめた記事に書きました。
重い計算: トリガーやボタン押下の同期処理でやらない。Batch Apex(Database.Batchable)に移す。Batchはレコードの塊ごとにトランザクションが切り直されるので、ガバナ枠がリセットされます。
数万件の処理が現実的になるのはこの仕組みのおかげです。定期実行ならScheduled Apexと組み合わせる。
制限を回避する設計の鉄則
個別のテクニックより、頭の切り替えのほうが効きます。「1つのトランザクションで全部やりきる」という発想を捨てることです。仕事を非同期で分割する前提に立つ。これだけで踏む回数が激減します。
加えて、SOQLはループの外で全件取得してMapやSetで参照する。この「バルク化」の習慣が身についているかどうかで、コードの寿命が変わります。動くかどうかではなく、増えても動くかどうかで考える。
そしてテストは本番同等の件数で行う。開発環境の数十件で通ったテストは、何の保証にもなりません。私がレビューで必ず確認するのは「200件以上を流すバルクテストがあるか」です。
無ければ本番で止まる前提で見ます。あの2週間を知っていると、この一手間がいかに安いかが分かる。
FlowとApex、どちらが制限に当たりやすいか
ノーコードのFlowは便利ですが、ガバナ制限の観点ではApexより危ない面があります。理由は「制限が見えにくい」ことです。
Flowのループ要素の中にレコード取得(Get Records)や更新(Update Records)を置くと、Apexでいうループ内SOQL/DMLと同じことが起きます。しかもビジュアルで簡単に組めてしまうので、危険だと気づかないまま本番に出る。Apexなら例外が返って開発中に気づけますが、Flowは条件次第で沈黙して失敗することがあります。
対策として、Flowでも大量データを扱うなら非同期パス(Run Asynchronously)を使い、ループ内のDBアクセスを避ける設計にする。結局のところ、FlowでもApexでも「1処理=1トランザクションで終わらせる」という思い込みが根っこの原因です。ツールの問題ではありません。
ガバナ制限は、設計力をあぶり出す
制限があるから、開発者の質が見えます。これは案件の現場で本当にそう感じる。制限を気にせず書いた人のコードは、データが増えた瞬間に総崩れになる。逆に、最初からバルク化と非同期を前提に設計できる人のコードは、件数が10倍になっても静かに動き続けます。
この差は提案や面談の場でそのまま出ます。「大量データ処理への対策方針」を自分の失敗談込みで具体的に語れる開発者は、明らかに扱いが違う。技術そのものより「どう語れるか」で足元を見られるかが決まる話は、単価交渉でつまずく理由をまとめた記事に書きました。ガバナ制限への理解は、その語れる材料の一つになります。
ガバナ制限を「面倒な縛り」と捉えるうちは、たぶんどこかで踏みます。あれは、スケーラビリティを強制的に身につけさせるための仕組みだと思っておくくらいが、ちょうどいい。開発環境で動いた、では終われないのがこの仕事です。

コメント