こんにちは、なかやまです。
このブログはSalesforce Platform Advent Calendar 2017の15日目の投稿になります。
最近同僚から”トリガから呼び出したfutureメソッドがエラー(UNABLE_TO_LOCK_ROW)になるので、Queueableを使ってみたところエラーが解消されたのだが、果たしてこの使い方で良いのか!?”という相談を受けました。私自身Queueableを利用したことがなかったので、整理してみたいと思います。
※futureやQueueableは非同期処理のことです。
ちなみにSalesforceにはTrailheadというオンラインで利用できる学習教材があります。Trailheadをご存知の方でしたら、こちらのモジュールにチャレンジするとより理解できると思います。
参考) リファレンス:非同期Apex
はじめに
Salesforceで開発するには出来るだけ処理に負荷がかからないコードを書く必要があります。どの程度までOKなのかは、Webサイトにまとまっているので開発者は常に意識しましょう。ちなみに処理中にガバナを越えるとエラーが発生して処理が中断してしまいます。怖いですね。負荷をかけないために、非同期で実行できる様々な方法があります。1つずつみていきましょう。
参考)Apex ガバナ制限
Salesforceで利用できる非同期機能
現時点では、以下の4つの機能が提供されています。
- @futureメソッド
- Queuable (Winter'15〜)
- Apex Batch
- Scheduled
@futureメソッド
とある処理を非同期で呼ぶ時に利用します。今までSalesforce開発をしてきましたが、利用機会はそこまで多くありません。標準のトランザクションでどうしても処理ができない場合に利用します。
global class FutureMethodRecordProcessing
{
// 1:メソッドにアノテーション@futureをつける。戻り値はvoid
@future
// 2:引数はプリミティブな変数のみ
public static void processRecords(List<ID> recordIds)
{
// Get those records based on the IDs
List<Account> accts = [SELECT Name FROM Account WHERE Id IN :recordIds];
// Process records
}
}
<ケース1:DML操作で同時に処理できないオブジェクトの更新>
SalesforceではDML処理で同一に処理できないオブジェクトがあります。例えば、取引先責任者(Contact)とユーザ(User)を同時に作成しようとするとエラーが発生します。この場合に取引先責任者(Contact)は現状のトランザクションで処理をし、成功したら@futureメソッド内で処理するようにします。
参考)DML 操作で同時に使用できない sObject
※非同期処理にしてしまうと、呼ばれた先でエラーになった場合のハンドリングが面倒になるので、その辺りもしっかり設計した上で実装しましょうね。
<ケース2:トリガが呼ばれた場合にコールアウト>
オブジェクトのトリガをキーにコールアウトをしたいケースの場合もよくあります。Salesforceの中ではトリガ処理内でコールアウトをするとエラーが発生します。トリガ内で外部コールアウトをするためには、コールアウトOKな非同期メソッドを呼び出す必要があります。
@future(callout=true)
ちなみに、コールアウトの頻度が少ない場合(運用上そこまで大量レコードの更新がない、〇〇が変更された場合のみ)は、アウトバウンドメッセージという仕組みもあります。こちらは UI上で設定ができるようになってます、トリガと違って送信する値の加工ができないのですが、データ受け取り手側で頑張れるのであれば、こちらの方法も使えますよ。
↓ワークフローを使用したコールアウトのあたりを参考に
参考)リファレンス:アウトバウンドメッセージについて
<ケース3:ガバナの壁を超えてしまった>
いくつかの項目は非同期として呼ぶことで、ガバナが緩和されます。
- 発行される SOQL クエリの合計数 100→200
- ヒープの合計サイズ 6 MB→12 MB
- Salesforce サーバの最大 CPU 時間 10,000 ミリ秒→60,000 ミリ秒
ガバナが緩和されるのは嬉しいのですが、保守を続けていくうちにまたガバナの壁を越えることもあります。。そこで既存のfutureメソッド処理を分割して、futureメソッドを呼び続けるとうまくいく気がしますが、、動かしてみるとエラーになってしまいます。futureメソッドからfutureメソッドを呼ぶことはできないのです。もどかしい気がしますが、この辺りが次に紹介するQueuableで解消されます。
<futureの制限>
・1トランザクションで呼べるのは50個まで(トリガ内で1レコードにつき1コールするとガバナエラーになるよ。まとめて1コールで済むように実装しましょう)
・futureからfutureの呼び出しNG
・緩和されるガバナもあるよ
・メソッドの引数はプリミティブな変数のみ(sObjectを渡すことはできないそうです。なんでも非同期処理のため、処理をする際にオブジェクトの情報が変わってしまうことを想定しているそうです。へー。)
・複数のfutureメソッドが同時に実行される可能性がある(レコードのロックに注意)
・戻り値はvoid
・トリガでは正常終了しないとfutureメソッドが呼ばれない
参考) help:トリガ内から@futureメソッドを実行した際の動作について
Queuable
とある非同期処理を呼ぶことができます。ジョブIDが返却されるので画面上で監視もできます。さらにQueuableからQueuableを呼ぶこともできます。futureよりも重たい処理や制限のある処理を作りたい場合に使えます。
public class AsyncExecutionExample implements Queueable {
// 1:↑Queueableインターフェース
// 2:void型。
public void execute(QueueableContext context) {
Account a = new Account(Name='Acme',Phone='(415) 555-1212');
insert a;
}
}
// 呼び出し方(引数がある場合は、クラス生成時にパラメータとして渡したりする)
ID jobID = System.enqueueJob(new AsyncExecutionExample());
<ケース1:非同期処理が終わったら、別の非同期処理を呼ぶ>
futureメソッドでは実現できなかった、非同期処理から非同期処理を呼ぶことができます。トランザクションは分かれるものの、処理が重いものや外部コールアウトをするため処理時間がかかりそうなもの、を処理分割しておき順に起動することができます。
<ケース2:sObjectの情報をQueuableに渡して処理する>
futureメソッドの場合はプリミティブな変数のみでしたが、Queuableでは非プリミティブ変数の受け渡しが可能です。計算途中のオブジェクトを次の非同期処理に引き渡すことこともできそうですね。
<Queuableの制限>
・1トランザクションで呼べるのは50個まで(futureと同じ)
・Queuableインターフェースを実装する
参考)リファレンス:Queueable Apex
Apex Batch
みんな大好きApex Batchです。Salesforceの中で大量データの処理をする場合は、間違いなく利用します。対象となるオブジェクトのクエリを定義し、そのクエリが200行ずつexecuteのメソッドとして呼ばれ、最後はfinishメソッドが呼ばれて処理が終わります。
バッチは画面からボタンを押したタイミングで起動したり、夜間バッチとしてスケジュールクラスから起動したりします。
global class SearchAndReplace implements Database.Batchable<sObject>{
// 1:バッチ用インターフェース。
global final String Query;
global final String Entity;
global final String Field;
global final String Value;
global SearchAndReplace(String q, String e, String f, String v){
Query=q; Entity=e; Field=f;Value=v;
}
// 2:バッチ開始時に呼ばれるメソッド(変数整理したり)
global Database.QueryLocator start(Database.BatchableContext BC){
return Database.getQueryLocator(query);
}
// 3:バッチサイズ単位でコールされる。startメソッドの戻り値がに引数にきます
global void execute(Database.BatchableContext BC, List<sObject> scope){
for(sobject s : scope){
s.put(Field,Value);
}
update scope;
}
// 4:バッチ終了時にコールされる。ログを書き込んだりメール送信したりする
global void finish(Database.BatchableContext BC){
}
}
// バッチ登録。第2引数にはバッチサイズを指定できます
ID batchprocessid = Database.executeBatch(SearchAndReplace,200);
<ケース1:大量データのアップデート>
1回のトランザクションではガバナ制限になるような件数や重い処理をApex Batchとして分割して処理できます。バッチ処理によっては処理終了時に完了メールを送信したり、エラー情報をカスタムオブジェクトに書き込んだりします。
<Apex Batchの制限>
・対象となるのは500万レコード
・start、execute、finishで10回ずつコールアウトできる
・バッチサイズ(デフォルト200)は
参考) help: Batch Apex (Apex の一括処理) の制限
参考) Apex Batch、Apex Batchの一括処理の使用
Scheduled
特定の時間に起動する処理を書くことできます。だいたいがApex Batchを呼び出す処理に使われます。バッチ処理が必要ない場合は、スケジュールクラスに処理ロジックを書くこともあります。
スケジュールの登録は画面上からもできますが、分単位の設定ができません。細かく設定したい場合は、開発者コンソールよりスケジュール登録処理を呼ぶことで分単位の指定もできるようになります。ただし、登録されたスケジュールクラスが時間通りに実行されるとは限らず、負荷のかかった状態だと少し遅れて実行されることがありました。
global class scheduledMerge implements Schedulable {
// 1:スケジュールインターフェースを実装↑
global void execute(SchedulableContext SC) {
mergeNumbers M = new mergeNumbers();
}
}
参考) Apex Scheduled
まとめ
今日は色々な非同期処理を調べてみました。Queuableについては、非同期処理から非同期処理を呼ぶケースの場合に使ってみようと思います。非同期処理を作る場合は例外が発生したケースも想定してリカバリできるようにしておきましょうね。
future |
ちょっとした非同期用 |
Quauable |
非同期処理から非同期処理を呼ぶケース |
Apex Batch |
大量レコードの処理 |
Schedule |
スケジュールした日時で起動させる |
※同僚から質問を受けた内容だと、Queuableでロックエラーが発生しないのは偶然な気がしています。。手元に環境がないのでなんともですが。
今回は紹介しませんでしたが、プラットフォームイベントというイベント駆動なる仕組みもあるので気になる方はこちらも見ていただけると良いかも。
それでは、みなさま素敵なSalesforceライフを!