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メソッドは同じです。
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
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ディレクトリ下に置かれるからです。
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
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メソッドがないため、やや煩雑なコードになっています。
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
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