2010-08-31
Rails3現時点での移行覚え書き
なんとかRails3にだいたい移行できたのでその覚え書きを。
非互換の機能への対応
Rails3では、ActiveRecordの扱いを始め多くの修正が加えられたが、その一方でこれまで用いられてきた機能で廃止や非推奨になったものがたくさんある。
移行でwarningが出るケースとしては、
- ActiveRecordの検索関係のメソッド
- ActiveRecord.human_name (→ ActiveRecord.model_name.human)
- 新しくなったRoute
- Viewにおける<%と<%=
- RAILS_ROOTなどの定数
などがある。非推奨や廃止になった機能はチェックできるので、それを参考に全文検索して修正すれば、作業量はそこそこあるが想像ほどは大変ではなかった。
これ以外にも非互換性のためにエラーになった箇所としては、以下のようなものがあった:
デフォルトで呼ばれるhelper :all
これは先日の日記に書いたとおり。ApplicationControllerでclear_helpersを呼び出すことで回避できた。しかし、副作用として共有のためにApplicationHelperで定義したメソッドが他のヘルパから呼び出せなくなってしまう。アプリケーション全体で使いたいヘルパがあれば、モジュール化してActionView::Baseあたりにincludeすればいいかもしれない。
予期せぬタグのエスケープ
Rails3ではビューの全ての文字列にHTMLエスケープが自動で施されるが、content_tag等を使わずにタグを直書きしたヘルパメソッドの出力も一緒にエスケープされてしまった。今回はともかく動けばいいというスタンスなので、複雑な部分はエスケープされたくない文字列をhtml_safeに通すことで応急処置することにした。
'<h1>Title</h1>'.html_safe
Webrat関連の不具合の応急処置
テストにはRSpecとCucumber+Webratを使用している。RSpecでのテストは上記のように粛々と修正することで無事通すことが出来たが、Cucumber+Webratの方は意外と大変だった。引っかかった部分の多くはWebratでRails3への対応が少し遅れていることが原因なので、状況はすぐに改善すると思うが、現状での対応策をメモっておく。
Webrat関係の不具合2つ
現状で遭遇した不具合は2つ。1つめはリダイレクトを含むアクションをテストする際に、レスポンスが"You are being redirected"となってしまいエラーになること。そして2つめは、Rails3で導入されたHTML5的な削除アクションをうまく処理できないことだ。
これら2つの不具合を修正するパッチが、それぞれ以下のウェブサイトとWebratのticketに載っていたので、これらをinitializerでモンキーパッチとして取り込むことにした。
- Webrat and Rails: Using assert_contain after click_button gives me "You are being redirected" - Stack Overflow
- #365 Allow Webrat to read the data-method attribute - Webrat - webrat
# config/initializers/webrat_patch.rb module Webrat class Session def current_host URI.parse(current_url).host || @custom_headers["Host"] || default_current_host end def default_current_host adapter.class==Webrat::RackAdapter ? "example.org" : "www.example.com" end end class Link < Element def http_method if !@element["data-method"].blank? @element["data-method"] elsif !onclick.blank? && onclick.include?("f.submit()") http_method_from_js_form else :get end end end end
後者については、削除するかどうかを確認するダイアログ表示で使用されるJavaScriptが読み込まれていることもチェックしておく。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> (略) <%= javascript_include_tag :defaults %> <%= csrf_meta_tag %>
以上の修正を加えることで、Rails2の時に書いたテストは全て通すことが出来た。
Rails3の実装はRails2とは全く変わってしまっているが、いくつかの不整合はあるものの、インターフェースとしては互換性が保たれるように設計されていると改めて感じた。
参考:使用したバージョン
現在のテスト関係のGemfileは以下のようにしている。WebratはGitHubから最新版を取ってくることにした。
group :development, :test do gem 'rspec', '2.0.0.beta.20' gem 'rspec-rails', '2.0.0.beta.20' gem 'shoulda' gem 'cucumber' gem 'cucumber-rails' gem 'database_cleaner', :git => 'git://github.com/bmabey/database_cleaner.git' gem 'spork' gem 'launchy' gem 'webrat', :git => 'git://github.com/brynary/webrat.git' gem 'ZenTest' end
2010-08-30
Rails3でデフォルトで呼ばれるhelper :allを回避する
Rails3.0.0がリリースされたり、WEB+DB PRESSで総説が掲載されたりと、Railsまわりは3への移行の動きが激しくなってきたので、ちょいと作成中のRailsアプリの移行の検討をはじめてみた。
移行の最初の段階は意外と順調で、空のプロジェクトを作り、gem管理をbundle用に書き直し、もとのプロジェクトのファイルを上書きし、deprecatedとされた部分を書き直していって、モデル中心のRSpecはとりあえず全部通すことが出来た。ところが次のCucumberで統合テストレベルのチェックを始めた途端、ほとんどのテストが通らない状態に。
最初に気づいた問題は、意図とは違うタイトルやメニューが表示されること。自分の作っているシステムでは、それぞれのcontrollerに対応したhelperでget_titleといったヘルパメソッドを用意し、layoutからこれを呼び出すことで、それぞれのタイトルを表示する方法をとっていた。それがうまくいかないということは、helperがincludeされる順番がおかしいということだ。
少し調べてみて、どうやら、Rails2で登場した全てのHelperを一気にincludeしてしまう命令であるhelper :allがActionController::Baseで呼び出されていることがわかった。すでにpatchが提出されており、helper :allを打ち消すclear_helpersを呼び出せばいいらしい。
class ApplicationController < ActionController::Base clear_helpers (以下略) end
その一方で、Rails2では、メソッドをApplicationHelperに書いておけば、どのヘルパからでも呼び出せたのだが、上記メソッドの副作用によってこちらは出来なくなっているようだ。
これは一応解決したが、しかしまだ色々動かず。前途多難である。
2010-06-03
定義したDBのconstraintがrakaのspecタスクで反映されなかった件
特に新しい話ではないが、データベースシステム固有の処理まわりではまったのでメモ。このエントリーはRails2系列のお話です。
Railsにおけるテーブル定義
RailsではデータベースのテーブルをDDLの代わりに、migrationのrubyスクリプトとして定義する。
class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.string :login, :null=>false t.string :name ... end end end
データベースシステムに依存しない形にテーブルを定義できるため、定義ファイルを変更することなくデータベースシステムを変更するというメリットがある。一方で、システムに依存するとしても、ビューや制約など、データベースシステムに固有の処理を書き加えたいこともある。その場合には、SQL文を引数にしたexecuteメソッドを用いればよい。
class CreateUsers < ActiveRecord::Migration def self.up ... execute <<-'SQL'; ALTER TABLE users ADD CONSTRAINT some_constraint CHECK ... SQL end
Rakeのspecタスクではまる
制約の動作確認のために、validationせずにsaveして例外が発生することをチェックするspecを書いた。
specify {lambda{user.save_with_validation(false)}.should raise_error}
ここで問題発生。このspecを実行する際に、script/specやautospec経由なら正常終了するがrake specではエラーが生じる。調べたところこの制約条件がデータベースに反映されていない。また、specタスクを走らせた後にscript/consoleで例外が発生すべき処理をやらせても通ってしまう。つまり制約が無くなってしまったのだ。
原因は、実際にデータベース定義に用いられるschema.rbにこのexecuteメソッドが抜け落ちていたこと。specタスクが呼び出すdb:test:prepareタスクによってデータベース構造がschema.rbの内容でリセットされてしまうのだ。schema.rbにはconstraintのexecuteメソッドがないのだから、当然constraintは定義されないままになってしまう。
解決策
これを解決するには、migration時に利用するデータベース定義ファイルを、rubyベースのschema.rbからSQLベースに変更すればよい。こうしないと、データベースシステム依存の処理は途中で削除されてしまうようだ。具体的には、environment.rbで
Rails::Initializer.run do |config| ... config.active_record.schema_format = :sql end
と追記する。
schema.rbを明示的に作るときはdb:schema:dumpタスクを実行するが、SQLベースの定義ファイルを作るにはdb:structure:dumpタスクを用いればよい。ということで、
$ rake db:structure:dump RAILS_ENV=test $ rake spec
で、期待した結果が返ってきた。
2010-03-04
ビューをRSpecでテストする際の覚え書き
世の中、モデルをRSpec、それ以外をCucumberでテストするというスタンスがメジャーになってきているが、レイアウトが適切に組まれているかをチェックするのにビューにRSpecで使ってみたのでいくらかのメモ書きを。実際は"The RSpec Book"を買ったので色々試してみたかったという意図もあるわけだが。
ビューのRSpecの基本構造
ビューをRSpecでテストしたい場合は、ファイル名はindex.html.erb_spec.rbなどとしてファイルを作成して以下のように書けば良い。
require 'spec_helper' describe 'greeting/index.html.erb' do before do render end it "は、タイトルを表示すること" do response.should have_tag('h1') end end
beforeブロックで呼び出されたrenderによってHTMLが組み立てられる。その結果はresponseメソッドで取得することができる。
have_tagとhave_selectorマッチャによるHTMLのチェック
ViewやHelperではHTMLあるいはその断片をチェックすることが多い。次の出力のチェックを考える。
<h1 id="title">Hello!</h1>
これが変数responseに入っているとき、直接的に
response.should == '<h1 id="title">Hello!</h1>'
とチェックしても動くが、これだとHTMLとしては等価なスペースなどの差異なども検出されてしまうため特別なマッチャが必要になる。
HTMLのチェックに使えるマッチャには、assert_selectをラップしたhave_tagとWebratを利用したhave_selectorがある。どちらも最初の引数にセレクタをとり、have_tagは2番目の引数に内容を、have_selectorは2番目以降に属性と内容をハッシュで指定できる。これらを利用することで、
# have_tag response.should have_tag('h1#title', 'Hello!') # have_selector response.should have_selector('h1#title', :content=>'Hello!')
と書くことができる。have_selectorは引数に属性をハッシュにしたものを取る。そのため、属性の値まで見たい場合には書き方が少し変わってくる。たとえば、
<img src="foo.jpg" alt="image foo">
をチェックしたい場合、両者での書き方は以下のようになる:
# have_tag response.should have_tag("img[src='foo.jpg'][alt='image foo']") # have_selector response.should have_selector("img", :src=>'foo.jpg', :alt=>'image foo'))
なお、have_selectorを使うには、自動生成されるspec_helper.rbの最初にあるwebratマッチャの項目のコメントアウトを外す必要がある。
# Uncomment the next line to use webrat's matchers require 'webrat/integrations/rspec-rails' #ここのチェックを外す
"The Rspec Book"ではhave_tagを使わず全部have_selectorで書いているのがちょっと印象的。
ビューのテストにおけるヘルパメソッドのスタブ化
ビューの中ではしばしばヘルパメソッドが呼び出されるが、これをスタブ化することで独立したテストを行うことを考える。すなわち、
<h1 id="title"><%= get_title %></h1>
といった場合に呼び出されるget_titleをスタブ化したい。
このとき、単純にたとえばApplicationHelper.stub(:get_title).and_return('Hello!')とやってもうまくいかずちょっと悩んだが、no titleに、templateオブジェクトに対してスタブ化してねーと書いてあるのを見つけた。つまり、以下のようにすれば良い。
require 'spec_helper' describe 'greeting/index.html.erb' do before do template.stub(:get_title).and_return('Hello!') render end it "は、タイトルを表示すること" do response.should have_tag('h1#title', 'Hello!') end end
あまり使う機会は無いかもしれないが、覚えておいて損は無いかな。
2009-08-19
スリムになったSearchlogic v2をいじってみた
SearchlogicはBen Johnson (binarylogic) 氏による使い勝手の良いActiveRecord(以下AR)拡張プラグインだ。使ってみて面白かったので、だいぶ前にここでアツイ検索プラグインSearchlogic: pagenation, sortable table - MothProgのうしろがわというエントリーを書いた。ところが、その後編を書く前に発表になったSearchlogic version 2(以下Searchlogic v2)では、根底の「ロジック」は変わっていないものの、インターフェースやアプローチは全く異なっていた。
そこで、前回の続きのかわりに、Searchlogic v2の機能をざっとまとめてみた。あわせて、Searchlogicでなにが拡張されるのかを示すために、ActiveRecordの検索機能についても、ちょこっと覚え書きしておくことにした。
今回も例によって以下の作者によるページをもとに書いているので、正確なところはこちらをどうぞ。すばらしいプラグインに感謝。
以下の紹介例では、学生Student(多)ークラブClub(1)という関連をもったテーブルを使って説明する。Studentは、名前を示すname、学年を示すgrade、クラブのIDをもつ。Clubはとりあえず名前nameだけをもつ。
ActiveRecordにおける検索機能
検索の基本、findメソッド
ActiveRecordで最も頻繁に行う処理は、テーブル内のレコード検索だろう。その基本となるのはAR#findメソッドで、以下のようなおなじみのコードを頻繁に書くことになる。
# 最初の1つを得る # conditions節に配列を用いる student = Student.find(:first, :conditions=>["name = ?", "Ana"]) # conditions節にハッシュを用いる student = Student.find(:first, :conditions=>{:name=>"Ana"}) # 全てのレコードを得る students = Student.find(:all, :conditions=>["name = ?", "Ana"]) students = Student.find(:all, :conditions=>{:name=>"Ana"})
最近では、find(:first), find(:all)のショートカットとして、AR#all, AR#firstメソッドが定義され、ちょっとだけ略記できるようになった。
student = Student.first(:conditions=>{:name=>"Ana"}) students = Student.all(:conditions=>{:name=>"Ana"})
上の例のように、あるフィールドの値が一致する最初の1つのレコードを得る場合には、動的に定義されるfind_by_xxxという形のメソッド(動的ファインダ)も使える。複数のフィールドの値で一致するレコードの場合には、find_by_xxx_and_yyyと表記することもできる。
student = Student.find_by_name("Ana") student = Student.find_by_name_and_grade("Ana", 2)
便利な新機能、named_scope
Rails2.1以降では、findメソッドに加えて、named_scopeという新たな検索方法が提供されている。named_scopeは、モデルクラスで、検索条件としてのfindメソッドの引数を名前をつけて設定する機能だ。上の例だと、
class Student named_scope :search_ana, :conditions=>{:name=>"Ana"} named_scope :search_name, lambda{|n| :conditions=>{:name=>n}} end
などとする。1つめの例は、conditionsに"Ana"という固定の引数を与えた例、2つめは実行時に引数を取ってconditionsの引数を動的に与える例だ。引数やロジックを与えるときは、上記のようにブロックで指定し、その返値として定義する。
どちらにせよ、定義したnamed_scopeは、findメソッドで検索する際に、フィルターとして利用する。つまり、以下のように、findメソッド(あるいはそのショートカットのfirst, allメソッド)にチェインするようにすればよい。
#利用例 student = Student.search_ana.first student = Student.search_name("Ana").first
さらに、Rails2.3では、動的ファインダfind_by_xxxに対応する動的スコープscoped_by_xxxが定義されているので、上記の場合は宣言無しに
student = Student.scoped_by_name("Ana").first
と書くこともできて便利だ。
named_scopeのメリット
個人的には、named_scopeを使うメリットは2つあると思う。
- 複数のnamed_scopeをチェインできること。名前だけでなく学年でも検索したいとき、findでは
students = Student.find(:all, :conditions=>{:name=>"Ana"}).find(:all, :conditions=>{:grade=>2})
とは書けない。なぜなら、find文はARのクラスメソッドだが、その返値はARインスタンス(first)またはその配列(all)だからだ。そのため、conditions節を追記するか、複数条件を動的ファインダで表現するfind_by_xxx_and_yyyを利用して、
students = Student.find(:all, :conditions=>{:name=>"Ana", :grade=>2}) students = Student.find_by_name_and_grade("Ana", 2)
と書かないといけない。
同じことをnamed_scopeでやると、
class Student named_scope :search_name_and_grade, lambda{|n, g| :name=>s, :grade=>g} .... students = Student.search_name_and_grade("Ana", 2).all # 2.3 以降 students = Student.scoped_by_name_and_grade("Ana", 2).all
としてもいいし、二つの条件をわけてnamed_scopeとして定義しておき、
class Student named_scope :search_name, lambda{|n| :conditions=>{:name=>n}} named_scope :search_grade, lambda{|g| :conditions=>{:grade=>g}} .... students = Student.search_name("Ana").search_grade(2).all # 2.3 以降 students = Student.scoped_by_name("Ana").scoped_by_grade(2).all
と書くこともできる。さらに絞り込みたければ、他のnamed_scopeあるいはfind文をチェインしていけばよい。
named_scopeを使うと、複雑な条件を単純な条件のチェインで表現できるので、ここでも検索条件の再利用効率とメンテナンス性をあげられるだろう。またnamed_scopeをチェインした場合でも、SQL呼び出しは全体で1回になるのでパフォーマンス上も安心だ。
Searchlogic v2
Searchlogic v2の概要
ようやくSearchlogic v2の話に移れる。新しくなったSearchlogicは、一言で言えばnamed_scopeを大幅に拡張するプラグインで、様々な検索条件を動的スコープよりも簡潔かつ柔軟に書けるようになっている。
上記のname="Ana"という検索条件をSearchlogic風に書いてみる。
Student.name_equals("Ana").all
これは、Rails2.3の動的スコープのscoped_by_xxxがxxx_equalsに変わっただけだが、メソッド名が「フィールド名(name)+演算子(equals_to)」となっていることに注意が必要だ。実は、この構造がSearchlogic v2のミソになっている。ARの動的ファインダや動的スコープは、引数が完全一致する時しか使えなかったが、Searchlogicでは演算子の部分を変えることで、それ以外の条件も記述できる。たとえば:
Student.name_does_not_equal("Ana") #否定 Student.name_begins_with("A") #前方一致 Student.name_ends_with("a") #後方一致 Student.name_like("n") #部分一致 Student.name_null
といった具合だ。同様に、数値であればgrade_greater_thanなどが使える(くわしくはドキュメントを参照)。
Searchlogic v2が返すのはnamed_scopeなので、使い方は普通のnamed_scopeと変わりなく、以下のようにすればよい。
student = Student.name_equals("Ana").first
Searchlogic v1とv2
Searchlogic v1とv2は、「フィールド名+演算子」の形で検索条件が簡潔に書けること、検索条件自体を変数として保持できることは共通している。ただし、v1では検索条件変数のクラスがオリジナルなSearchlogic::Cache::[モデル名]Searchであるのに対し、v2ではARデフォルトのnamed_scopeを使っている。これにより、役割の重複したクラスを作成することによりコード量を減らし、他のプラグインやARとの親和性も高めることができた。
また、Searchlogic v1は、様々な機能を実装したオールインワンなプラグインだったが、v2ではページネーションなどAR本体あるいは他のプラグインでカバーできる部分が削除された。これにより、プラグインの方向性がより明確になり、機能を把握しやすくなったと思う。
これらの方針で完全に書き直した結果、Searchlogic v2はv1とは完全に別物になり、コードは2300行から400行に減ったという。確かに出来ることは減ったが、以上のような理由で使い勝手はむしろ向上したように私は感じる。
少し複雑な例
Searchlogic v2では、このほか、AND/ORによる複数の条件、並び替え、関連テーブルの値の参照も動的スコープとして定義できる。ざっと例をあげてみる。
#OR条件は_anyプレフィックスを末尾につける #:conditions=>["name Like '%?%' OR name LIKE '%?%'", 'a', 'b']と同義 students = Students.name_like_any("a", "b") #AND条件は_allプレフィックスを末尾につける #:conditions=>["name Like '%?%' AND name LIKE '%?%'", 'a', 'b']と同義 students = Students.name_like_all("a", "b") #ソート条件はascend_by_xxx, descend_by_xxxで students = Students.ascend_by_name.all #関連テーブル名+フィールド名で関連テーブルの値を参照 # Student has_one :club # Club has_many :students とする students = Student.club_name_equals("basketball")
これらの記法を使うと、「名前がa, bのどちらかを含み、バスケットボール部に入っている生徒を名前順に出力する」という検索条件は、
students = Student.name_like_any("a", "b").club_name_equals("basketball").ascend_by_name.all
と書ける。同じものをAR#findのオプションで書くと以下のようになり、SQLで直に書いた方がマシに思えてしまう。
students = Student.all(:conditions=>["(students.name LIKE ? OR students.name LIKE ?) AND clubs.name=?", "%a%", "%b%", "basketball"], :order=>"students.name ASC", :include=>:club)
Searchlogic v2を使うことで、かなり複雑な条件を簡潔に書くことができる。
Searchlogic v2は関連テーブル情報へのアクセスも高速化できる。関連テーブルにアクセスする際に、Searchlogic v2では「関連テーブル名+フィールド名」を、findでは:includeオプションを用いるが、前者は後者よりも5倍程度高速だという(公式ドキュメント参照)。
searchメソッド
Searchlogicには、上記のように動的スコープ風の「フィールド名+演算子」という形とは別に、findのように検索条件をハッシュの引数として指定するsearchメソッドが定義されている。これを使うと、
students = Student.search(:name_equals=>"Ana").all students = Student.search(:name=>"Ana").all students = Student.search(:name_like=>"n").all
のように、Searchlogic v2の記法である「フィールド名+演算子」のハッシュでnamed_scopeを生成できる。2番目の例のように「_equals」は省略しても構わない。もちろん、返値はnamed_scopeなので、その結果は他のnamed_scopeやfindメソッドにチェインできる。
Searchlogicで記述できないこと
もちろん、Searchlogic v2では書けない条件もある。たとえば、複数のフィールドのOR条件、たとえば「名前がAnaか、学年が2(name = 'Ana' OR grade = 2)」といった条件は書くことが出来ない。また、SQLで、列記した値に含まれる場合(たとえば、grade in (1,2)など)も書けないが、これについてはticketがあって目下議論が進められているようだ(Issues - binarylogic/searchlogic - GitHub)。
とりあえず、今日はこの辺までで。