結論(先に言う)
ガバナンス超過の原因は、ほぼ3つのパターンに集約されます。
どれも「開発環境では動く」「少ないデータでは気づかない」という共通点があります。本番に上げてから初めて発覚する、というのが厄介なところです。
パターン①:ループの中でrecord.loadを呼ぶ
一番よくやります。自分も最初にこれで詰まりました。
「受注一覧を検索して、1件ずつ顧客名を取得して処理する」という要件を素直に実装するとこうなります。
// NG:1件ごとにrecord.loadが走る search.create({ type: 'salesorder', filters: [['status', 'anyof', 'SalesOrd:B']], columns: ['internalid', 'entity'] }).each((result) => { const orderId = result.id; // これが1回10ユニット消費する const rec = record.load({ type: record.Type.SALES_ORDER, id: orderId }); const customerName = rec.getValue({ fieldId: 'entityid' }); log.debug('顧客名', customerName); return true; });
100件処理するとrecord.loadだけで1,000ユニット消費します。UserEventのガバナンス上限がちょうど1,000ユニットなので、これだけで上限に達します。Scheduledでも1,000件を超えたあたりから危なくなります。
対策:search.lookupFieldsでピンポイント取得する
record.loadは対象レコードの全フィールドをサーバーから取ってきます。1フィールドしか読まないのにレコード全体をロードするのは完全に無駄です。
必要なフィールドだけを取得するsearch.lookupFieldsに切り替えると、消費ユニットが大幅に下がります。
// OK:lookupFieldsで必要なフィールドだけ取得 search.create({ type: 'salesorder', filters: [['status', 'anyof', 'SalesOrd:B']], columns: ['internalid', 'entity'] }).each((result) => { const orderId = result.id; // record.loadの代わりにlookupFieldsを使う(消費ユニットが大幅に少ない) const fields = search.lookupFields({ type: search.Type.SALES_ORDER, id: orderId, columns: ['entityid'] }); const customerName = fields.entityid; log.debug('顧客名', customerName); return true; });
さらに言うと、検索のカラムに最初から必要なフィールドを含めておけばlookupFieldsすら不要になります。
// ベスト:検索時点で必要なフィールドを取得しきる search.create({ type: 'salesorder', filters: [['status', 'anyof', 'SalesOrd:B']], columns: ['internalid', 'entity', 'entityid'] // 必要なフィールドをここに全部入れる }).each((result) => { const customerName = result.getValue({ name: 'entityid' }); log.debug('顧客名', customerName); return true; });
「ループの中でrecord.loadを呼んでいないか」は、コードレビューのときに必ず確認するようにしています。
パターン②:afterSubmitで重い処理をぶん回す
前回の記事でも書いたのですが、afterSubmitで重い処理を書いてしまうパターンです。あのときは「保存が遅くなる」という問題として書きましたが、実はガバナンス超過の原因にもなります。
UserEventのガバナンス上限は1,000ユニットです。afterSubmitでsearch.eachを回して1件ずつrecord.submitFieldsを呼ぶと、あっという間に上限に達します。
// NG:afterSubmitでこれをやるとガバナンスもUXも死ぬ const afterSubmit = (ctx) => { const customerId = ctx.newRecord.getValue({ fieldId: 'entity' }); // search.create で10ユニット // each の中で submitFields が1件10ユニット // 100件で1,000ユニット → 上限到達 search.create({ type: 'salesorder', filters: [['entity', 'anyof', customerId]] }).each((result) => { record.submitFields({ type: record.Type.SALES_ORDER, id: result.id, values: { custbody_processed: false } }); return true; }); };
対策:afterSubmitではScheduledを蹴るだけにする
afterSubmitでやることは「Scheduled Scriptを起動するだけ」に絞る設計にします。Scheduledのガバナンス上限は10,000ユニットなので、同じ処理でも余裕が10倍あります。
// OK:afterSubmitはScheduledを起動するだけ define(['N/task', 'N/log'], (task, log) => { const afterSubmit = (ctx) => { if (ctx.type === ctx.UserEventType.DELETE) return; task.create({ taskType: task.TaskType.SCHEDULED_SCRIPT, scriptId: 'customscript_update_related', deploymentId: 'customdeploy_update_related', params: { custscript_customer_id: ctx.newRecord.getValue({ fieldId: 'entity' }) } }).submit(); log.debug('Scheduled起動', 'afterSubmit完了'); }; return { afterSubmit }; });
afterSubmitのガバナンス消費はtask.createとsubmitだけになるので、消費ユニットは数十で収まります。
パターン③:MapReduceのmapフェーズに処理を詰め込む
MapReduceを使えばガバナンス問題が解決する、と思って移行したのに、今度はmapフェーズで詰まるというパターンです。
MapReduceはフェーズごとに独立したガバナンスが設定されています。mapフェーズの1キューあたりのガバナンス上限も無制限ではありません。1件の処理にrecord.loadを複数回呼んだり、外部APIを叩いたりすると、1件ずつの処理でもガバナンスを使い切ることがあります。
// NG:mapの中で重い処理を詰め込む const map = (ctx) => { const result = JSON.parse(ctx.value); const orderId = result.id; // record.loadで10ユニット const order = record.load({ type: record.Type.SALES_ORDER, id: orderId }); // さらにrecord.loadで10ユニット const customerId = order.getValue({ fieldId: 'entity' }); const customer = record.load({ type: record.Type.CUSTOMER, id: customerId }); // N/https.getで10ユニット const response = https.get({ url: `https://api.example.com/customer/${customerId}` }); // record.submitFieldsで10ユニット record.submitFields({ type: record.Type.SALES_ORDER, id: orderId, values: { ... } }); ctx.write({ key: customerId, value: orderId }); };
1件の処理で40ユニット以上消費しています。件数が多いと積み重なってフェーズ単体で上限超えが起きます。
対策:mapでは「振り分けだけ」してreduceで処理する
mapフェーズの責務を「データの振り分け(ctx.writeするだけ)」に絞り、実際の更新処理はreduceフェーズに持っていく設計にします。reduceは同じkeyのデータをまとめて処理するフェーズなので、複数件をまとめて1回のAPI呼び出しに集約できます。
// OK:mapは振り分けだけ、処理はreduceで const map = (ctx) => { const result = JSON.parse(ctx.value); // mapでは最低限の情報だけwriteする ctx.write({ key: result.values.entity.value, // 顧客IDをキーに value: result.id // 受注IDをバリューに }); }; const reduce = (ctx) => { // 同じ顧客の受注IDが配列で来る → まとめて処理 const orderIds = ctx.values.map(v => JSON.parse(v)); // 顧客レコードは1回だけload const customer = record.load({ type: record.Type.CUSTOMER, id: ctx.key }); // 受注はsubmitFieldsでまとめて更新 orderIds.forEach(orderId => { record.submitFields({ type: record.Type.SALES_ORDER, id: orderId, values: { custbody_processed: true } }); }); ctx.write({ key: ctx.key, value: orderIds.length }); };
mapでrecord.loadを呼ぶのをやめるだけで、フェーズあたりのガバナンス消費が大幅に下がります。
3つのパターンに共通する根本原因
改めて整理すると、ガバナンス爆死の根本原因は「ループの中でコストの高い操作を呼んでいる」という1点に尽きます。
具体的にはこの3操作が高コストです。
record.load:レコード全体をDBから取得するrecord.save/record.submitFields:DBに書き込むN/https.get/N/https.post:外部APIを呼ぶ
これらをループの外に出す、呼び出し回数を減らす、というのがガバナンス節約の基本方針です。次のセクションでは具体的な節約テクニックをまとめます。
文字数は約2,400字、コードブロックは7つです。
パターン②の内部リンクURLは仮のままにしています。SuiteScript完全ガイド記事の実際のURLに差し替えてください。3つのパターンが「開発環境では動く → 本番で詰まる」という同じ構造で書けているので、読者が「自分もこれやってた」と感じやすい内容になっていると思います。