yonet77的な雑記帳

日々思いついたネタなどを書き留めておきます

Force.comでのJSON Support -ApexコントローラのRemoteActionを添えて-

Force.comでのJSON Support - yonet77 的な雑記帳 の続き的な感じで。
せっかくForce.comでJSONが扱えるようになったので、外部システムと連携させてみたい...と思うのが人の性でしょうか。
そんな直球勝負(?)は避けて、今回は小賢しい話を紹介してみたいと思います。

ApexコントローラのRemoteAction

Spring '11 からJavascriptを介してApexコントローラのメソッドを呼び出せるようになりました。
元々、apex:actionFunctionコンポーネントを使うと、Apexコントローラのメソッドを呼び出すことはできましたが、Visualforce Page内のどこかのDOMを再描画させるもので、コールバックは用意されてませんでした。
apex:inputHiddenのvalueを介して、apex:actionFunction/apex:actionStatusコンポーネントを組み合わせながら値をやり取りすることが多かったかもしれません。
(まぁ、自分もそうしてました。結構面倒だった記憶が....)

そこで、このRemoteActionを使ってみる、というワケです。
※なお、RemoteActionについての説明は コチラ を参照下さい。

  • RemoteActionを使って、SObjectのデータを取得する

※実際には、JSONを使ってませんがついでに。。
例えば、こんな感じにRemoteAction使うとデータを簡単に取得できます。
■Apexコントローラ

global with sharing class SampleRemoteAction {
  @RemoteAction
  global static List<sObject> queryData(String query){
    return Database.query(query);
  }
}

■Visualforce Page

var soqlQuery = 'Select Name,AccountNumber From Account';
SampleRemoteAction.queryData(soqlQuery, function(res, event){
  if(event.status){
    // resに取得したデータが格納されているので、データを好きに処理..
    var rowData = res;
  } else {
    // Error
  }
},{escape:true});
  • RemoteActionを使って、JSONデータを受け取ってApex側で処理する

Apexコントローラ側では、受け取ったJSON文字列をdeserializeして、Apexオブジェクトに変換します。
変換したApexオブジェクトに対して、適当にデータ処理すれば良いでしょう。
■Apexコントローラ

global with sharing class SampleRemoteAction {
  @RemoteAction
  global static String processData(String paramJSON){
    List<Sample> objs = (List<Sample>)JSON.deserialize(paramJSON, List<Sample>.class);
    for(Sample wk : objs){
      // 適当なデータ処理(DML発行)など
    }
  }
}
public class Sample{
  public String key;
  public String name;
  public Integer index;
}

■Visualforce Page
※ここでは、jQueryを使って、JSON文字列に変換してます。
Visualforce Page側は、例えば処理したいデータを格納したオブジェクトをJSON文字列に変換して、上記コントローラのRemoteActionに渡せば問題ないと思います。

var updates = [];
updates.push({'key':'aaa', 'name':'bbb', 'index':100});
SampleRemoteAction.processData(jQuery.toJSON(updates), function(res, event){
  if(event.status){
    // Success
    alert('Success!');
  } else {
    // Error
    alert('Error: ' + event.message);
  }
},{escape:true});

Visualforce PageとApexコントローラの間でJSON形式でデータをやり取りする基本形は上記の感じかと思います。

でもちょっと面倒臭くない?

Visualforce PageからApexコントローラにクエリを投げてデータを取得する方に関しては、Visualforce Page側で正しくクエリさえ作れれば、標準オブジェクト/カスタムオブジェクトでも特に問題なくデータが取得できるかと思います。
一方、後者の方では、事前にdeserializeする型(上記例では、Sampleクラス)を用意しておかなければならないという面倒臭さが匂います。
単に型だけを定義した何てことのないクラスがたくさん生成される(コピペの予感)...なんて状況は気に入らないですよね。
スピードワゴンに言わせれば、

こいつはくせえッー! コピペコード大量生成のにおいがプンプンするぜッ―――ッ!!
こんな冗長なコードには出会ったことがねえほどなァ――――ッ
環境で冗長になっただと? ちがうねッ!!

と、ロウソク台を蹴りつけられる感じかもしれません。 (/++)/
特に、カスタムオブジェクトをCreate/Updateだけしたい場合なんて、オブジェクト定義用のクラスを都度用意するのも面倒です。

ここでは、単純にカスタムオブジェクトをCreate/Updateしたいだけなら、もう少し簡単に済ませたいものです。
ということで、こんな感じでいけるんじゃないか?という手を考えてみました。

■Apexコントローラ

global with sharing class SampleRemoteAction {
  @RemoteAction
  global static String processData(String objName, String paramJSON){
    try{
      Type objType = Type.forName('List<' + ObjName + '>');

      List<SObject> sObjs = (List<SObject>)JSON.deserialize(paramJSON, objType);
      upsert sObjs;

      return '';
    }catch(Exception ex){
      return ex.getMessage();
    }
  }
}

■Visualforce Page

var updates = [];
updates.push({'Name':'aaa', 'AccountNumber':'bbb', 'AnnualRevenue':100});
SampleRemoteAction.processData('Account', jQuery.toJSON(updates), function(res, event){
  // 第一引数にカスタムオブジェクト名、第二引数にJSON文字列を指定します
  if(event.status){
    // Success
    alert('Success!');
  } else {
    // Error
    alert('Error: ' + event.message);
  }
},{escape:true});

Apexコントローラに、カスタムオブジェクト名を引数として渡して、動的にdeserializeする型を指定する、という感じです。
このやり方であれば、Visualforce Page上で扱うカスタムオブジェクトが何であれ、カスタムオブジェクト名とそのデータ項目が分かっていれば、RemoteAction経由でApexコントローラに渡して処理できる...
というのを狙ってます。

もちろん、REST APIを使えばもっと楽になりますが、APIコール数の消費を回避できるので、APIコール数を気にせずに処理できる、という感じです。
Apex:actionFunction使っていた頃よりもパフォーマンス的には優れているので、最近はRemoteActionお気に入りですw

でもちょっと惜しい!

上記の方法を使えば、特に問題なく処理できるように思えますが、1点解決しなければいけない問題が残ってます。
Force.comでのJSON Support - yonet77 的な雑記帳 でも書きましたが、管理パッケージ下にあるオブジェクトをdeserializeする型に指定した場合、日付のパースがうまくできない、という問題です。。
これについては、今後回避策など検討予定です...

そんなわけで、現時点ではカスタムオブジェクトの項目を定義した型を用意するのが安全のようです。。。残念。
(管理パッケージ下のオブジェクトでなければ、上記のやり方でも大丈夫だとは思いますが。)

ちなみに、
Community - Don't know the type of the Apex object to deserial... - Page 4 - Force.com Discussion Boards
でも似たような問題に遭遇した事例が取り上げられてるみたいです。今後改善されるのかなー?と期待しつつ、今回はここまで!
ちなみに、次回も未定です。

Enjoy, JSON Support & Apex RemoteAction!

なお、このエントリーはForce.com Advent Calendarに参加しています
僕の担当がかなり遅くなってしまって、後続の担当者に多大な迷惑をかけてしまったこと、お詫びします。。。m(_ _)m

Force.comでのJSON Support

突然ですが、お仕事ではForce.com上での開発が多いです。(というか、大半)
そんな中で気になったことなど、ココに書き残します。

JSON Support的なお話

Winter '12 からJSONがネイティブでサポートされるようになって、JSONObjectを使っていた頃が懐かしく感じられます。(苦笑)
そんなJSON SupportのdeserializeでJOJO程ではないですが奇妙な挙動に遭遇しました。

基本

元々のリファレンスには、

Deserializes the specified JSON string into an Apex object of the specified type.
The jsonString argument is the JSON content to deserialize.
The apexType argument is the Apex type of the object that this method creates after deserializing the JSON content.
The following example deserializes a Decimal value.

JSON Methodリファレンス

と説明書きがあって、付随するコードは

Decimal n = (Decimal)JSON.deserialize('100.1', Decimal.class);
System.assertEquals(n, 100.1);

という感じに紹介されています。
要は、JSON形式の文字列をApexオブジェクトに変換してくれます。

deserializeメソッドの第一引数にJSON文字列を、第二引数に「System.Type」を指定してます。
上記の例では、Decimal.class ですが、ココには標準オブジェクトやカスタムオブジェクトも指定できるみたいです。
例えば、

Account acc = (Account)JSON.deserialize('{"Name":"AAAA", "AccountNumber":"001", "AnnualRevenue":500000}', Account.class);
System.assertEquals(acc.Name, 'AAAA');

となります。

遭遇した奇妙な出来事

JOJOの冒険にはほど遠いですが、奇妙な挙動に遭遇しました。
ポルナレフ風に言えば、

あ…ありのまま 今起こった事を話すぜ!
『おれは日付もパースできたと思ったらいつのまにかエラーが起きてた』
な…何を言っているのかわからねーと思うがおれも何をされたのかわからなかった…

という感じですが、何を言ってるのか全く分からないので、実際のコードを交えてみます。

以下のコードをSandbox環境で実行してみました。
なお、「kanseibi__c」は日付のカスタム項目です。

String jsonString = '[{"Name":"TestName01", "AccountNumber":"001", "AnnualRevenue":500000, "kanseibi__c":"2011-12-30"}]';
List<Account> rtn = (List<Account>)JSON.deserialize(jsonString, List<Account>.class);
System.debug('*** rtn = ' + rtn);

結果:

USER_DEBUG [3]|DEBUG|*** rtn = (Account:{Name=TestName01, AnnualRevenue=500000.0, kanseibi__c=2011-12-30 00:00:00})

うん、思った通りの結果が返ってきました。パニックになる要素は微塵もありません。

では、今度はこんなコードを実行してみます。
なお、「gii__SalesOrder__c」というのは、とある管理パッケージのカスタムオブジェクトで、「gii__RequiredDate__c」は日付項目です。

String jsonString = '{"gii__RequiredDate__c":"2011-12-30", "gii__Released__c":true}';
gii__SalesOrder__c rtn = (gii__SalesOrder__c)JSON.deserialize(jsonString, gii__SalesOrder__c.class);
System.debug('** rtn =  ' + rtn);

結果:

FATAL_ERROR System.JSONException: Cannot deserialize instance of date from VALUE_STRING value 2011-12-30

日付項目に対して、同じ日付を指定したのに・・・JSON Exceptionが返ってた・・・だとッ!?
 「意外!それはJSONExceptionッ!」
と言わんばかりに意外な結果でした。。
※ちなみに、管理パッケージ下にないカスタムオブジェクトの日付項目はちゃんと読み込んでくれました。


Sandbox環境が悪いのか、管理パッケージが悪いのか・・どういう条件下でエラーが発生するのか特定できてないですが、同じ日付文字列を指定しただけなのに挙動が変わるってのは・・なかなか興味深いところです。(って自分だけかも。。)

もう少し色々調べてみようと思いますが
 「そんなの当たり前だゼー!こうすればいいんだよッ!!」
というのがありましたら、ぜひツッコミ下さい。
※例えば、Salesforce ISV部隊に所属(している|していた歴代の)ナイスガイが
 「やれやれだぜ...」
と言いながらも華麗にツッコミしてくれることを期待しますww えぇ、あくまで例えです。


こんなん使ってどうすんの?的な話は次回にできたらいーなー・・と思います。


あ、ちなみに、このエントリーはForce.com Advent Calendarに参加しています。
ちょっと良く分からないのですが、2周しそうな雰囲気がでてるのがなんともはや。

「お…恐ろしいッ おれは恐ろしい!
なにが恐ろしいかって ジョースター! Force.com Advent Calendar が1周で終わらないんだ
2回担当するように変わっているんだぜーーーーッ!!」

と、シュトロハイムばりに恐怖を感じつつ。
(内容薄いなー... ごめんなさい。 次回はまだ未定です。)