RubyAMF で関連を扱う

RubyAMF の続き、今度は関連を扱います。一対多と多対多のケースについて。
ここを読む前に、以前の記事を参照してとりあえず RubyAMF を動かしてみることをオススメします。

一対多のサンプル

以下のようなモデルと関連を考えます。ユーザ登録画面のユーザデータと、そのユーザが住んでいる都道府県の選択だと考えてください。

  • User(belongs to State)
    • family_name
    • given_name
    • state_id # 都道府県選択
  • State(has many Users)
    • title # name は避けて、 title を選択。


まずは ActionScript 側の準備から。User 及び State モデルを定義しておきます。

// User.as
package {
  [RemoteClass(alias="User")]
  public class User {
    public var id:int;
    public var errors:Object;
    public var family_name:String;
    public var given_name:String;
    public var state_id:int;
  }
}
// State.as
package {
  [RemoteClass(alias="State")]
  public class State {
    public var id:int;
    public var errors:Object;
    public var title:String;
  }
}

次に、実際の入力画面にあたる mainApp.mxml に通信に使うサービスを定義します。

<mx:RemoteObject
  id="stateService"
  destination="rubyamf"
  endpoint="http://localhost:3000/rubyamf_gateway/"
  source="stateController"
  showBusyCursor="true"
/>

さらに、入力に使う ComboBox も作成。

<mx:FormItem label="都道府県">
  <mx:ComboBox width="150" id="size" dataProvider="{stateArray}" />
</mx:FormItem>

dataProvider に指定した stateArray に RubyAMF を使ってデータを流し込みます。
続いて、肝心のデータ読み取り部分のソース。

<mx:Script>
  <![CDATA[
    [Bindable]
    // stateArray を定義しておく
    private var stateArray:Array = new Array();

    // 初期化時のメソッドとしてデータ呼び出しを定義
    private function init():void {
      // 都道府県をロード(stateController の list メソッドを呼び出す)
      var stateCall:AsyncToken = stateService.list();
      stateCall.addResponder(new mx.rpc.Responder(
        function(e:ResultEvent):void {
          for each(var st:State in e.result as Array) {
            stateArray.push({'label':st.title, 'data':st.id});
          }
        },
        function(e:FaultEvent):void {
          Alert.show('通信時にエラーが発生しました');
        }
      ));
    }
  ]]>
</mx:Script>

先ほど作成した stateArray に値を入れているのが分かると思います。
後は Rails 側の実装、 state_controller に list メソッドを定義します。

class StateController < ApplicationController
  def list
    render :amf => State.find(:all)
  end
end

この場合 User の state_id に値を入れる事ができるので、後のやり取りは以前の記事同様になります。

多対多

先ほどの User モデルにさらに追加、趣味を複数回答可能で選択できるようにします。関連は has_many through を利用して以下のように定義しておきます。

  • User(belongs to State, has many Favorites through FavoriteUser)
    • family_name
    • given_name
    • state_id
  • Favorite(has many Users through FavoriteUser)
    • title # ここも name は避けておく
  • FavoriteUser(belongs to User, belongs to Favorite)
    • user_id
    • favorite_id

ActionScript 側でクラスを定義するところまでは先ほどと同じなので省略、まずは mainApp.mxml の編集から。

まずは通信に使うサービスの定義。ここも殆ど同じです。

<mx:RemoteObject
  id="favoriteService"
  destination="rubyamf"
  endpoint="http://localhost:3000/rubyamf_gateway/"
  source="favoriteController"
  showBusyCursor="true"
/>

入力には CheckBox を使いますが、動的に作成する必要があるのでとりあえず以下のように定義しておきます。

<mx:FormItem label="趣味(複数回答可能)" id="favoriteForm" />

では、ここに動的に CheckBox を作っていきましょう。

<mx:Script>
  <![CDATA[
    [Bindable]
    // favoriteArray を定義するが、今回は初期化はしない
    private var favoriteArray:Array;

    // 初期化時のメソッドとしてデータ呼び出しを定義
    private function init():void {

      // 中略

      // 趣味をロード
      var favoriteCall:AsyncToken = favoriteService.list();
      favoriteCall.addResponder(new mx.rpc.Responder(
        function(e:ResultEvent):void {
          for each(var fb:Favorite in e.result as Array) {
            var fbBox:CheckBox = new CheckBox();
            fbBox.data = fb.id;
            fbBox.label = fb.title;
            // ここで先ほどの favoriteForm の子として CheckBox を登録
            favoriteForm.addChild(fbBox);
          }
        },
        function(e:FaultEvent):void {
          Alert.show('通信時にエラーが発生しました');
        }
      ));
    }
  ]]>
</mx:Script>

先ほどとは違い、呼び出し段階で動的に子要素を追加していきます。
こちらも favorite_controller に list メソッドを先ほど同様の形で定義しましょう。

さて、先ほどとは違い、ここで入力された趣味のデータは User モデルの一部として送信する事はできません。そこで、このデータは個別に送る事にします。先ほど作成した favoriteArray の使いどころです。mainApp.mxml に以下のメソッドを追加します。

// 趣味の ID を取得する
private function setFavoriteId():void {
  // 二重送信対策、ここで配列を初期化してしまう
  favoriteArray = new Array();
  for each(var ckb:CheckBox in favoriteForm.getChildren()) {
    if(ckb.selected == true) favoriteArray.push(ckb.data);
  }
}

favoriteForm.getChildren で子要素の配列が取得できるため、それらすべてについて選択されているかどうかチェックしていきます。次に Flash から Rails へのデータ送信時に、このデータも同時に渡すよう変更を行います。

// ユーザデータの保存
private function save(user:User):void {
  var call:AsyncToken = userService.save({user:user, favoriteArray:favoriteArray});
  call.addResponder(new mx.rpc.Responder(onSaveSuccess, onSaveFault));
}

実は RubyAMF はどんなデータでも渡す事ができるので、このように user データと同時に favoriteArray を送信してしまえば、 Rails 側でこのデータを読めるようになります。
では、 Rails アプリからこの値を読み込んで、データを保存します。

# user_controller.rb
class UserController < ApplicationController
  def save
    @user = params[:user] # ユーザデータ本体
    @favorites = params[:favorite_array] # 趣味の id の配列

    respond_to do |format|
      format.amf do
        # ここは本当はトランザクションにすべき
        if @user.save
          # 続けて趣味を保存
          unless @favorites.blank?
            @favorites.each do |f|
              fb = Favorite.find(f)
              # 趣味のデータを保存
              @user.favorites << fb
            end
          end

          render :amf => params[:user]
        else
          render :amf => FaultObject.new(params[:user].errors.join("\n"))
        end
      end
    end
  end
end

ここで params[:favorite_array] という形でアクセスするには、以前の記事を参考に設定を行ってください。

総括

強引な気がしますが、以上でとりあえず関連も扱う事ができました。RubyAMF のドキュメントが見つかればきちんと調べるのですが、公式サイトにもなかったような気が。私も色々知りたいので、上のコードへの突っ込みは歓迎します。
それにしてもはてなActionScriptシンタックスハイライトが実装されるのはいつになるのやら。