Rails勉強会@東京30 - what's new in Rails 2.1 セッションまとめ
July 25th, 2008
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について情報が載ってそうなところあれこれ。
- Ryan’s Scraps
http://ryandaigle.com/tags/Edge%20Rails - redemption in a blog
http://blog.codefront.net/category/ruby-on-rails/ - GitHub
http://github.com/rails - Lighthouse
http://rails.lighthouseapp.com/dashboard - Trac
新しいバグは全て Lighthouseに登録されるようになっているが、
旧バグの中にはまだこちらで生き残っているものもあり。
http://dev.rubyonrails.org/ - Riding Rails
A taste of what’s coming in Rails 2.1
http://weblog.rubyonrails.org/2008/4/1/a-taste-of-what-s-coming-in-rails-2-1
2.1 RC1 リリース告知
http://weblog.rubyonrails.org/2008/4/30/rails-2-1-release-candidate-is-imminent - Ruby on Rails: Core
http://groups.google.co.jp/group/rubyonrails-core - Rails Envy
http://railsenvy.com/ - Railscasts
http://railscasts.com/ - 松田ぽじぺ@Rails東京29
http://docs.google.com/Doc?id=ddn3rmd_11gx4t2cjg - 松田ぽじぺ@Rails東京30
http://docs.google.com/Doc?id=ddn3rmd_12ddzcw4q3
■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の新機能たち
主立ったものを列挙してみます。これまた当日に思い出しベースで書いているので網羅はできてない気がしますが。
- named_scope
昨秋登場した has_finder という life changingなプラグインが本家に取り込まれた。
個人的には、おそらくこれが Rails2.1の最大のウリなんじゃないかと(詳しくは後述)。
http://ryandaigle.com/articles/2008/3/24/what-s-new-in-edge-rails-has-finder-functionality
http://pivots.pivotallabs.com/users/nick/blog/articles/284-hasfinder-it-s-now-easier-than-ever-to-create-complex-re-usable-sql-queries
- has_one :through
関連テーブルを挟んだ has_one 関連がエレガントに書ける機能。
http://ryandaigle.com/articles/2008/3/24/what-s-new-in-edge-rails-has-one-through
- ARのレコードに dirty object機能
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
- AR#create にブロックを渡せるように
ブロック内部を returning的に書ける文法が追加された。見た目がRubyっぽく書けて嬉しいかも?
http://github.com/rails/rails/commit/dd120ede53eaf71dee76894998a81626b7a689fc
- migrationファイルのファイル名が連番じゃなくてタイムスタンプに
http://ryandaigle.com/articles/2008/4/2/what-s-new-in-edge-rails-utc-based-migration-versioning
- schema_infoテーブル → schema_migrations という新テーブル
- Railsプロジェクトに依存している gemを設定できるように
http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies
- RAILS_ROOT → Rails.root, RAILS_ENV → Rails.env
http://dev.rubyonrails.org/changeset/9180
- 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
- javascript_include_tag とかの :defaults を自分で再定義できるように
http://blog.codefront.net/2008/03/20/living-on-the-edge-of-rails-12/
- ドキュメントがすごい勢いで充実中
- Ruby1.9 / 1.8.7 対応
実はこれが一番嬉しい変更、という人も多いかも。
1.9系のメソッドたちは ActiveSupportと激しくかぶるので、ActiveSupportにかなり修正が入っている。
- インフラの変化
Subversion → Git
Trac → Github
Trac → Lighthouse
- script/plugin install が Gitのリポジトリに対応
- script/dbconsole
上記のとおり。
http://github.com/rails/rails/commit/4a07103687084496b773e18a03b1f2f5e686f7ad
(※懇親会で喋ってて思い出したので追記)
- ARで connection pool の実装が開始
http://groups.google.co.jp/group/rubyonrails-core/browse_thread/thread/16851dd787844a34/e89e9c741f89227e
■ 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.