読者です 読者をやめる 読者になる 読者になる

/* あとでなおす */

友達募集中

【Salesforce】レコードが5万件以上存在する2つのオブジェクトに対して一度に処理を行う

表題は厳密には50,001件以上ですが、記事にすると書きにくいので5万件にしています。
Salesforce で開発を行っている方ならご存知の通り SOQL で Salesforce 上の
オブジェクトに対して5万件以上のレコードを一度に取得しようとするとガバナ制限に
引っかかってしまいます。
5万件以上の場合は Salesforce 上では Apex バッチを使用します。

Apex バッチとは

一般的なシステムのバッチのようなもので、大量データを処理するプログラムだと思ってください。
Apex バッチではなくApex 一括処理とも呼ばれています。
「5万件を超えたらApex バッチを使用することをお勧めします。」的な記事を過去に
見かけたような気がしますが、5万件を超えたらApex バッチを使用する以外解決方法は
無いと考えた方が良いかと思います。*1
Apex バッチではガバナ制限が一部緩和され、5万件以上のレコードを一度に取得することが可能です。*2
書き方など細かい点に関しては trailhead 等を参照していただければと思います。

複数オブジェクトは扱えない

Apex バッチは5万件以上のレコードを取得することは可能ですが、複数のオブジェクトへ
同時にアクセスすることはできません。
参照関係または主従関係の場合は可能ですが、そうでもない限り1バッチにつきアクセス
できるのは1オブジェクトまでです。

では複数オブジェクトを扱うにはどうするか?
答えは「1バッチで1オブジェクトしか扱えない」のならばオブジェクトの数だけ
バッチを作成することで複数の5万件以上のオブジェクトに対して処理を行うことが可能です。

サンプル

取引先 (Account) と取引先責任者 (Contact) が5万件以上存在した場合のサンプルです。
取引先と取引先責任者は参照関係があるためバッチを分ける必要はありませんが
例としてバラバラで取得して処理しています。

簡単に説明しますと
 1.取引先を取得し、取得したレコードを取引先責任者のバッチに渡して呼び出し
 2.取引先責任者を取得し、前バッチで渡された取引先と何かしらの処理を実行
といった感じです。
1つ目のバッチが2つ目のバッチを実行しているので、下記の処理を行う場合は1つ目の
バッチさえ呼び出せば処理が完了します。

1.取引先を取得し、取得したレコードを取引先責任者のバッチに渡して呼び出し
public class DoAccount implements Database.Batchable<sObject>, Database.Stateful {
    
    // 取引先のクラス変数
    List<Account> accounts = new List<Account>();
    
    public Database.QueryLocator start(Database.BatchableContext bc) {
        // 取引先取得のSOQL
        return Database.getQueryLocator('SELECT XXX FROM Account');
    }
    
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // scope には start で記載した SOQL に該当する取引先レコードが
        // 代入されているので、それをクラス変数へ代入
        this.accounts = scope;
    }
    
    public void finish(Database.BatchableContext bc) {
        // 取引先のクラス変数を次の取引先責任者へのバッチへ渡して実行
        DoContact batch = new DoContact(this.accounts); 
        Database.executeBatch(batch);
    }
    
}
2.取引先責任者を取得し、前バッチで渡された取引先と何かしらの処理を実行
public class DoContact implements Database.Batchable<sObject>, Database.Stateful {
    
    // 取引先のクラス変数
    List<Account> accounts = new List<Account>();
    
    public DoContact(List<Account> accounts) {
        // 前の取引先のバッチにて取得したレコードを受け取ってクラス変数へ代入
        this.accounts = accounts
    }
    
    public Database.QueryLocator start(Database.BatchableContext bc) {
        // 取引先責任者取得のSOQL
        return Database.getQueryLocator('SELECT XXX FROM Contact');
    }
    
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        // 何かしらの処理
        // 前バッチと同様 scope には start で記載した SOQL に該当する取引先責任者のレコードが代入されている
        // scope と取引先のクラス変数を用いることでレコードの比較などを行うことが可能
        // ガバナ制限にひっかからなけらば insert, upsert 等の処理を行うことも可能
    }
    
    public void finish(Database.BatchableContext bc) {
        // 終了処理
        // 何かしら必要であれば記載
    }
    
}


取引先 → 取引先責任者の順で処理を実行していますが、上記の例だと逆順でも実行可能です。

まとめ

全然綺麗なやり方ではありませんが、一応一度に複数の5万件以上のオブジェクトから
レコードを取得して処理を行うことが可能です。
今私が行っている仕事では50万件以上のレコードが存在しますが、この方法で処理可能です。

それにしても5万件が大量レコードとは…
Salesfoce, もうちょっとなんとかならんのかね…

*1:分割して無理くりも不可ではないかと思いますが、運用開始後のレコードの増加に耐えられない可能性もあります。

*2:公式通りだと5,000万件まで取得可能です。その前にヒープサイズでエラーになりそうですが…