本記事は、「何故こうなるのか?」を個人的に学習・解釈したまとめになります。
InvocableMethodについて
InvocableMethodはApexのアノテーションの一種で、Lightningフローで利用したいApexメソッドに付加するものです。
これを使えば、Apexでメソッドを呼び出すのと同じようにフローからApexメソッドを呼び出すことができます。
※詳細は公式ドキュメント参照
「何故こうなるのか?」と思った点
公式ドキュメントには下記のような引数と戻値についての記載があります。
- プリミティブデータ型のリスト、またはプリミティブデータ型のリストのリスト – 汎用 Object 型はサポートされていません。
- sObject 型のリスト、または sObject 型のリストのリスト。
- 汎用 sObject 型のリスト (List<sObject>)、または汎用 sObject 型のリストのリスト (List<List<sObject>>)。
- サポートされている型の変数、またはユーザ定義の Apex 型を含み、InvocableVariable アノテーションが付加されているユーザ定義型のリスト。各自のデータ型を実装するカスタムのグローバルまたは公開 Apex クラスを作成し、そのクラスに呼び出し可能な変数アノテーションが付加されているメンバー変数が少なくとも 1 つ含まれていることを確認します。
一括処理を正しく実装するには、入力と出力がサイズと順序の両方の点で一致する必要があります。たとえば、i 番目の出力エントリは i 番目の入力エントリに一致する必要があります。これは、アクションが一括処理で実行されるときにデータの正確性を維持するために必要です。たとえば、Apex アクションがレコードトリガフローで使用されるときに必要となります。
つまり、InvocableMethodの引数と戻値は「List<~>
」の形でなければならず、「引数リスト[0]
の結果は戻値リスト[0]
、引数リスト[1]
の結果は戻値リスト[1]
、...引数リスト[n]
の結果は戻値リスト[n]
」というような「n:n
」の関係になります。しかし、Flow BuilderでApexアクションを設定する際には、引数と戻値は共にリストの中身(List<~>
の~
の部分)を使います。
例えば、Apexで「List<String>
」を引数としている場合、Flow Builderでは「String
」を渡すことになります。逆に、Flow Builderから「List<String>
」をApexに渡す場合、Apexでは「List<List<String>>
」が引数になります。
この点が個人的に「何故こうなるのか?」と思った点になります。
Apexトリガからヒントを得る
Apexトリガを初めて書いた日に、Trigger.new
やTrigger.old
などのトリガ変数がレコードのリストやマップという点に難儀したことを思い出してみました。
Apexトリガは、トリガの中の処理は単一レコードではなく複数レコード(最大200レコード)に対して処理を記述します。これは、リストビューなどでレコードを一括処理した際に、個々のレコードがトリガするのではなく一定単位のレコードの集合(バッチ)がトリガするためです。
この点に着目して、Lightningフローでも同じような処理の方法だった場合に引数と戻値の違和感を説明できるのではないかと考えました。
LightningフローのBulkification(バルク化)
Lightningフローの一括処理について調査を進めていくと、LightningフローにはBulkification(バルク化)という振る舞いがあることがわかりました。
この振る舞いは個々の処理を記述しているLightningフローの処理が特定の処理の際にまとめて処理を行うようにできています。
これに関しては、こちらの記事が分かりやすいです。
InvocableMethodのApexメソッドがBulkificationの対象だった場合、Lightningフローで処理されている個々の値がまとめられてApexではリストを引数として受け取り、Apexで処理された戻値のリストが展開されて個々のLightningフローの処理に戻ることになります。
簡単な例で確認
下記のようなLightningフローから与えられる取引先レコードのリストの数を数値で返すInvocableMethodのApexを作成します。
public class checkBulkification {
@InvocableMethod( label='check' category='Apex' )
public static List<Decimal> check( List<Account> accounts ){
List<Decimal> dList = new List<Decimal>();
Decimal d = accounts.size();
for( Account a : accounts ){
dList.add( d );
}
return dList;
}
}
簡単なLightningフローとして、下記を作成します。
- 取引先のレコードトリガフロー
- 上記のApexアクションに
$Record
を渡す - 上記のApexアクションの戻値で特定の数値項目を更新する
フローを有効化し、下記のようなリストビューで取引先を一括更新します。
結果としては下記のような状態になります。
※13個のレコードを更新したため、InvocableMethodのApexへ13個のレコードが与えられ、数値として13を返しています。
この振る舞いを活かせる場面
この振る舞いで特筆すべき点は、「個々のLightningフローの処理の間の値のやり取りができる」ことではないかと思います。
例えば、標準のレコードトリガフローでは一括処理時の他のレコードの状態を取れないことから対応できない一括処理時のカスタムの重複判定で使えます。
また、よく紹介されている参照関係の積み上げ集計のフロー(子側から親側の値を更新するタイプ)の子側のレコードの一括作成に関する問題にも対応できます。
まとめ
InvocableMethodのApexはBulkificationの対象となるため、個々のLightningフローの処理がまとめられた形で実行されます。
それにより、
InvocableMethodの引数と戻値は「
List<~>
」の形でなければならず、「引数リスト[0]
の結果は戻値リスト[0]
、引数リスト[1]
の結果は戻値リスト[1]
、...引数リスト[n]
の結果は戻値リスト[n]
」というような「n:n
」の関係になり、Flow BuilderでApexアクションを設定する際には、引数と戻値は共にリストの中身(List<~>
の~
の部分)を使う
というようなズレが生じます。