5月に行われた前回(第30回)Rails勉強会@東京では、ふりかえりの際に 『次回から「前回の勉強会のまとめ」を開催告知Wikiページに載っけるようにしてみよう』という Tryが挙げられていた ので、第30回の時にやった「what’s new in Rails 2.1 セッション」のまとめを。

と言っても、2ヶ月も前のことを今更思い出せるわけないので、以前 GoogleDocsに揚げたやつ をそのまま貼るだけなんだけどね。
なので、以下の内容はあくまでも「2ヶ月前の最新情報」であり、現在の最新情報とは違うかもしれないのでご注意ください(以下こぴぺ)。
 


what’s new in Rails 2.1 セッション

セッション中に即席で書いてるんでだいぶいい加減な内容です。すみません・・・

■ネタ元

Rails2.1について情報が載ってそうなところあれこれ。

■Rails Edgeを cloneしてみよう

今のところ、Rails 2.1系を触ってみるには、以下のような方法がある。

  • RC1を使う
% rake rails:freeze:edge

zipで固められた最新ソースの玉を落として解凍してくれるように変更されたらしい
(たぶん Gitがまだ充分に普及していないから(?))。
が、zipファイルを上げる運用はまだ運用が開始されていない模様。

  • piston
    pistonが Gitに対応してくれたらこれが一番ラクかも。
  • gitコマンドで
% rails edge_080518
% cd edge_080518/rails
% git clone git://github.com/rails/rails.git

 

■Rails2.1の新機能たち

主立ったものを列挙してみます。これまた当日に思い出しベースで書いているので網羅はできてない気がしますが。

  • ARのレコードに dirty object機能
カラム名 + _changed? メソッドが例によって各 content_columnsに追加される ARマジック。

save時に発行される update文を、変更されたカラムのみを対象にすることができて、うまく使えばパフォーマンスの向上が見込まれる。
さらに、変更前のオリジナルの値も保持してくれている。
ADO .Netの DataTableにあったようなあの機能という理解で良いかと。
http://ryandaigle.com/articles/2008/3/31/what-s-new-in-edge-rails-dirty-objects
http://dev.rubyonrails.org/changeset/9157
http://blog.codefront.net/2008/04/05/living-on-the-edge-of-rails-14-the-extreme-edition-extremely-late/
http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-partial-updates

  • schema_infoテーブル → schema_migrations という新テーブル
  • ActiveModel
    2.1には正式に入るのかも?
  • TimeZone機能
rake time:zones:*

→ 非US系開発者から若干反感を買ってる模様(以下のコメント欄など参照)。
http://rails.lighthouseapp.com/projects/8994/tickets/195-convenience-methods-for-time-zones-outside-the-u-s

  • ドキュメントがすごい勢いで充実中
  • Ruby1.9 / 1.8.7 対応
    実はこれが一番嬉しい変更、という人も多いかも。
    1.9系のメソッドたちは ActiveSupportと激しくかぶるので、ActiveSupportにかなり修正が入っている。
  • インフラの変化
    Subversion → Git
    Trac → Github
    Trac → Lighthouse
  • script/plugin install が Gitのリポジトリに対応

(※懇親会で喋ってて思い出したので追記)

■ named_scopeを詳しく

以下、セッション中にデモで書いたコード +α をいちおう載っけておきます。
もう終わった勉強会のぽじぺを見る人って居るんかな?とは思いつつ・・・

  • Railsアプリを生成して Edge Railsを取得
    手順は上記のとおり。
  • こんな感じのいいかげんな modelを一丁 generate
% ./script/generate model person name:string age:integer gender:integer
% rake db:migrate
  • サンプルデータを何件か作って、
>> Person.create :name => 'person 1', :age => 20, :gender => 1

    >> Person.create do |p|
    >>   p.name = 'person 2'
    >>   p.age = 30
    >>   p.gender = 2
    >> end

    >> Person.create :name => 'person 3', :age => 3, :gender => 1
  • named_scopeをあれこれ作ってみる
class Person < ActiveRecord::Base
      # Hashで conditions。基本形。
      named_scope :male, :conditions => {:gender => 1}
      named_scope :female, :conditions => {:gender => 2}
      # Stringで conditions。
      named_scope :adult, :conditions => 'age >= 20'
      # Procで scopeを定義した形。例がへぼくてすいません。
      named_scope :name_start_with_p, lambda { {:conditions => ["name like ?", 'p%']} }
      # Proc方式だと scopeに引数も渡せる。
      named_scope :older_than, lambda {|n| {:conditions => ['age >= ?', n]} }
      # さっきのへぼい例もこれなら実用的。
      named_scope :name_start_with, lambda {|s| {:conditions => ["name like ?", "#{s}%"]} }
    end
  • 作ったnamed_scopeたちをつついて遊んでみる
>> Person.male
  Person Load (0.000603)   SELECT * FROM "people" WHERE ("people"."gender" = 1)
>> Person.adult
  Person Load (0.000596)   SELECT * FROM "people" WHERE (age >= 20)
>> Person.older_than(7)
  Person Load (0.000651)   SELECT * FROM "people" WHERE (age >= 7)

動いた。あれ?genderは予約語だったか?とか思いつつ。
こんな調子で、ちっちゃいクエリの断片をあらかじめ名前を付けて定義しておくことができる、というのが named_scope の基本機能。
これだけだと、昔 Javaが流行ってた頃に DAO って言われてたやつと何が違うの?と言われてしまいそうなところだが、

  • なんと、ネストして呼び出した時のSQLの発行は一回だけ!
>> Person.female.adult
      Person Load (0.000494)   SELECT * FROM "people" WHERE ((age >= 20) AND ("people"."gender" = 2))

とにかくこれがすごい。呼び出し側のコードの可読性がめちゃめちゃ高くてシビれる。
→ 流れるようなインターフェース(笑)

  • そういえばセッションでは説明し忘れたけど、countとか書いても勝手にネストしてくれるよ。
>> Person.older_than(10).female.name_start_with('').count
  SQL (0.000375)   SELECT count(*) AS count_all FROM "people" WHERE (((name like '松%') AND ("people"."gender" = 2)) AND (age >= 10))

join とか sortとか、その他もろもろは今回は省略。
ちなみに、実はそのあたりは今でも盛んにコミットが行われているところなので、仕様もまだ多少揺れる可能性があるかも。

  • named_scopeメソッドで作られるものたちの実体は?
>> pp Person.scopes
{:male => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :name_start_with => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :female => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :adult => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :name_start_with_p => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :scoped => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>,
 :older_than => #<Proc:0x017ead50@/Users/matsuda/railstokyo/edge080518/vendor/rails/activerecord/lib/active_record/named_scope.rb:87>}

こんな感じの、キー = 宣言した名前、値 = Proc のインスタンス、な Hash で保持されていることがわかる。

  • これを踏まえて、よくある検索画面を想定して動的にクエリを組み立ててみる
    さて、この named_scope機能が実際のアプリでどう使えるか、って考えてみると、
    よくある検索画面みたいなやつで条件に含める項目自体が動的に変化するようなケースを
    思いつくんじゃないだろうか。
    つまり、どの scopeをクエリに組み込むか、というのを動的に選んでいきながら複雑なクエリを組み立てて、
    最後に一発ドーンと実行するようなイメージ。
def self.search(params)
  exec_scopes = []
  if params[:is_male]
    exec_scopes << [:male, nil]
  end
  if params[:age_min]
    exec_scopes << [:older_than, params[:age_min]]
  end
  # (中略)
end

おまけ

セッションでは上記のように書いて、こんな感じで実行したい scopeを配列とかに溜めて
evalか injectぐらいでガツッと実行してやるしかないよねー、みたいな説明をしておいたが、
Yuumiさんから「かなりぐちゃぐちゃなコードになりそう」
というようなご意見を頂いた。うーん。そうなんですよねー。

さて、そこらへんはその場でコードは書ききっていなかったので、以下参考までに実装イメージを。

※ 実装案1

つまりこういう文字列を組み立てて、

exec_scopes_string = exec_scopes.map {|s| "#{s[0]}(#{s[1]})"}.join('.')

evalに食わせるという。
引数の型とか数とかまともにハンドリングしてないけどまぁ大体こんな流れで。

eval "Person.#{exec_scopes_string}"

※ 実装案2

injectでぐりぐり実行させる。

exec_scopes.inject(Person) {|p, s| scopes[s[0]].call p, *s[1]}

やっぱりこっちのほうがイケてそうかなぁ。

ということで、案2のほうで実装してみたサンプルがこちら。
引数を複数取る scopeにも対応したかったので、例としてこんな scopeを追加して、

# 複数の引数。
    named_scope :age_between, lambda {|from, to| {:conditions => ['age between ? and ?', from, to]} }

検索を実行するメソッドをこんなふうに。

def self.search(params)
      exec_scopes = []  # [実行するスコープ, [引数の配列]] な配列
      exec_scopes << [Person.scopes[:male], nil] if params[:is_male]
      exec_scopes << [Person.scopes[:older_than], params[:age_min]] if params[:age_min]
      exec_scopes << [Person.scopes[:age_between], [params[:age_between][0], params[:age_between][1]]] if params[:age_between]
      # あとは、こんな調子で他の paramsたちの解釈をずらずらと書く。ここでは省略。

      # これが呼び出し部
      exec_scopes.reverse.inject(Person) {|p, s| s[0].call p, *s[1]}.all
    end

実行結果はこちら。

>> Person.search({:is_male => true, :age_between => [10, 30]})
      Person Load (0.000518)   SELECT * FROM "people" WHERE (("people"."gender" = 1) AND (age between 10 and 30))

ということで、とりあえずめでたく動作。

けっこうスッキリ書けたと思うが、でもこれは確かに書いた本人にしかわからないコードかも・・・
ポイントは便利メソッド injectの()内引数の使いかたと、最後にさりげなく書かれた .all(scopeが1個も使われなかった場合のデフォルトscopeになる)の工夫。
やっぱり Rubyはこんな書き方ができちゃうから面白い。
ただ、このあたりは需要がありそうなので、きっとフレームワークがもうちょっと親切な仕組みを提供してくれることでしょう。

* セッション後、もろはしさんが言ってた「これって集合のイメージだよね」みたいな説明のしかたが一番わかりやすいかもですね。

Sorry, comments are closed for this article.