Tech Racho エンジニアの「?」を「!」に。
  • 開発

Ruby on Rails 4.0.1リリース!大量のバグ修正、3系からの移行も少し簡単になりました

本日、Ruby on Rails 4.0の最初のアップデートである、Rails 4.0.1がリリースされました。

6月に4.0がリリースされてから約4ヶ月、かなり久しぶりのアップデートとなりますが、どのような変更が含まれているのでしょうか。

まとめ

量が多いので最初にまとめです。

今回のリリースは大量のdiffを含みますが、そのほとんどは細かいバグ修正です。
Railsのリリース時期的には3.2.13 → Rails 4.0.0 → 3.2.14 → 3.2.15 → Rails 4.0.1となるため、3.2.14以降で適用されていた細かい修正も取り込まれています。4系が3系にようやく追いつきました。

4系はなんかデグレっぽいバグや非互換が多いなーと思っていた箇所が、ほぼ解消されいる印象です。
現在、有名プロジェクトで4系への移行があまり進んでいないように感じますが、Rails 4.0.1のリリースが移行の引き金になるかもしれません。

重要な変更点

orderの仕様変更

ActiveRecordで、order()を複数回呼んだときの優先順位が逆になりました。先に呼んだ方が優先されるようになります。
もともとRails 4.0.0では3系の逆になっていたので、元に戻ったことになります。

User.order("name asc").order("created_at desc")

# Rails 4.0.0
# SELECT * FROM users ORDER BY created_at desc, name asc

# Rails 4.0.1
# SELECT * FROM users ORDER BY name asc, created_at desc

非常に影響範囲が大きいので、注意が必要です。gemの対応も確認する必要がありそうです。
逆に、3系からのアップデートをこれから行う人には朗報ですね。

その他、パフォーマンス改善が含まれています。

ActionMailer

defaultはProcの場合のみcallされるようになった

ActionMailerでは、以下のようにしてdefault設定ができますが、設定値にはProcを渡すこともできます。

class MyMailer < ActionMailer::Base
  default from: 'no-reply@example.com',
          subject: -> { "MAIL #{Time.now}" }
end

Rails 4.0.0では、設定値がProcでなくても、to_procを受け取る場合はto_procしてProcとして扱っていました。

しかしこの場合、たとえばArrayにto_procを定義した場合、ActionMailerのdefaultに配列を指定できなくなってしまいます。
Symbol#to_procが存在するのはもちろんのこと、ArrayやHashにto_procを定義するテクニックは便利なので、割と使われています。

default指定するときには上記サンプルコードのように素直にProcを渡せば良いため、Rails 4.0.1ではto_procによる変換は行われなくなりました。

ActionPack

redirect時にSCRIPT_NAMEが考慮されるようになった

Mountable Engine内でredirectを使い、そのEngineをアプリからサブディレクトリにマウントした場合、Rails 4.0.0ではエラーになっていました。
Rails 4.0.1ではこれが修正されています。

サンプルプログラムとしては以下のようなものです。

# mountable engine作成
rails plugin new myblog --mountable

# myblog/config/routes.rb
Myblog::Engine.routes.draw do
  get '/admin' => redirect('admin/dashboard')
end

# myblog/test/dummy/config/routes.rb
Rails.application.routes.draw do
  mount Myblog::Engine => "/myblog"
end

# Rails 4.0.0
Started GET "/myblog/admin" for 127.0.0.1 at 2013-10-26 14:26:07 +0900
ERROR URI::InvalidURIError: the scheme http does not accept registry part: localhost:3000admin (or bad hostname

# Rails 4.0.1
Started GET "/myblog/admin" for 127.0.0.1 at 2013-10-26 14:30:41 +0900
Started GET "/myblog/admin/dashboard" for 127.0.0.1 at 2013-10-26 14:30:41 +0900

意図しないIP spoofing attackエラーが修正された

Rails 3.2.15でも修正された意図しないIpSpoofAttackErrorが発生する問題が修正されました。

StrongParametersでネストした数値キーに関するバグが修正された

StrongParametersで以下のようになるバグが修正されました。

# これは当たり前
params = ActionController::Parameters.new(user: { data: { "name" => "hello" }})
params.require(:user).permit(data: ['name'])
=> {"data"=>{"name"=>"hello"}}

# キーが数値だと、Rails 4.0.0では値がnilになる
params = ActionController::Parameters.new(user: { data: { "1" => "hello" }})
params.require(:user).permit(data: ['1'])
=> {"data"=>{"name"=>nil}} # Rails 4.0.0
=> {"data"=>{"name"=>"hello"}} # Rails 4.0.1

HTTPアクセス時にSTSヘッダが送出されないようになった

HTTPSでないHTTPのときにSTS(Strict Transport Security)ヘッダを送出するのは無意味なので、HTTPSのときのみ送出するようになりました。
当然ですが、config.force_sslがtrueのときのみの影響です。

actionにif/falseとアクション指定が両方あるときの挙動

before_actionやafter_actionには、onlyやexceptでactionを限定することができるほか、if/unlessで条件指定することができます。
このとき、Rails 4.0.0では先にif/unless条件がチェックされていましたが、先にaction条件がチェックされるようになりました。

たとえば以下のように2回に1回フィルタを実行するようなものがあったとして、Rails 4.0.0ではshowアクションでもカウンタが増えていました。Rails 4.0.1ではindexアクションでのみカウントされます。

class UsersController < ApplicationController
  before_action :myfilter, only: [:index], if: -> { ($c = $c.to_i + 1).even? }
end

※Rails 3系では大分前に対応されていたので、Rails 4.0.0のデグレが修正された形です。

エスケープ文字が含まれるときのroutingやcurrent_page?判定が修正された

URLのパス文字列をパーセントエンコーディングする際、Railsは大文字を使いますが、nginxがリバースプロキシとして動作するときは小文字で送信してきます。
そのため、Rails 4.0.0ではcurrent_page?が意図せずfalseを返したり、routingにマッチしないということがありました。
Rails 4.0.1ではこの問題が修正され、パーセントエンコーディングはデコードされた状態で比較されます。

TestRequestでREMOTE_ADDRなどをオーバーライドできるようになった

ActionDispatch::TestRequest.newにHashを渡すことで、REMOTE_ADDR, HTTP_HOST, HTTP_USER_AGENTをオーバーライドできるようになりました。

text_areaにvalue=nilを指定すると空文字で上書きされるようになった

以下のようなコードの場合、Rails 4.0.0ではform_forに渡したオブジェクトのcommentがvalueとして出力されていました。
Rails 4.0.1では、valueがnilで上書きされるようになりました。これでtext_fieldと同じ挙動になりました。

f.text_area :comment, value: nil

link_toにブロックとURLハッシュを渡したときのバグが修正された

例を見れば一目瞭然ですが、変なバグが修正されました。

link_to(action: 'bar', controller: 'foo') { content_tag(:span, 'Example site') }

# Rails 4.0.0
# => "<a action=\"bar\" controller=\"foo\"><span>Example site</span></a>"

# Rails 4.0.1
# => "<a href=\"/foo/bar\"><span>Example site</span></a>"

partialを再帰的に呼んだときのStack Level Too Deepが修正された

コメントが子コメントを持つ掲示板などで以下のようなコードを書いた場合、Rails 4.0.0ではStack Level Too Deepが発生していました。

# app/views/comments/_comment.html.erb
<%= render partial: 'comments/comment', collection: comment.children %>

Rails 4.0.1ではこの問題が修正されています。

date_fieldにvalueを渡せるようになった

以下のような形式で、date_field/datetime_field/color_fieldにvalueを設定できるようになりました。

<%= date_field 'user', 'birthday', value: Date.new(2000,10,1) %>

これは、Rails 4.0.0ではなぜか無視されていたものです。

link_to_unlessが常時エスケープされるようになった

こちらでも紹介しているRails 3.2.14の変更が適用されました。

ActiveModel

has_secure_passwordがBcryptのcostを参照するようになった

Rails 4.0.0ではhas_secure_passwordはBCrypt::Engine::DEFAULT_COSTを参照していましたが、Rails 4.0.1ではBCrypt::Engine.costを参照するようになりました。

ところで、bcrypt-rubyは仕様上costが31までしか設定できないですが、stretchingを1000回とかにしたい時は自前でやるしかないんですかね?31回ってえらく少ない気がするのですが、詳しい方いたら教えてください。

inclusion/exclusion validatorでRange指定に関するバグが修正された

inclusion/exclusionのvalidatorでは、Range指定した場合、Range#cover?が使われます。
この特性上、以下のような問題が発生していました。

class Student < ActiveRecord::Base
  # 成績はA,B,C,Dの4段階
  validates :grade, inclusion: { in: :A..:D }
end

Student.new(grade: :AB).valid?
# => true

これではよろしくないので、Range#cover?は数値Rangeの場合のみ使われるようになりました。

ActiveRecord

NullRelation#pluckが複数の引数を受け取るようになった

Rails 4ではpluckが複数カラムに対応しましたが、Rails 4.0.0ではNullRelationだけはなぜか複数カラムに非対応でした。
Rails 4.0.1では複数カラムに対応し、通常のRelationと互換性が保たれます。

# Rails 4.0.0
User.pluck(:id, :name) # => [[1, "Taro"], [2, "Jiro"]]
User.none.pluck(:id, :name) # => ArgumentError

# Rails 4.0.1
User.pluck(:id, :name) # => [[1, "Taro"], [2, "Jiro"]]
User.none.pluck(:id, :name) # => []

orderがコーテーションで囲まれるようになった

order(:id)のようにした場合、カラム名がコーテーションで囲まれるようになりました。

Post.order(:id).to_sql
# Rails 4.0.0
# => ... ORDER BY "posts".id ASC

# Rails 4.0.1
# => ... ORDER BY "posts"."id" ASC

11/3追記
はてブでご指摘頂きましたが、これが影響するのはorderの引数にシンボルを渡したときのみです。
Post.order(id: :desc)のようにすれば4.0.0でも4.0.1でもコーテーションで囲まれます。
Post.order('id')のようにすれば4.0.0でも4.0.1でも囲まれません。

whereでプレースホルダー形式を指定したときにサブクエリが生成されるようになった

以下のように、whereに配列で指定した場合にも、サブクエリが発行されるようになりました。

User.where(id: User.where(id: 1))
# =>  SELECT "users".* FROM "users" WHERE "users"."id" IN (SELECT "users"."id" FROM "users" WHERE "users"."id" = 1)

# Rails 4.0.0
User.where("id in (?)", User.where(id: 1).select(:id))
# => SELECT id FROM "users" WHERE "users"."id" = 1
# => SELECT "users".* FROM "users" WHERE (id in (1))

# Rails 4.0.1
User.where("id in (?)", User.where(id: 1).select(:id))
# => SELECT "users".* FROM "users" WHERE (id in (SELECT id FROM "users" WHERE "users"."id" = 1))

INを発行するには通常配列で指定するため、これはクエリ数が減って嬉しいですね。

joinしたときにincludesの指定が消えるバグが修正された

Rails 4.0.0には、Relationに対してincludesでjoinすると、selectで指定した値が消えるバグがありました。
to_sqlでは発生しなくて分かりづらいのですが、実行すると以下のようになります。

# Rails 4.0.0
User.select('name AS hoge').order('hoge').includes(:posts).where(posts: { id:1})
# SQLite3::SQLException: no such column: hoge:
# SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1,
# "posts"."id" AS t1_r0, "posts"."user_id" AS t1_r1 FROM "users"
# LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
# WHERE "posts"."id" = 1  ORDER BY hoge

# Rails 4.0.1
User.select('name AS hoge').order('hoge').includes(:posts).where(posts: { id:1})
=> #<ActiveRecord::Relation []>
# SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1,
# "posts"."id" AS t1_r0, "posts"."user_id" AS t1_r1, name AS hoge FROM "users"
# LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
# WHERE "posts"."id" = 1 ORDER BY hoge

NullRelationがwhere_values_hashを上書きしなくなった

Rails 4.0.0では、NullRelationがwhere_values_hashを上書きしていたため、Relationに設定されていたwhereがクリアされていました。
Rails 4.0.1ではこれが改善され、NullRelationでもwhere_values_hashが維持されるようになりました。

これは、次のような違いをもたらします。

# Rails 4.0.0
Post.where(user_id: 1).none.build
=> #<Post id: nil, body: nil, user_id: nil>

# Rails 4.0.1
Post.where(user_id: 1).none.build
=> #<Post id: nil, body: nil, user_id: 1>

Rails 4.0.1のほうが直感的な挙動ですね。

inverse_ofが設定されていてもfind([1])がArrayを返すようになった

Post.find([1,2])のようにすると、複数のデータを配列で取得できます。Post.find([1])だと当然、要素数1のArrayが取得できます。
しかしRails 4.0.0では、inverse_ofが設定されている場合に要素数が1だとArrayになりませんでした。Rails 4.0.1ではこれが修正されています。

class User < ActiveRecord::Base
  has_many :posts, inverse_of: :user
end

class Post < ActiveRecord::Base
  belongs_to :user, inverse_of: :posts
end

# Rails 4.0.0
User.first.posts.find([1]) # => #<Post id: 1>
User.first.posts.find([1,2]) # => [#<Post id: 1>, #<Post id: 2>]

# Rails 4.0.1
User.first.posts.find([1]) # => [#<Post id: 1>]
User.first.posts.find([1,2]) # => [#<Post id: 1>, #<Post id: 2>]

inverse_ofが設定されていているcallbackはメモリ上のparentを参照するようになった

こちらでも紹介しているRails 3.2.15の修正が適用され、inverse_ofが適用されているオブジェクトにcallbackを設定した場合、parentの参照はDBアクセスをしないようになりました。

特定条件でjoinsが不正なSQLを出力するバグが修正された

has_manyの第2引数にprocを渡して条件を追加し、その条件としてEQとそれ以外を組み合わせた場合、symbol指定でjoinしたときに不正なSQLが出力される問題が修正されました。

class User < ActiveRecord::Base
  has_many :hoge_posts,
           -> { where(title: 'HOGE').where('body LIKE "%HOGE%"') },
           class_name: 'Post'
end

class Post < ActiveRecord::Base
end

# Rails 4.0.0
User.joins(:hoge_posts)
# SQLite3::SQLException: syntax error
# SELECT "users".* FROM "users" INNER JOIN "posts"
# ON "posts"."user_id" = "users"."id"
# AND "posts"."title" = "HOGE", (body LIKE "%HOGE%")

# Rails 4.0.1
User.joins(:hoge_posts)
# SELECT "users".* FROM "users" INNER JOIN "posts"
# ON "posts"."user_id" = "users"."id"
# AND "posts"."title" = "HOGE" AND (body LIKE "%HOGE%")

find_in_batchesとfind_eachがlogger無しで動くようになった

find_in_batchesとfind_eachは、Rails 4.0.0ではActiveRecord::Base.loggerがnilの場合にエラーになっていましたが、Rails 4.0.1ではこれが修正されました。
(単純にログが出力されないようになりました)

has_oneでの空トランザクションが抑制された

以下のような無駄なトランザクションが発行されないようになりました。

# User has_one Accountとする

# Rails 4.0.0
User.new.account = Account.new
# BEGIN TRANSACTION
# COMMIT TRANSACTION

# Rails 4.0.1
User.new.account = Account.new
# (NO SQL)

inverse_ofなassociationでfindしたときに全件取得しなくなった

inverse_ofのassociationにfindを呼んだ場合、Rails 4.0.0では問答無用で全件取得してメモリに展開してから選択していました。
これはchildrenが多いときに壊滅的に遅くなるので、Rails 4.0.1では必要なもののみfindするようになりました。findしたものはinverse_ofなのでメモリ上に残ります。

class User < ActiveRecord::Base
  has_many :posts, inverse_of: :user
end

class Post < ActiveRecord::Base
  belongs_to :user, inverse_of: :posts
end

# Rails 4.0.0
user.posts.find(1)
# user.postsが全件取得されている!
# SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1

# Rails 4.0.1
user.posts.find(1)
# postsは1件しか取得しない
# SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 AND "posts"."id" = 1

exists?がtrue/falseを返すようになった

ActiveRecord::FinderMethods#exists?が必ずtrue/falseを返すようになりました。
Rails 4.0.0ではnilだったり1だったり色々あったのでわかりやすくなりました。

fixturesがsymlinkに対応

こちらでも紹介しているRails 3.2.15の修正が適用され、fixturesディレクトリのsymbolic linkが辿られるようになりました。

autosaveではinsertの前にdeleteが実行されるようになった

autosaveなassociationで、mark_for_destructionしている場合、INSERT文が発行される前にDELETE文が発行されるようになりました。
これにより、UNIQUE制約が付いているDBでのエラーが解消されます。

class User < ActiveRecord::Base
  has_many :posts

  # accepts_nested_attributes_forをするとautosave: trueになる
  accepts_nested_attributes_for :posts
end

Post.connection.add_index :body, unique: true

@user.posts.create(body: 'TEST')
@user.posts[0].mark_for_destruction
@user.posts.build(body: 'TEST')
@user.save!
# => Rails 4.0.0ではActiveRecord::RecordNotUniqueが発生

counter_cacheが増えないバグが修正された

4.0.0でデグレしていた箇所が修正されました。

class User < ActiveRecord::Base
  has_many: :posts
end

class Post < ActiveRecord::Base
  has_many: :user, counter_cache: 'posts_count'
end

@user.posts << Post.create(body: 'HELLO')
# Rails 4.0.0だとposts_countがインクリメントされない!

ActiveSupport

ActiveSupport::Cache::FileStore#cleanup

こちらでも紹介しているRails 3.2.15の修正が適用され、ActiveSupport::Cache::FileStore#cleanupの問題が修正されました。

ActiveSupport::Deprecation.behaviorに:raiseが追加された

以下のようにすることで、DEPRECATEDな機能を呼び出したときにActiveSupport::DeprecationExceptionをraiseさせることができるようになりました。

# config/environments/development.rb
ActiveSupport::Deprecation.behavior = :raise

テストを回すときには便利そうです。

DateTimeにusecとnsecが追加された

DateTimeにusecとnsecが追加されました。ActiveSupport::TimeWithZoneもあわせて対応します。

Time.zone = 'Tokyo'
DateTime.now.in_time_zone.usec
# => 0 (Rails 4.0.0)
# => 94805 (Rails 4.0.1)

Time.atがUTC offsetを維持するようになった

例を見るとわかりやすいと思います。

Time.zone = 'Tokyo'

# Rails 4.0.0
Time.now # => 2013-10-26 20:49:17 +0900
Time.utc(2000) # => 2000-01-01 00:00:00 UTC
Time.at(Time.utc(2000)) # => 2000-01-01 09:00:00 +0900
Time.at(Time.utc(2000)).zone # => "JST"

# Rails 4.0.1
Time.now # => 2013-10-26 20:49:17 +0900
Time.utc(2000) # => 2000-01-01 00:00:00 UTC
Time.at(Time.utc(2000) # => 2000-01-01 00:00:00 UTC
Time.at(Time.utc(2000)).zone # => "UTC"

Railties

イベント名が変更された

loggerでsubscribeするときなどに使うイベント名が、action_dispatch.requestからrequest.action_dispatchに変更されました。

config.log_levelがカスタムロガーにも対応した

config.log_level = :infoのように設定したログレベルが、標準のLoggerだけでなくカスタムで設定したLoggerにも適用されるようになりました。

skip-javascript時にgeneratorがturbolinksのパラメータを出力しなくなった

Rails 4.0.0では、rails newしたときに、app/views/layout/application.html.erbで以下のようにturbolink設定が含まれます。

<%= stylesheet_link_tag    "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>

ただし、rails new --skip-javascriptしたときにはこれは無駄なので、Rails 4.0.1ではskip-javascript時にはturbolinksのattributeが付かないようになりました。

最後に

それなりに変更点を書き出してみましたが、他にもたくさんあります。
より詳しく知りたい方はソースコードを読んでみてください。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。