LoginSignup
20
21

More than 5 years have passed since last update.

Railsアンチパターン<モデル編>③ スパゲッティSQL

Posted at

訳しながらまとめたメモを遂行して投稿する、って形で書いているので、若干視線が偉そうなのはご勘弁ください。実際自分も「ほえー」って言いながら読んでるレベルの人間ですので。

タイトルの通りだが、この章のアンチパターンは、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を使おう

ソリューション:全文検索エンジンを使おう

そういうものがある、ということを知っておこう。まとめてあるページ

結論:全文検索エンジンというものがあるということを知っておき、適切なケースで利用しよう

20
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
21