Memos About Salesforce

Salesforceにハマってたこと!

apex 非同期 メソッド futureアノテーション @futrue

f:id:jude2016:20190920155743j:plain

こんにちは、管理人の@Salesforce.Zです。

Salesforceにはfutureアノテーションがあります。メソッドに@futrueをつけて、非同期メソッドになります。 そして、非同期にすることによってメリットもあります。future メソッドは、バッググラウンドで非同期で実行されます。外部 Web サービスへのコールアウト、独自のスレッドを独自の時間に実行する処理など、長時間にわたる処理を実行する場合に future メソッドをコールできます。

読んだら得ること

★ @futureアノテーションメソッドの書き方
★ 非同期メソッドの使用ケース
★ 非同期の使用例:Webサービスのコールアウトとテストクラス

目次

@futureアノテーションメソッドの書き方

この方法によって、長時間にわたる処理の完了を待たずにコードを実行できます。future メソッドを使用する利点は、SOQL クエリの制限やヒープサイズ制限など、一部のガバナ制限値が高くなる点にあります。

基本の記載方法

future メソッドを定義するには、単に次のように future アノテーションを使用してアノテーションを付加するだけ

global class FutureClass{
    @future
    public static void myFutureMethod(){   
         // Perform some operations
    }
}

非同期メソッド:よく使用するパターン

  • 長時間を要するメソッドがあり、Apex トランザクションの遅延を防止する必要がある場合

  • 外部 Web サービスへのコールアウトを実行する場合

  • DML 操作を分離して混合保存 DML エラーを回避する場合

引数を受け取る応用記載方法

記載注意点

  • Staticのメソッドである必要がある(静的メソッド)

  • 引数はプリミティブデータ型である つまりプリミティブ、配列、List、Mapのみ

  • 戻り値はVoidのみ

  • sObject またはオブジェクトを引数とすることがダメ

  • 呼び出した順に処理されるかは保証されない

  • 1 つの Apex トランザクションで、HTTP 要求または API コールに対するコールアウトを最大 100 回実行できます

  • コールアウト(外部接続)するときはcallout=trueを指定する

  • @future(callout=true)メソッドから@futureメソッドを呼出することはできない
  • テストメソッドでcalloutを実行するとスキップしてしまうので、フラグまたはコールアウト用クラスを作ってテスト処理はモックで実行する
  • コールアウトする前後にDML処理(insert, update, upsert, delete)を行うとCalloutExceptionが発生する >>> System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling

コール順番で検証した結果

# 順番 結果
1 DML ⇒ callout
2 callout ⇒ DML
3 DML ⇒ callout ⇒ DML
4 callout ⇒ DML ⇒ callout

future メソッドに sObject を引数として渡せない理由は、メソッドをコールしてからそのメソッドを実行するまでの間に sObject が変更されてしまう可能性があるためです。この場合、future メソッドが以前の sObject 値を取得して新しい値を上書きしてしまう可能性があります。データベースにすでに存在する sObject を使用するには、代わりに sObject ID (または ID のコレクション) を渡し、ID を使用して最新のレコードに対してクエリを実行します。次の例では、ID のリストを使用してこれを実行する方法を示します。

応用例A

global class FutureMethodRecordProcessing{
    @future
    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
    }
}

応用例B

外部サービスへのコールアウトを実行するには、コールアウトが許可されることを示す追加パラメータ (callout=true) で対応する

global class FutureMethodExample{
    @future(callout=true)
    public static void getStockQuotes(String acctName){   
         // Perform a callout to an external service
    }

}

Webサービスコールアウト

例えば、どこで、Webサービスのコールアウトをしたい場合があります。大体下記のような場合かと考えられる。

Webサービスのコールアウト例

    /**
     * Webサービスのコールアウト:トリガやクラスの中の非同期メソッドからWebサービスのコールアウト
     *
     * @param idList 処理したいsObjectのIDリスト
     */
    private static void callWebService(Set<Id> idList){
        YourApexClass.httpCalloutStaticClass(idList);
    }
public with sharing class YourApexClass {
  
        public static void httpCalloutStaticClass(Set<Id> idList) {
            List<sObject> dataList = YourDao.getRecsByIds(idList);
            if(dataList != null && !dataList.isEmpty()){
                // ....処理
            }
            callOutWebService(your want to post message);
        }

        /**
        * Webサービスのコールアウト
        *
        * @param jsonMsg
        */
  @future(callout=true)
        private static Integer callOutWebService(String jsonMsg){
            try{
                Http http = new Http();
                HttpRequest req = new HttpRequest();
                //your end point
                req.setEndpoint('https://yourendpoint.com');
                req.setMethod('POST');
                req.setHeader('Content-Type', 'application/json');
                req.setBody(jsonMsg);
                HttpResponse res = http.send(req);
                System.debug('Post result:' + res.getStatus());
                return res.getStatusCode();
            }catch(System.CalloutException e) {
                System.debug(LoggingLevel.INFO, 'ERROR:' + e.getMessage());
                return 404;
            }
        }
    }
Webサービスのコールアウトのテストクラス

WebサービスのコールアウトをテストするにはHttpCalloutMock インターフェースを実装して respond メソッドで送信される応答を指定する必要があります。

そして、この応答の指定は疑似です。本当のコールアウトではありません。

擬似応答の値を指定したら、テストメソッドで Test.setMock をコールし、この擬似応答を送信するように Apex ランタイムに指示できます。次のように、第 1 引数では HttpCalloutMock.class を渡し、第 2 引数では HttpCalloutMock のインターフェース実装の新しいインスタンスを渡します。

下記はHttpCalloutMock インターフェースを実装したYourHttpCalloutMockImplクラスの使用例

Test.setMock(HttpCalloutMock.class, new YourHttpCalloutMockImpl());
/**下記のようなコールアウトクラスがあるとします*/
public class CalloutClass {
 @future(callout=true)
    public static HttpResponse getInfoFromExternalService() {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('http://example.com/example/test');
        req.setMethod('GET');
        Http h = new Http();
        HttpResponse res = h.send(req);
        return res;
    }
}
/**上記のコールアウトクラス()のテストクラス*/
@isTest
private class CalloutClassTest {
     @isTest static void testCallout() {
        // Set mock callout class 
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
        
        // Call method to test.
        // This causes a fake response to be sent
        // from the class that implements HttpCalloutMock. 
        HttpResponse res = CalloutClass.getInfoFromExternalService();
        
        // Verify response received contains fake values
        String contentType = res.getHeader('Content-Type');
        System.assert(contentType == 'application/json');
        String actualValue = res.getBody();
        String expectedValue = '{"example":"test"}';
        System.assertEquals(actualValue, expectedValue);
        System.assertEquals(200, res.getStatusCode());
    }
}
/**CalloutClassTest で使う疑似コールアウトクラスの定義*/
@isTest
global class MockHttpResponseGenerator implements HttpCalloutMock {
    // Implement this interface method
    global HTTPResponse respond(HTTPRequest req) {
        // Optionally, only send a mock response for a specific endpoint
        // and method.
        System.assertEquals('http://example.com/example/test', req.getEndpoint());
        System.assertEquals('GET', req.getMethod());
        
        // Create a fake response
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setBody('{"example":"test"}');//なんでもよい
        res.setStatusCode(200);
        return res;
    }
}

最後に

基本、Webサービスのコールアウトなら、非同期メソッドを使用すると覚えたら、いいじゃないかなと思います。

リファレンス