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

