JunshuO
ServiceNow Employee

ServiceNow で数千のレコードを移行または更新する場合、1 つの大規模な GlideRecord クエリを実行すると、インスタンスの速度が低下し、操作の制御が難しくなる可能性があります。

200 件のレコードに対して完全に機能するスクリプトが、200,000 件のレコードでは大きく異なる動作をする可能性があります。結果セットが大きいと、メモリの消費量が多くなり、データベースリソースの保持期間が長くなり、実行時間の長いトランザクションがトリガーされ、ビジネスルール、フロー、監査履歴、および統合に負荷がかかる可能性があります。

ServiceNow アーキテクトとして、私は通常、次の 1 つの質問を念頭に置いて一括操作を検討します。

これは、より小さく、予測可能なチャンクで安全に処理できますか?

そこで GlideRecord.chooseWindow() が便利になります。

 

バッチ処理されていない一括操作の問題

一般的な一括更新スクリプトは次のようになります。

var gr = new GlideRecord('ast_contract');
gr.addQuery('active', true);
gr.query();

while (gr.next()) {
    gr.setValue('u_migration_status', 'migrated');
    gr.update();
}

これは簡単ですが、大規模にはリスクが伴う可能性があります。

クエリで非常に多数のレコードが返される場合、スクリプトは次の可能性があります。

  • 単一のトランザクションで実行時間が長すぎます。
  • データベースに不要な負荷をかけます。
  • ビジーなテーブルをロックするか、集中的にアクセスします。
  • ビジネスルールまたはフローを何千回もトリガーします。
  • 再起動制御が制限された状態で途中で失敗します。
  • すべてが 1 回の大規模な実行で発生するため、トラブルシューティングを困難にします。

一括作業は、1 回限りのギャンブルではなく、制御されたプロセスのように設計する必要があります。

 

ソリューションの概要:chooseWindow() を使用したレコードのバッチ処理

GlideRecord.chooseWindow(firstRow, lastRow) は、スクリプトがクエリ結果から特定の範囲のレコードのみを取得できるようにします。

次のように考えてください。

chooseWindow() はサーバーサイドスクリプトのページネーションです。

リストビューでは、ページネーションを使用してレコードをページごとに表示できます。chooseWindow() を使用すると、スクリプトはレコードをバッチごとに処理します。

例:

バッチメソッド呼び出しレコード処理済み
バッチ 1 chooseWindow(0, 500) 行 0-499
バッチ 2 chooseWindow(500, 1000) 行 500-999
バッチ 3 chooseWindow(1000, 1500) 行 1000-1499

最初の数字は包括的です。2 番目の数字は排他的です。したがって、chooseWindow(0, 500) は最大 500 件のレコードを返します。

このメンタルモデルにより、ServiceNow に結果セット全体を一度に処理するように頼むのではなく、一度に 1 「ページ」のレコードを要求するという方法を理解しやすくなります。

 

シナリオ例:契約レコードの移行

ある ServiceNow インスタンスから別のインスタンスに契約レコードを移行するとします。

安全な移行アプローチは、最初にソースデータをステージングテーブルにロードしてから、そのステージングデータをバッチで処理することです。

ステージングテーブルの例:

u_contract_migration_staging

フィールドの例:

  • u_source_sys_id
  • u_vendor
  • u_short_description
  • u_start_date
  • u_end_date
  • u_migration_status
  • u_target_sys_id
  • u_error_message

以下のスクリプトは、一度に 500 のステージングレコードを処理し、契約テーブルのレコードを挿入または更新します。

 

コード例:chooseWindow() を使用したバッチ移行

(function migrateContractsInBatches() {
    var BATCH_SIZE = 500;
    var batchNumber = 1;
    var totalProcessed = 0;

    while (true) {
        var staging = new GlideRecord('u_contract_migration_staging');
        staging.addQuery('u_migration_status', 'ready');

        // Use a stable order so batches are predictable.
        staging.orderBy('sys_created_on');
        staging.orderBy('sys_id');

        // Process the first 500 remaining ready records.
        staging.chooseWindow(0, BATCH_SIZE);

        // Avoid an unnecessary COUNT(*) query when total count is not needed.
        staging.setNoCount();

        staging.query();

        var processedThisBatch = 0;

        while (staging.next()) {
            processedThisBatch++;

            try {
                var contract = new GlideRecord('ast_contract');

                // Use a legacy/source ID field as the migration key.
                var found = contract.get(
                    'u_legacy_contract_sys_id',
                    staging.getValue('u_source_sys_id')
                );

                if (!found) {
                    contract.initialize();
                    contract.setValue(
                        'u_legacy_contract_sys_id',
                        staging.getValue('u_source_sys_id')
                    );
                }

                contract.setValue('vendor', staging.getValue('u_vendor'));
                contract.setValue('short_description', staging.getValue('u_short_description'));
                contract.setValue('starts', staging.getValue('u_start_date'));
                contract.setValue('ends', staging.getValue('u_end_date'));

                var targetSysId = found ? contract.update() : contract.insert();

                staging.setValue('u_target_sys_id', targetSysId);
                staging.setValue('u_migration_status', 'migrated');
                staging.setValue('u_error_message', '');
                staging.update();

            } catch (ex) {
                staging.setValue('u_migration_status', 'error');
                staging.setValue('u_error_message', ex.message || ex.toString());
                staging.update();
            }
        }

        totalProcessed += processedThisBatch;

        gs.info(
            'Contract migration batch ' + batchNumber +
            ' processed ' + processedThisBatch +
            ' records. Total processed: ' + totalProcessed
        );

        // If fewer than 500 records were processed, there are no more full batches.
        if (processedThisBatch < BATCH_SIZE) {
            break;
        }

        batchNumber++;
    }

    gs.info('Contract migration completed. Total processed: ' + totalProcessed);
})();

 

このパターンが機能する理由

次の行に気付くかもしれません。

staging.chooseWindow(0, BATCH_SIZE);

一見すると、スクリプトが次のものを使用すると思うかもしれません。

 
staging.chooseWindow(500, 1000);​

 

2 番目のバッチの場合。

ただし、この移行パターンでは、処理された各レコードは「ready」から「migrated」に変わります。つまり、クエリ結果はバッチごとに変更されます。最初の 500 件のレコードが「準備完了」の結果セットから削除された後に行 500 〜 999 行目に移動すると、誤ってレコードがスキップされる可能性があります。

したがって、ステータスベースの処理では、次の方が安全です。

staging.chooseWindow(0, BATCH_SIZE);​

 

平易な英語で:

常に、まだ準備ができている最初の 500 件のレコードを処理します。

これにより、スクリプトの再開が容易になり、レコードが見落とされるリスクが軽減されます。

 

代替パターン:安定した結果セットによるページング

スクリプトが安定した結果セットから読み込んでおり、クエリ基準を変更していない場合は、開始位置を使用してページ転送できます。

var batchSize = 500;
var start = 0;

while (true) {
    var gr = new GlideRecord('ast_contract');
    gr.addQuery('active', true);
    gr.orderBy('sys_created_on');
    gr.orderBy('sys_id');

    gr.chooseWindow(start, start + batchSize);
    gr.setNoCount();
    gr.query();

    var count = 0;

    while (gr.next()) {
        count++;

        // Example processing logic.
        gr.setValue('u_review_required', true);
        gr.update();
    }

    if (count < batchSize) {
        break;
    }

    start += batchSize;
}​

 

このアプローチは、基礎となる結果セットが処理中に安定したままである場合にのみ使用します。

 

アーキテクトのヒント:再起動可能にする

移行には、自分で制御できるため、移行ステータスフィールドのあるステージングテーブルを使用することをお勧めします。

レコードは、次のようなステータスを経ることができます。

  • 準備完了
  • 移行済み
  • スキップ
  • エラー

これにより、移行の監視、再起動、および監査が容易になります。ジョブが失敗した場合、停止した場所を推測する必要はありません。u_migration_status が「ready」のレコードを再実行するか、「error」とマークされたレコードを確認するだけです。

 

ベストプラクティス

- 本番環境で実行する前に、必ず準本番環境でテストしてください。クエリ、バッチサイズ、ビジネスルール、および予想されるデータ変更を検証します。

- バッチ処理を使用して、大規模で長時間実行される操作を回避します。実用的な開始点はバッチサイズ 500 ですが、適切なサイズはテーブルサイズ、ルール、統合、およびインスタンスの負荷によって異なります。

- 可能な場合はインデックス付きフィルターを使用します。バッチ処理は役立ちますが、コストのかかるクエリは修正されません。

- 安定したソート順を使用します。orderBy('sys_created_on') と orderBy('sys_id') を組み合わせることで、バッチ結果を予測可能に保つのに役立ちます。

- クエリフィルターで使用されるフィールドを更新するときは注意してください。スクリプトがクエリ対象と同じフィールドを変更する場合は、毎回残りのレコードの最初のバッチを処理します。

- 動作を完全に理解していない限り、setLimit() と chooseWindow() を併用しないでください。ServiceNow ドキュメントでは、setLimit() が chooseWindow() と予期しない結果を生み出す可能性のある形で作用する可能性があると記載されています。

合計行数が不要な場合は、setNoCount() を使用してください。

- 責任を持ってログを監視します。本番環境ですべてのレコードをログに記録するのではなく、バッチレベルのサマリーをログに記録します。

- ピーク時間帯は避けてください。適切に設計されたバッチスクリプトでもデータベース作業が実行され、プラットフォームロジックをトリガーできます。

- 移行後に検証します。カウントの比較、レコードのスポットチェック、エラーのレビュー、参照が正しくマッピングされていることを確認します。

サマリー: 重要なポイント

GlideRecord.chooseWindow() は、サーバーサイドスクリプトのページネーションに似ています。

レコードをバッチ処理すると、スケーラビリティが向上し、パフォーマンスのボトルネックを防止できます。

レコードをチャンクで処理することで、一括更新と移行をより予測しやすくなります。

移行作業では、ステージングテーブルとステータスフィールドを使用してプロセスを再起動可能にします。

chooseWindow() は query() の前に使用します。

合計行数が不要な場合は、setNoCount() を使用します。

本番データに対して実行する前に、必ず準本番環境でテストしてください。

 

まとめ

一括更新と移行は、ServiceNow プラットフォームの実際の所有権の一部です。リスクの高いスクリプトと本番環境に対応したスクリプトの違いは、多くの場合、その周りの設計にあります。

GlideRecord.chooseWindow() は、管理しやすいバッチでレコードを処理するためのクリーンな方法を提供します。安定した順序付け、ステージングテーブル、ステータス追跡、慎重なテストと組み合わせることで、大規模なデータ操作を制御し、拡張しやすくし、サポートしやすくすることができます。

 

つまり、ServiceNow にデータセット全体を一度に実行させないでください。明確に定義されたバッチを 1 つずつ与えます。

 

参照

ありがとうございます、ラトナカール
 

この記事は機械翻訳されております。最新は元となる記事をご覧ください: https://www.servicenow.com/community/developer-blog/efficient-data-migration-in-servicenow-with-glid...。

免責事項: 一部の日本語は、翻訳ソフトウェアを使用してお客様の便宜のために翻訳されています。正確な翻訳をご提供できるよう相当な努力を払っておりますが、いかなる自動翻訳も人間の翻訳者に代わすることはなく、そのようなことは意図されておりません。翻訳は「現状のまま」提供されています。他言語への翻訳の的確性、信頼性または正確性については、明示または黙示を問わず、いかなる保証も行われません。翻訳ソフトには限界があるため、一部のコンテンツが正確に翻訳されていない場合があります。これらの資料の公用言語は英語です。翻訳の際に生じる相違または不一致は、コンプライアンスまたは履行の目的に関しては拘束力を有さず、法的効力はないものとします。

This article was machine translated and replicated from the original source: https://www.servicenow.com/community/developer-blog/efficient-data-migration-in-servicenow-with-glid...。