Translate

2011年5月11日水曜日

RESTサービス呼び出し、非同期、DOMを使ったApexコードサンプル

Force.comでは

SOAP APIのサーバとしてAPIを公開し他システムへ利用させる(コールイン)ことも、

外部のSOAP APIをApexクラスから呼び出すこと(コールアウト)もできる。



しかし

RESTサービスについてはコールアウト(外部RESTを呼び出す)のみ実装可能で、

残念ながらRESTサービスサーバにすることはできない。



おそらく、

ガバナ制約という足かせプラットフォーム環境で、

よくAjaxで活用されるRESTコールを受け付けるのは

どう考えても向いていないから

なのだろう。



以下のサンプルは、

Force.com開発者コースDEV541の演習8-3で使用したコードである。



このコードはRESTのコールアウトのサンプルでもあるが、

非同期Apexのサンプルでもある。



非同期Apexは、

Javaでいう別Theadのrun()のような非同期メソッドを含む

Apexのことである。



非同期Apexメソッドを呼び出すと、

その結果を待たずに次の行へ処理が進み、

非同期Apexメソッド上の処理は別途スレッドで実行される。

(スレッド、という名前なのかはわからないけど)



ここではApexトリガからRESTサービスを呼び出しているが、

処理時間がインターネットや相手側の負荷でよめないので

非同期メソッド上に処理を記述している。





トリガというアプリ屋泣かせの足かせに

非同期というさらに重石を載せるような

コードをかかなくてはならず、

結果紹介するコードはサンプルのくせに

実装にフラグを使った保守しにくいコードになっている。





さらにこのコードは受け取ったXMLをDOMを使って

要素内の情報を取り出しているサンプルにもなっている。

注意
XMLDom クラスは
Force.comにあらかじめ定義されているクラスではありません

[developerforce]XML Dom parser(英語)
のViewSourceボタンを押してソースを入手できます。
テストクラスも一緒に置いてあるので、
本番環境にもあげられます。






// Apex Code 演習8-3 非SOAP HTTPコールアウト(1)
//
//
trigger CandidateAddressValidation on Candidate__c (after insert, after update, before update) {

 // トリガ対象が1件のみの場合
 if (Trigger.new.size() == 1){ 
  // 新規追加の場合
  if (Trigger.isInsert) {
   // 新規の場合は非同期妥当性検査フラグの状態にかかわらず
   // 住所情報妥当性チェック(非同期処理)を行い結果を
   // レコード上に書き込み更新する非同期処理を実行する
   // なお、非同期実行は1回のApex呼び出しで最大10回まで
   AddressValidator.validateAddress(Trigger.new[0].id);

   // 上記メソッド内のロジックは非同期で処理されており
   // コールしたら結果を待たずに次の処理へ進む

  }

  // 更新の場合
  if (Trigger.isUpdate){
   // 更新前処理の場合
   if (Trigger.isBefore) {

    // 住所情報更新の場合
    if ((Trigger.new[0].Street_Address_1__c !=
     Trigger.old[0].Street_Address_1__c) ||
     (Trigger.new[0].Street_Address_2__c !=
     Trigger.old[0].Street_Address_2__c) ||
     (Trigger.new[0].City__c !=
     Trigger.old[0].City__c) ||
     (Trigger.new[0].State_Province__c !=
     Trigger.old[0].State_Province__c)) {

     // 非同期妥当性検査フラグを偽にする
     Trigger.new[0].AsyncValidationFlag__c
      = false;

     // 更新時妥当性検査処理は
     // after updateでのみ行うのでフラグを
     // 偽にすることでいちいち呼び出さない
     // ようにしている
    }
    // デバッグログ
    System.debug('\ncandidate=' +
     Trigger.new[0] + '\n');

   // 更新後処理の場合
   } else {
    // 非同期妥当性検査フラグが真の場合
    if (!Trigger.new[0].asyncvalidationflag__c){
     // 住所情報妥当性チェック
     // (非同期処理)を実行し結果を
     // 応募者レコード上に書き込む(更新)
     // 非同期実行は1回のApex呼び出しで
     // 最大10回まで
     AddressValidator.validateAddress(
      Trigger.new[0].id); 

     // 上記メソッド内のロジックは非同期で
     // 処理されておりコールしたら結果を待
     // たずに次の処理へ進む
    }
   }
  } // 更新の場合
 } // 1件のみの場合

 // 複数県の場合≒バッチ実行で複数件が来た場合は何もしない
 // おそらく、大量投入されたらコールアウトのガバナ制約を食いつぶすから
}




通常トリガクラスで

呼び出し元となるレコードの更新を行う場合

DML処理(insert/update/delete)はご法度である。



しかしREST結果取得に時間がかかりそうなので

非同期処理化してupdateステートメントを

使っている。





// Apex Code 演習8-3 非SOAP HTTPコールアウト(1)
//
// StrikeIronのサービスの1つである住所妥当性検査を使用して
// 応募者(Candidate)レコード上の住所情報の妥当性をチェックして
// 結果を同レコード上に格納する
//

public class AddressValidator {

 // StrikeIronサービスへログインするための情報
 static final StrikeIronInfo__c info =
  [select userid__c,
   password__c
   from strikeironinfo__c
   where active__c = true limit 1];

 // StrikeIronInfoはカスタムオブジェクトであり、
 // あらかじめStrikeIronサイトで登録したユーザID、
 // パスワード(平文)が1行だけ格納されている

 // 応募者の住所情報妥当性をチェックして結果を
 // 応募者レコード上に更新書き込みを行う
 //
 // ただしこの処理は米国住所のみ対応しており
 // 日本の住所妥当性は検査できない
 // 引数: id 応募者レコードID
 // 戻り値: なし(非同期メソッドはvoid必須)
 //
 // 非同期コールアウトメソッドとして指定しているので
 // このメソッドを呼び出した後は呼び出し元の次の処理へ
 // 進み、この中の処理は非同期に実行される
 @Future(callout=true) 
 public static void validateAddress(String id) {
  // カスタムオブジェクトError_Logがあらかじめ
  // 定義されておりレコードとして書き込むための
  // メッセージ用変数
  String msg = '';

  try {
   // 引数で渡されたレコードIDから
   // レコード情報を取得
   Candidate__c cand =
    [select
     // 非同期妥当性検査フラグ
     // StrikeIronを通したかどうかであり
     // 検査結果の合否ではない
     AsyncValidationFlag__c,
     // 住所1
     Street_Address_1__c,
     // 住所2
     Street_Address_2__c,
     // 市
     city__c,
     // 州
     state_Province__c,
     // 郵便番号
     zip_postal_code__c,
     // 国
     country__c,
     // 住所妥当性フラグ
     valid_us_address__c,
      // 行政区画
     county__c,
     // 議会地区(選挙区?)
     congress_district__c,
     // 緯度(99.9999)
     latitude__c,
     // 経度(999.9999)
     longitude__c,
     // 住所エラーメッセージ
     // StrikeIronサービスのエラーメッセージ
     // を格納するための項目
     address_error__c
     // 応募者オブジェクト
     from candidate__c
     // 引数で渡されたIDと一致する
     where id=:id];   
   // 応募者情報・StrikeIronログイン情報をメッセージに追加
   msg += 'candidate=' + cand + '\n\n';
   msg += 'info=' + info + '\n\n';


   // 送信情報(リクエスト)を生成
   HttpRequest req = new HttpRequest();

   // エンドポイント(接続するサービスのURL)文字列を
   // 作成する
   // GETパラメータもURLの後ろに付加させるので
   // 適宜EncodingUtilを使用してUTF-8エンコード化して
   // 文字列を完成させる

   // エンドポイント用変数
   String endpoint =
    'http://ws.strikeiron.com/' + 
    'StrikeIron/USAddressVerification4_0/' +
    'USAddressVerification/VerifyAddressUSA?';

   // GETパラメータ文字列:ユーザID
   String uId = 'LicenseInfo.RegisteredUser.UserID=' +
    // EncodingUtilクラスを使ってエンコード
    EncodingUtil.urlEncode(info.userid__c,'UTF-8');

   // GETパラメータ文字列:パスワード
   String pw = '&LicenseInfo.RegisteredUser.Password=' +
    info.password__c;

   // GETパラメータ文字列:住所情報1
   String street1 = '&VerifyAddressUSA.addressLine1=';
   street1 +=
    cand.Street_Address_1__c != null ?
    // 存在する場合はEncodingUtilでエンコード化
    EncodingUtil.urlEncode(
    cand.Street_Address_1__c,'UTF-8') :
    // nullの場合は空文字にする
    '';

   // GETパラメータ文字列:住所情報2
   String street2 = '&VerifyAddressUSA.addressLine2=';
   street2 +=
    cand.Street_Address_2__c != null ?
    // 存在する場合はEncodingUtilでエンコード化
    EncodingUtil.urlEncode(
    cand.Street_Address_2__c,'UTF-8') :
    // nullの場合は空文字にする
    '';

   // GETパラメータ文字列:市、州、郵便番号
   String citystatezip =
    '&VerifyAddressUSA.city_state_zip=';
   citystatezip +=
    cand.City__c != null ?
    // 存在する場合はEncodingUtilでエンコード化
    // して次のデータのための空白1個追加
    EncodingUtil.urlEncode(
    cand.City__c + ' ','UTF-8') :
    // nullの場合は空文字にする
    '';
   citystatezip +=
    cand.State_Province__c != null ?
     // 存在する場合はEncodingUtilでエンコード化
    // して次のデータのための空白1個追加
    EncodingUtil.urlEncode(
    cand.State_Province__c + ' ','UTF-8') :
    // nullの場合は空文字にする
    '';
   citystatezip +=
    cand.Zip_Postal_Code__c != null ?
     // 存在する場合はEncodingUtilでエンコード化
    // して次のデータのための空白1個追加
    EncodingUtil.urlEncode(
    cand.Zip_Postal_Code__c,'UTF-8') :
    // nullの場合は空文字にする
    '';

   // エンドポイント文字列作成
   endpoint += uId + pw + street1 + street2 +
    citystatezip +
    // 米国住所妥当性検査
    '&VerifyAddressUSA.casing=Proper';

   // メッセージにエンドポイント追加
   msg += 'endpoint=' + endpoint + '\n';

   // リクエストにエンドポイント登録
   req.setEndpoint(endpoint);
   // HTTP MethodをGETにする
   req.setMethod('GET');

   // HTTP通信インスタンス生成
   Http http = new Http();

   // リクエスト送信しレスポンスを受領
   HttpResponse res = http.send(req);

   // HTTP状態コードが200(通信成功)の場合
   if (res.getStatusCode() == 200) {

    // 応募者レコードを更新する

    // レスポンスから受信メッセージ本文の
    // DOMオブジェクトを生成
    XMLDom doc = new XMLDom(res.getBody());

    // メッセージ内のVerifyAddressUSAResultタグ部分
    // のElementインスタンスを取得
    XMLDom.Element result =
     doc.getElementByTagName(
      'VerifyAddressUSAResult');

    // 属性AddressStatus値がValid(妥当性検査合格)
    // の場合
    if (result.getValue('AddressStatus')=='Valid'){

     // 住所妥当性検査フラグを真にする
     cand.valid_us_address__c = true;
     // 国は米国固定なので決め打ち
     cand.Country__c = 'US';
     // 属性ZipPlus4値を郵便番号に格納
     cand.Zip_Postal_Code__c =
      result.getValue('ZipPlus4');
     // 属性County値を行政区画に格納
     cand.County__c =
      result.getValue('County');
     // 属性CongressDistrict値を議会地区に
     // 格納
     cand.Congress_District__c =
      result.getValue(
       'CongressDistrict');
     // 属性Latitude値を緯度に格納
     cand.Latitude__c =
      // 数値項目なのでDoubleクラスを
      // 使ってDouble化する
      Double.valueOf(
       result.getValue(
        'Latitude'));
     // 属性Longitude値を経度に格納
     cand.Longitude__c =
      Double.valueOf(
       result.getValue(
        'Longitude'));
     // 妥当性検査成功なので空文字にする
     cand.address_error__c = '';

    // Validでない=不合格の場合
    } else {

     // 住所情報は書き換えない
     // StrikeIronサービスで取得できる
     // 行政地区、議会地区、緯度、経度
     // は空文字やnull値に初期化しておく

     // 行政区画を空文字にする
     cand.County__c = '';
     // 議会地区を空文字にする
     cand.Congress_District__c = '';
     // 緯度をnullにする
     // 画面に項目は出るが値の部分には
     // なにも出ない
     cand.Latitude__c = null;
     // 経度をnullにする
     cand.Longitude__c = null;

     // StrikeIronエラーメッセージを格納
     cand.address_error__c =
      // エラー番号
      result.getValue(
      'AddressErrorNumber') + ' - ' +
      // エラーメッセージ
      result.getValue(
      'AddressErrorMessage');
     // 住所妥当性検査フラグを偽にする
     cand.valid_us_address__c = false;

    }

    // 結果がValidであるかどうかはともかく
    // StrikeIron妥当性検査を実施したので
    // 非同期妥当性検査フラグを真にする
    cand.asyncvalidationflag__c = true;

    // 更新後応募者レコード情報をメッセージに追加
    msg += 'async updated candidate=' +
     cand + '\n';

    // 応募者レコードを更新する
    update cand;

    // この更新処理についても
    // CandidateAddressValidationトリガを
    // 2度(before update/after update)実行
    // されてしまうが、
    // トリガ内で住所1、住所2、市、州の更新を
    // した場合のみafter updateで妥当性検査が
    // 実行される実装になっているため、
    // StrikeIronサービスは呼び出されない

    // だったらinsertとbefore updateのみの
    // トリガにすればいいのだけど、
    // Apexトリガには
    // 1. 元のレコードがロード、または初期化される
    // 2. 新しいレコードの値がロードされ、古い値を
    //    上書きされ、必須項目などの検証処理が実行
    //    される
    // 3. すべてのbeforeトリガが実行される
    // 4. カスタム検証ルールを含むシステム検証が
    //    行われる
    // 5. レコードはデータベースに保存されるが、
    //    コミットはされない
    // 6. すべてのafterトリガが実行される
    // 7. 割り当てルールが実行される
    // 8. 自動返信ルールが実行される
    // 9. ワークフロールールが実行される
    // 10. ワークフローフィールドが更新されたら、
    //     レコードがリロードされる
    // 11. レコードの更新後、beforeトリガおよび
    //     afterトリガがもう一度起動される
    // 12. エスカレーションルールが実行される
    // 13. すべてのDML操作がデータベースにコミット
    //     される
    // 14. 電子メールの送信など、コミット後の
    //     ロジックが実行される
    // ※引用元:
    //     Force.com開発者コードテキストDEV541 p174
    // この順番に処理されていく。
    // だから、宣言的開発で設定された必須
    // チェックや入力規則などを先に実行させたい
    // がために面倒でもafterで処理しなくてはならな
    // かったから、と読んだ・

   // HTTP状態コードが200ではない(通信失敗の)場合
   } else {
    // デバッグログにレスポンス情報を書き込む
    System.debug('Callout failed: ' + res);
    // Error_Logへメッセージやレスポンスの主要情報
    // を書き込む
    msg += 'STATUS:'+res.getStatus();
    msg += '\nSTATUS_CODE:'+res.getStatusCode();
    msg += '\nBODY: '+res.getBody();
    Error_Log__c log = new Error_Log__c();
    log.trace__c = msg;
    insert log;
   } // HTTP状態コードのif文

  // 例外発生時処理
  } catch(Exception e) {
   // デバッグログに例外情報書き込む
   System.debug('ERROR: '+ e);
   // Error_Logへ例外情報を書き込む
   Error_Log__c log = new Error_Log__c();
   log.trace__c = msg + '\n' + e.getCause() + '\n' +
    e.getMessage();
   insert log;
  } // try-catch句:終わり
 } // validateAddress():終わり
}




非同期処理であり、

普通のプログラミング言語でもデバッグが大変なため

上記のようなError_Logカスタムオブジェクトを別途用意して

エラーの詳細情報をくみ取れるようにしておくと便利である。



が、

ログ情報がレコードとしてがんがんたまっていくので

容量上限に気を付けないとシステムが止まってしまう。



おまけに自動削除Apexトリガなどは埋め込みにくいので

人間系管理が面倒になることも注意だ。











ぬふー

0 件のコメント:

既存アプリケーションをK8s上でコンテナ化して動かす場合の設計注意事項メモ

既存アプリをK8sなどのコンテナにして動かすには、どこを注意すればいいか..ちょっと調べたときの注意事項をメモにした。   1. The Twelve Factors (日本語訳からの転記) コードベース   バージョン管理されている1つのコードベースと複数のデプロイ 依存関係 ...