CodeZine(コードジン)

特集ページ一覧

Curl+JRuby+Google App EngineでTwitter風アプリを作る
~GAE編~

最終回

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2009/09/30 14:00
目次

GAE用の書き換え 1

 それでは、前回ローカル環境で構築したアプリをGAEで動作させるために、どのように書き換えたのか見ていくことにしましょう。

Friendshipモデルの書き換え

 ActiveRecordを使ったモデルはActiveRecord::Baseを継承してしましたが、BumbleではBumbleをinclude(Mixin)するだけです。ActiveRecordFeaturesForBumbleというActiveRecordと互換を取るために作成したモジュールもinclude(Mixin)しています。詳しくは後述します。

 ActiveRecordではカラム情報(属性情報)はRDBから自動的に取得していますが、Bumbleではdsでカラム名(属性名)を定義します。belongs_toの定義は多少違いますが、ActiveRecordと似た形になっています。

 条件に合うレコードすべて取得するメソッドはBumbleでも all ですが、一致検索条件を hash で指定します。また Datastoreの制限で <> (一致しない) は指定できないので、self.friendsメソッドでは自分自身のレコードを除くためにRuby配列の reject!メソッドを使っています。

 次の self. not_friend_usersメソッドでは、Datastoreには in 演算子の機能がないので、やはりRubyのイテレータを使って全ユーザーから友人のレコードを削除し、友人以外の一覧を作っています。self.create_selfメソッドは同じです。

ActiveRecord版
class Friendship < ActiveRecord::Base
  belongs_to :user
  belongs_to :friend, :class_name => "User", :foreign_key => "friend_id"
  
  def self.friends(my_id)
    Friendship.all(:conditions => ["user_id = ? and friend_id <> ?", my_id, my_id])
  end

  def self.not_friend_users(my_id)
    User.all(:conditions => ["id not in (?)", Friendship.find_all_by_user_id(my_id).map(&:friend_id)])
  end
  
  def self.create_self(my_id)
    Friendship.new(:user_id => my_id, :friend_id => my_id).save!
  end
end
Bumble版
class Friendship
  include Bumble
  include ActiveRecordFeaturesForBumble
  
  ds :user_id, :friend_id
  belongs_to :user, User
  belongs_to :friend, User
  
  def self.friends(my_id)
    Friendship.all(:user_id => my_id).reject {|f| f.friend_id == my_id}
  end

  def self.not_friend_users(my_id)
    not_friends = User.all
    Friendship.all(:user_id => my_id).each do |friend|
       not_friends.reject! {|user| user.id == friend.friend_id}
    end
    not_friends
  end

  def self.friend_user_ids(my_id)
    Friendship.all(:user_id => my_id).map{|f| f.friend_id}
  end
  
  def self.create_self(my_id)
    Friendship.new(:user_id => my_id, :friend_id => my_id).save!
  end
  
end

Userモデル

 Userモデルでの変更は、Friendshipモデルで説明したのとほぼ同じです。ただし、画像が格納されるディレクトリが変更になっているのはJavaのデプロイ環境に合わせ、app/以下のプログラムが public/WEB-INFディレクトリ下に置かれるからです。

ActiveRecord 版
class User < ActiveRecord::Base
  has_many :friendships
  has_many :statuses
  
  def self.authenticate(screen_name, password)
    user = self.find_by_screen_name(screen_name)
    (user && user.password == password) ? user.id : nil
  end

  IMAGES_DIR = "./public/images/"
  PROFILE_IMAGES_DIR = "profile_images"
  
  def self.profile_image_selector
    Dir.glob(IMAGES_DIR + PROFILE_IMAGES_DIR + "/*").map {|path| path.sub(IMAGES_DIR, '')}
  end
  
  after_create {|rec| Friendship.create_self(rec.id)}

end
Bumble 版
class User
  include Bumble
  include ActiveRecordFeaturesForBumble

  ds :screen_name, :password, :profile_image_path
  has_many :friendships, Friendship, :user_id
  has_many :statuses, Status, :user_id
  
  def self.authenticate(screen_name, password)
    user = User.first(:screen_name => screen_name)
    (user && user.password == password) ? user.id : nil
  end

  IMAGES_DIR = "../images/"
  PROFILE_IMAGES_DIR = "profile_images"
  
  def self.profile_image_selector
    Dir.glob(IMAGES_DIR + PROFILE_IMAGES_DIR + "/*").map {|path| path.sub(IMAGES_DIR, '')}
  end
  
  after_create {|rec| Friendship.create_self(rec.id)}
end

Statusモデル

 Datastoreにはjoinの機能はありませんので、友人毎の発言を取得しRubyの機能で合成、ソートしています。JSON用のHashデータを作成する部分も、Datastoreから取得したデータをHashに変換する際に便利な attributsメソッドがないため、やや煩雑なコードになっています。

ActiveRecord版
class Status < ActiveRecord::Base
  belongs_to :user
  
  def self.all_frends(my_id)
    Status.all(:conditions => ["friendships.user_id = ?", my_id],
                    :joins => "JOIN friendships ON statuses.user_id = friendships.friend_id",
                    :order => "created_at DESC")
  end
  
  def api_data(image_url)
    status_attrs = self.attributes
    user_attrs = User.find(self.user_id, :select => "id,screen_name").attributes
    user_attrs["profile_image_url"] = image_url.call(self.user.profile_image_path)
    status_attrs["user"] = user_attrs
    status_attrs
  end
end
Bumble版
class Status
  include Bumble
  include ActiveRecordFeaturesForBumble
  
  ds :text, :user_id, :created_at
  belongs_to :user, User
  
  def self.all_frends(my_id)
    statuses = []
    Friendship.all(:user_id => my_id).each do |friend|
      statuses += Status.all(:user_id => friend.friend_id)
    end
    statuses.sort {|a, b| b.created_at <=> a.created_at}
  end
  
  def api_data(image_url)
    status_attrs = Hash["text", self.text, "user_id", self.user_id, "created_at", self.created_at]
    user = User.find(self.user_id)
    user_attrs = Hash["id", user.id, "screen_name", user.screen_name]
    user_attrs["profile_image_url"] = image_url.call(self.user.profile_image_path)
    
    status_attrs["user"] = user_attrs
    status_attrs
  end
end

ActiveRecordFeaturesForBumbleモジュール

 Bumbleの足りない機能を追加するActiveRecordFeaturesForBumbleですが、かなり難易度の高いRubyモジュールのため、詳細に説明すると本連載の範囲を超えてしまいますので、内容を簡単に説明します。

 まずモジュールの構成ですが、クラスに追加する インスタンスメソッドを InstanceMethodsモジュールに定義、クラスメソッドをClassMethods モジュールに定義し、include時に呼び出される self.includedメソッドを使ってincludeしたクラスに各メソッドの追加を行っています。このようなスタイルはRailsの内部やプラグインでよく使われています。

 id、update_attributes、save、destroyメソッドはBumbleには無いですが、コントローラー等で使われているメソッドの追加です。また、new_record?、errors、to_paramメソッドは明示的に呼び出されていませんがテンプレート用のメソッド等から呼び出されています。これらのメソッドはActiveRecordの全仕様を実装したものではなく、このアプリで必要な部分のみの実装です。例えば、入力値等の検証(Validation)結果を戻すerrorsはいつでもエラー無しを戻します。

 さて、find、allはBumbleで定義されているメソッドの置き換えです。最後の方にあるalias first find; alias all_origin all;でBumbleに定義されたfindメソッドをfirst、allメソッドをall_originという名前を付けて保存し、find、allを再定義しています。Rubyはオープンクラスで既存クラスのメソッドを後から追加・変更できます。find、allのおもな変更点は。新規に作成しまだDBに保存してないインスタンスと、DBから読み出したインスタンスを区別するための目印を付ける事です、この目印をチェックするのがnew_record?メソッドです。

 saveメソッドではややトリッキーな事を行っています。まずafter_createで定義されたDatastoreレコードの作成後に実行するコードの実行する部分と、カラム名(=インスタンス変数名)により特殊な処理を行う_instance_conversionです。

 関連を示すID番号等はformから文字列でポストされてきますが、Datastore内には数値として格納されています。この変換はActiveRecord内で自動的に行われますがBumbleでは行われません。そこで_instance_conversionは_idで終わるカラムは数値だという前提で、文字列を数値に変換しています。また、created_atカラムには現在の時間を代入しています。

module ActiveRecordFeaturesForBumble
  module InstanceMethods
    def id
      key
    end

    def update_attributes(attrs)
      attrs.each do |k, v|
        send "#{k}=", v
      end
      save
    end

    def save
      _instance_conversion
      begin
        new_recod = new_record?
        save!
        _after_create.call(self)   if respond_to?(:_after_create) && new_recod
        true
      rescue
        RAILS_DEFAULT_LOGGER.debug("+++ save error :#{$!}")
        false
      end
    end
    
    def destroy
      delete!
    end

    def new_record?
      !instance_variable_get(:@_finded_record)
    end

    def errors
      ret = []
      def ret.count
        0
      end
      ret 
    end
    
    def to_param
      key.to_s
    end

    private

    def _instance_conversion
      methods = self.class.instance_methods(false)
      methods.each do |method|
        if method =~ /_id=$/
          send(method, send(method[0..-2]).to_i)
        elsif method == "created_at="
          self.created_at = Time.now
        end
      end
    end
  end

  module ClassMethods
    
    def find(id)
      obj = self.get(id)
      obj.instance_variable_set(:@_finded_record, true)
      obj
    end
    
    def all(opt = {})
      list = all_origin(opt)
      list.each {|e| e.instance_variable_set(:@_finded_record, true)}
      list
    end
        
    def after_create(&block)
      cattr_accessor :_after_create
      self._after_create = block
    end
  end
  
  def self.included(base)
    base.send :include, InstanceMethods
    base.class_eval("class << self; alias first find; alias all_origin all; end")
    base.send :extend,  ClassMethods
  end
end

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 吉田裕美(ヨシダユウミ)

    有限会社 EY-Office 取締役 CADのベンチャー企業でCADのコア部分や図面管理システムなどの開発に従事した後、独立しJava,Ruby,PerlでWebアプリを中心に開発してきた。現在は殆どの開発はRuby on Rails。 ここ数年はソフトウェアエンジニアの教育に興味をもち、従来の...

バックナンバー

連載:Ruby on Rails + Curl
All contents copyright © 2005-2020 Shoeisha Co., Ltd. All rights reserved. ver.1.5