Hatena::ブログ(Diary)

MothProgのうしろがわ このページをアンテナに追加

2010-08-31

Rails3現時点での移行覚え書き

なんとかRails3にだいたい移行できたのでその覚え書きを。

非互換の機能への対応

Rails3では、ActiveRecordの扱いを始め多くの修正が加えられたが、その一方でこれまで用いられてきた機能で廃止や非推奨になったものがたくさんある。

移行でwarningが出るケースとしては、

などがある。非推奨や廃止になった機能はチェックできるので、それを参考に全文検索して修正すれば、作業量はそこそこあるが想像ほどは大変ではなかった。

これ以外にも非互換性のためにエラーになった箇所としては、以下のようなものがあった:

デフォルトで呼ばれる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でモンキーパッチとして取り込むことにした。

# 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

参考:https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/5348-visibility-of-helpers-seems-all-wrong

その一方で、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!')とやってもうまくいかずちょっと悩んだが、Page not found · GitHub Pagesに、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)。

とりあえず、今日はこの辺までで。