訳しながらまとめたメモを遂行して投稿する、って形で書いているので、若干視線が偉そうなのはご勘弁ください。実際自分も「ほえー」って言いながら読んでるレベルの人間ですので。
タイトルの通りだが、この章のアンチパターンは、ActiveRecordが用意してくれている豊富な機能を無視して、ほとんど直接SQLを書いているのと変わらないようなコードを書いてしまう、というもの。それを踏まえて以下のソリューションを読んでほしい。
ソリューション:関連を使う
例えば、以下のようなコードがあるとする。
class Toy < ActiveRecord::Base
def self.find_cute_for_pet(pet)
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓?
where(:pet_id => pet.id, :cute => true)
end
end
当然だが、「ActiveRecordには、関連というものがあってだな……」と突っ込みたくなると思う。こうすべきだ。
class Pet < ActiveRecord::Base
has_many :toys
def find_cute_toys
self.toys.where(:cute => true)
end
end
次に、前述(のパターンで触れた)の問題がある。PetモデルがToyモデルの実装を知った(:cute => true
)finderを実行してしまっている。これはよろしくないことだった。
関連にはブロックとメソッドを渡すことでチェーンメソッドを追加することができる。これを使えば、pet.toys.cute
という感じで呼べる。少しだけ改善にはなったが、依然Toyモデルの実装情報が漏れていることに変わりはない。どうするか。
extendオプションを使う(簡単な方の解説 Railsガイドにも解説がある)。これにより、関連を柔軟に拡張することができる。
これを使えば、以下のようになるだろう。
module ToyAssocationMethods
def cute
where(:cute => true)
end
end
class Pet < ActiveRecord::Base
has_many :toys, -> { extending ToyAssocationMethods }
end
これにより、ようやくtoyの実装をようやく隠蔽でき(このモジュールをToyクラスのモジュールにしてしまえば良い)、拡張した関連によってcuteなtoyを取得できるようになった。また、一度モジュールに定義してしまえば、他のモデルからtoyを呼び出す際にも使用できる。
さらに便利なテクニックがある。関連の所有者はproxy_association.owner
でアクセスできるので、
class Pet < ActiveRecord::Base
# has column :age
has_many :toys do
def appropriate
where(["minimum_age < ?", proxy_association.owner.age])
end
end
end
とかできる。便利。
またscopeを使えることも忘れてはいけない。簡単な条件ならこれで十分だ。
結論:関連を使いこなそう。ブロックメソッド渡し、scope, extendingを使えば実装を他クラスから隠蔽しつつ柔軟なモデル操作が可能だ。proxy_associationというテクニックもある。
ソリューション:スコープ大好きになれ
逐一で複雑なfinderを書くより、細かくscopeを作ることで再利用しやすくなる。
ただ、scopeの連結による.
はデミテルの法則を破る。
そこで、scopeを組み合わせたメソッドを作る(これはあんまり本質的な解決ではない気がするが)。
class RemoteProcess < ActiveRecord::Base
scope :running, where(:state => 'Running')
scope :system, where(:owner => ['root', 'mysql'])
scope :sorted, order("percent_cpu desc")
scope :top, lambda {|l| limit(l) }
def self.find_top_running_processes(limit = 5)
running.sorted.top(limit)
end
def self.find_top_running_system_processes(limit = 5)
running.system.sorted.top(limit)
end
end
巨大かつ複雑なfinderがある場合は、分割したscopeで「フィルタリング」するように書くとわかりやすい。
def self.search(title, artist, genre, published)
finder = matching(:title, title)
finder = finder.matching(:artist, artist)
finder = finder.matching(:genre, genre)
finder = finder.published unless published.blank?
return finder
end
scopeはメソッドチェーンであることが保証されていて扱いやすいし、モデルのある範囲について「名前」をつけることで意味を持たせることができるので、とても良い機能だ。どんどん使おう。
結論:scopeを使おう
ソリューション:全文検索エンジンを使おう
そういうものがある、ということを知っておこう。まとめてあるページ。