Salesforce の Apex コードでは、外部のWebサービスを利用することができます。つまり HTTP リクエストを送信することができます。 これを Salesforce ではコールアウトと呼んでいます。

通常の Apex コードではコールアウトを行うことはできず、callout=true を付与した future メソッドなどの、個別のスレッドを使用する仕組みを用いなければコールアウトを行うことはできません。

future メソッドを実装すること自体は、単に @future アノテーションを使用すればよいだけなので、特段難しいことはありません。
ただし、以下のような点には注意が必要です。

  • future メソッドの引数にはプリミティブ型かプリミティブ型のコレクションしか指定できない (つまりオブジェクトをそのまま渡すことはできないし、独自のクラスのインスタンスを渡すこともできない)
  • future メソッドは別のトランザクションになる – static な変数を共有することはできない
  • future メソッドは非同期である – いつ実行されるのかは不定である (ただし単体テストでは Test.stopTest の後には完了している)
  • future メソッドから future メソッドを呼ぶことはできない
  • future メソッドは、ガバナ制限により1つのトランザクションでは50回までしかコールできない

上記ののような future メソッドに関する注意点には気をつけていたのですが、先日初めてのエラーに遭遇しました。
こんな処理がありました。

トリガ発動 → コールアウト実行 → 結果をDMLで書き込み

コールアウトを行う箇所は future メソッドになっていて、コールアウト実行後、その結果を UPDATE するというよくあるような処理です。

当初は問題なく動作していました。

その後、複数レコードに未対応であることが発覚し、以下のように修正しました。

トリガ発動 → (コールアウト実行 → 結果をDMLで書き込み)をレコード数分繰り返し

ところがここで、標題の「System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out」が発生してしまいました。

この原因はメッセージの通りで、未コミットのデータベース変更がある状態ではコールアウトは実行できないというものです。

コールアウト実行 → 結果をDMLで書き込み(ここまではOK) → コールアウト実行(ここでエラー) というわけです。

なんのための制限かわかりませんが、(コールアウト実行 → 結果をリストやマップなどに保持)を繰り返す → レコード更新 とする必要があります。

Salesforce はこの手の落とし穴のようなところが少なくありませんので、気をつけたいところです。