こんにちは、Forkwell 開発チームの @sinsoku です。
本日の Riding Rails: Rails 5.0: Action Cable, API mode, and so much more で Rails 5.0.0 のリリースが告知されました。
Rails 5 ではいくつもの新機能・変更点があります。
- Action Cable
- Rails API
- Railsコマンド
- Turbolinks 5
- …
各機能の詳細については、リリース前から既にいくつも記事が出てますし、そちらを参照ください。
そんな Rails 5 のリリース直後ですが、弊社では早速 Forkwell Jobs をアップグレードして、デプロイしてみました。
#Rails 5 公開から10時間余り…皆さんのアプリはもう 5.0.0 になりましたか? Forkwell は先ほどアップグレードしました!ぜひ見ていってください、軽快に動く Rails 5 の Forkwell をっhttps://t.co/KbvgrynKkK
— Forkwell (@Forkwell_ja) 2016年7月1日
いくつかの変更は Rails 4.2.6 でも対応できますので、業務の合間などに少しずつ進めておくのがオススメです。 この記事が皆さんの Rails 5 アップグレードの参考になれば幸いです。
目次
- Rails 5 へのアップグレード前の環境
- gem の依存関係を解決する
- 設定ファイルなどの更新
- Rails Guide に従って更新
- Active Record Models Now Inherit from ApplicationRecord by Default
- Halting Callback Chains via throw(:abort)
- ActiveJob Now Inherits from ApplicationJob by Default
- Rails Controller Testing
- schema.rb の更新
- ApplicationMailer を継承するように修正
- Turbolinks v5.0.0 対応
- Progress bar の表示
- イベントの変更
- PhantomJS 1.9.2 対応
- API の仕様変更への対応
- [Error] Rails 3 時代の count(:xxx, distinct: true) を削除
- [Error] Rails 3 時代の mass-assignment の修正
- [Error] find_by(n) みたいな使い方していたので修正
- [Error] ActiveRecord::Relation で slice が使えなくなっているので to_a.slice に修正
- [Error] has_many の collection<<(object…) で select を使用したコードを修正
- [Error] content_tag_for を使わないように修正
- [Error] namespace 内の root でメソッドが生成されないので修正
- [Warning] uniq を distinct に変更
- [Warning] Controller の envを request.env に修正
- [Warning] alias_method_chain を Module#prepend に修正
- [Warning] alias_method_chain を Module#prepend に修正
- [Warning] db/migrate/*.rb で ActiveRecord::Migration[4.2] を使うように修正
- RSpec
- [Warning] HTTP request methods の引数の変更
- 調べる時間がなかった
- [Error] routes の helper メソッドの引数の挙動が変
- [Error] テストでミリ秒まで比較するとテストが落ちる
- [Error] ActionController::Parameters#slice! がエラー
- [Error] テストで使用していた params でエラー
- [Error] ActiveRecord#touch が 2回走る?
- [Warning] render :text を render :plain に修正
- あとがき
Rails 5 へのアップグレード前の環境
Ruby: 2.3.1 Rails: 4.2.6
また、他の gem は tachikoma と tachikoma_ai を使い、毎週アップデートを実施してました。
gem の依存関係を解決する
まず rails 以外の gem を可能な限りアップデートします。
$ bundle update
次に rails をアップグレードします。
$ bundle update rails
エラーが出た gem を1つずつ対応していきます。
- beta, pre などのバージョンがないか確認
gem 'rspec', '>= 3.5.0.beta3'
などで対応- GitHub の master ブランチが対応していないか確認する
gem 'sinatra', github: 'sinatra/sinatra'
のように対応- Issue や Pull Request で対応されていないか確認する
- 対応されていれば fork されたリポジトリを使用する
- ソースコード読んで自分で直す
- プルリクを投げるチャンスですね!
地道に対応すれば、そのうち bundle update
できるようになります。
設定ファイルなどの更新
rake app:update
を実行し、設定ファイルなどを更新していきます。
ただ、競合したファイルはひとまず skip しておきます。
次に別ディレクトリに rails new app5
のように 5.0.0 で新しいアプリを作成し、 skip したファイルは新しいアプリと比較して、手で直しました。
比較方法の一例として、 vim を使った方法を紹介します。
$ vim -d config/environments/development.rb ~/tmp/app5/config/environments/development.rb
変更内容はアプリによって異なるので、各自で頑張ってください。
ちなみに、弊社では ActionCable はまだ未使用のため、コメントアウトしています。
- config/application.rb
require "action_controller/railtie" require "action_mailer/railtie" require "action_view/railtie" # require "action_cable/engine" require "sprockets/railtie" # require "rails/test_unit/railtie"
Rails Guide に従って更新
upgrading-from-rails-4-2-to-rails-5-0 を見ながら、各項目の対応をしました。
Active Record Models Now Inherit from ApplicationRecord by Default
ActiveRecord::Base
を直接継承せず、 ApplicationRecord
を継承するように直しました。
$ git grep -l ActiveRecord::Base -- app/models | xargs sed -i '' "s/ActiveRecord::Base/ApplicationRecord/g"
既存ファイルの継承元を sed
で修正し、 ApplicationRecord
を新規に作成します。
# app/models/application_record.rb class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end
Halting Callback Chains via throw(:abort)
4.2 では before_save
などで false
を返すと、ROLLBACK されていました。
これが 5.0 では throw(:abort)
と明示的に例外を投げる必要があります。
class User before_save { false } before_save { p 'Hello' } # 4.2 では実行されないが、 5.2 では実行される end
git grep before_ -- app/models
などで該当箇所を調べ、 return false
を throw(:abort)
に修正しました。
ActiveJob Now Inherits from ApplicationJob by Default
ActiveJob::Base
を直接継承せず、 ApplicationJob
を使うようになりました。
$ git grep -l ActiveJob::Base -- app/models | xargs sed -i '' "s/ActiveJob::Base/ApplicationJob/g"
既存ファイルを一斉置換したあとに ApplicationJob
を新規作成します。
# app/jobs/application_job.rb class ApplicationJob < ActiveJob::Base end
Rails Controller Testing
Rails 5 からコントローラーのテストで assigns
などを使う場合は rails-controller-testing の gem が必要になりました。
gem がない状態でテストを実行すると下記のようなエラーが発生します。
Failure/Error: expect(assigns(:user)).to be_a_new(User) NoMethodError: assigns has been extracted to a gem. To continue using it, add `gem 'rails-controller-testing'` to your Gemfile.
また、 rails-controller-testing
には rails/rails-controller-testing#22 の問題があるため、 require: false
にして読み込みを遅延させる必要があります。
# Gemfile group :test do gem 'rails-controller-testing', require: false end # spec/rails_helper RSpec.configure do |config| require 'rails-controller-testing' [:controller, :view, :request].each do |type| config.include ::Rails::Controller::Testing::TestProcess, :type => type config.include ::Rails::Controller::Testing::TemplateAssertions, :type => type config.include ::Rails::Controller::Testing::Integration, :type => type end end
追記: id:maedana さんからコメントを頂きました。rspec-rails 3.5 以上なら Gemfile に追加するだけになり rails_helper.rb の変更も不要になったようです。
# Gemfile group :test do gem 'rails-controller-testing' end
schema.rb の更新
Rails 5 で schema.rb
の形式が変わっているので、更新しました。
一回 rails db:migrate
を実行すれば更新されます。
$ rails db:migrate
Rails 5 から rake db:migrate
は rails db:migrate
でも実行できるようになっています。
ApplicationMailer を継承するように修正
ApplicationRecord
, ApplicationJob
と同様に ApplicationMailer
になっていたので修正。
$ git grep -l ActionMailer::Base -- app/mailers | xargs sed -i '' "s/ActionMailer::Base/ApplicationMailer/g"
既存ファイルを一斉置換したあとに ApplicationMailer
を新規作成します。
# app/mailers/application_mailer.rb class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' end
Turbolinks v5.0.0 対応
Progress bar の表示
2.x では Turbolinks.enableProgressBar()
を実行していましたが、 5.0.0 ではメソッドが無くなり、デフォルトで ON になっています。
https://github.com/turbolinks/turbolinks/tree/v5.0.0#displaying-progress
色を変える場合は下記のような CSS を用意します。
.turbolinks-progress-bar { height: 5px; background-color: green; }
また、プログレスバーを非表示にする場合は visibility: hidden
にします。
.turbolinks-progress-bar { visibility: hidden; }
イベントの変更
turbolinks のイベントが変更されているので、対応が必要です。
https://github.com/turbolinks/turbolinks/tree/v5.0.0#full-list-of-events
- $(document).on 'ready page:load', -> + $(document).on 'turbolinks:load', ->
よくありがちな ready page:load
は turbolinks:load
に変更する必要があります。
PhantomJS 1.9.2 対応
PhantomJS 2系だと一部のテストが動かないため、 PhantomJS 1.9.2 を使っていたのですが、 PhantomJS 1.9.2 だと Turbolinks 5.0.0 は動きませんでした。
5.0.0.beta5 までは動いたので、 vender/assets/turbolinks_5_beta5.js
を置いて、バージョンを固定して対応しました。
-//= require turbolinks +//= require turbolinks_5_beta5
ただ、これだと turbolinks をアップデートできなくなるので、早めに PhantomJS をバージョンを上げたい。
API の仕様変更への対応
5.0.0 で API の挙動が色々と変わっていたので、対応しました。 一部、弊社では謎のコード・挙動にも遭遇しましたが、知見の共有として紹介します。
[Error] Rails 3 時代の count(:xxx, distinct: true) を削除
# 4.2.6 User.count(:name, distinct: true) # (0.2ms) SELECT COUNT("users"."name") FROM "users" #=> 0 # 5.0.0 User.count(:name, distinct: true) #=> ArgumentError: wrong number of arguments (given 2, expected 0..1)
上記のように、エラーが発生します。
ただ、元のコードでは DISTINCT
効いていません。もしこんなコードを見つけたら、ちゃんと仕様を確認して、修正しましょう。
弊社の場合 使われていないページ でしたので、該当箇所のコードは全て消しました。直す場合は下記の通りです。
# 5.0.0 User.distinct.count(:name) # (0.6ms) SELECT DISTINCT COUNT(DISTINCT "users"."name") FROM "users" #=> 0
[Error] Rails 3 時代の mass-assignment の修正
# 4.2.6 User.new(params[:user], as: :admin) #=> <User id: nil, name: "foo", ... # 5.0.0 User.new(params[:user], as: :admin) #=> ArgumentError: wrong number of arguments (given 2, expected 0..1)
上記のように、エラーが発生します。 Rails 4 からは Strong Parameters を使うべきなので、修正しましょう。
class UsersController < ApplicationController def create @user = User.new user_params end private def user_params params.require(:user).permit(:name) end end
[Error] find_by(n) みたいな使い方していたので修正
# 4.2.6 User.find_by(1) # User Load (0.2ms) SELECT "users".* FROM "users" WHERE (1) LIMIT 1 #=> nil # 5.0.0 User.find_by(1) # ArgumentError: Unsupported argument type: 1 (Fixnum)
上記のように、エラーが発生します。 弊社の場合、運良く動いていましたがメソッドの使い方が謎なので、仕様を確認して直しましょう。
[Error] ActiveRecord::Relation で slice が使えなくなっているので to_a.slice に修正
# 4.2.6 User.all.slice(0) # User Load (1.9ms) SELECT "users".* FROM "users" #=> #<User id: 1, ... # 5.0.0 User.all.slice(0) # User Load (1.2ms) SELECT "users".* FROM "users" #=> NoMethodError: undefined method `slice' for #<User::ActiveRecord_Relation:0x007fad46c2bcf8>
上記のように、エラーが発生します。
ActiveRecord 周りの仕様が変わったぽい。ひとまず to_a
すれば元の挙動を維持できるので、それで対応しました。
# 5.0.0 User.all.to_a.slice(0) # User Load (0.1ms) SELECT "users".* FROM "users" #=> #<User id: 1, ...
[Error] has_many の collection<<(object…) で select を使用したコードを修正
モデルの構造は下記の通り。
class User < ApplicationRecord has_many :posts, through: :bookmarks has_many :bookmarks end
# 4.2.6 u.posts << Post.select(:id) # Post Load (0.1ms) SELECT "posts"."id" FROM "posts" # (0.1ms) begin transaction # SQL (0.6ms) INSERT INTO "bookmarks" ("user_id", "post_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["user_id", 1], ["post_id", 1], ["created_at", "2016-06-12 11:49:54.391603"], ["updated_at", "2016-06-12 11:49:54.391603"]] # SQL (0.1ms) INSERT INTO "bookmarks" ("user_id", "post_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["user_id", 1], ["post_id", 2], ["created_at", "2016-06-12 11:49:54.401580"], ["updated_at", "2016-06-12 11:49:54.401580"]] # (0.9ms) commit transaction # Post Load (0.2ms) SELECT "posts".* FROM "posts" INNER JOIN "bookmarks" ON "posts"."id" = "bookmarks"."post_id" WHERE "bookmarks"."user_id" = ? [["user_id", 1]] #=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 1>, #<Post id: 2>]> # 5.0.0 u.posts << Post.select(:id) # Post Load (0.1ms) SELECT "posts"."id" FROM "posts" # (0.1ms) begin transaction # (0.1ms) rollback transaction #=> ActiveModel::MissingAttributeError: missing attribute: user_id
上記のように、エラーが発生します。
ただ、そもそも元のコードで N+1 が起きていたので、弊社では zdennis/activerecord-import を使って collection<<(object...)
を使わない形に修正しました。
[Error] content_tag_for を使わないように修正
<!-- 4.2.6 --> <table> <!-- --> <%= content_tag_for(:tr, @user) do %> <td><%= @user.name %></td> <% end %> </table> <!-- 5.0.0 --> <table> <!-- --> <%= content_tag_for(:tr, @user) do %> <td><%= @user.name %></td> <% end %> </table> <!-- ActionView::Template::Error (The `content_tag_for` method has been removed from Rails. To continue using it, add the `record_tag_helper` gem to your Gemfile: gem 'record_tag_helper', '~> 1.0' Consult the Rails upgrade guide for details.): -->
上記のように、エラーが発生します。 メッセージに従ってrecord_tag_helperを入れても良かったのですが、使用箇所も1つでしたので使わないように変更しました。
ちなみに、関連するIssueは↓です。
I remember adding RecordTagHelper as an experiment, but never felt happy with its use in real life. Let’s kick it out in a gem and remove it from core. https://github.com/rails/rails/issues/18337
[Error] namespace 内の root でメソッドが生成されないので修正
# 4.2.6 Rails.application.routes.draw do root 'top#index' namespace :admin do root 'top#index' end end # root GET / top#index # admin_root GET /admin(.:format) admin/top#index # 5.0.0 Rails.application.routes.draw do root 'top#index' namespace :admin do root 'top#index' end end # root GET / top#index # GET /admin(.:format) admin/top#index
上記のように、helper メソッドが生成されなくなります。
as
オプションを使用して修正する必要があります。
Rails.application.routes.draw do root 'top#index' namespace :admin do root 'top#index', as: :root end end # root GET / top#index # admin_root GET /admin(.:format) admin/top#index
[Warning] uniq を distinct に変更
# 4.2.6 User.uniq # User Load (2.9ms) SELECT DISTINCT "users".* FROM "users" #=> #<ActiveRecord::Relation [#<User id: 1, name: nil, created_at: "2016-06-05 13:39:31", updated_at: "2016-06-05 13:39:31">]> # 5.0.0 User.uniq #DEPRECATION WARNING: uniq is deprecated and will be removed from Rails 5.1 (use distinct instead). (called from irb_binding at (irb):2) # User Load (0.2ms) SELECT DISTINCT "users".* FROM "users" #=> #<ActiveRecord::Relation [#<User id: 1, name: nil, created_at: "2016-06-05 13:39:19", updated_at: "2016-06-05 13:39:19">, #<User id: 2, name: nil, created_at: "2016-06-12 11:36:46", updated_at: "2016-06-12 11:36:46">]>
上記のように、警告が発生します。 修正方法は下記の通り。
User.distinct # User Load (0.2ms) SELECT DISTINCT "users".* FROM "users" #=> #<ActiveRecord::Relation [#<User id: 1, name: nil, created_at: "2016-06-05 13:39:19", updated_at: "2016-06-05 13:39:19">, #<User id: 2, name: nil, created_at: "2016-06-12 11:36:46", updated_at: "2016-06-12 11:36:46">]>
[Warning] Controller の envを request.env に修正
# 4.2.6 env['HTTP_USER_AGENT'] #=> "Mozilla/5.0 (Macintosh; Intel Mac OS X... # 5.0.0 env['HTTP_USER_AGENT'] DEPRECATION WARNING: env is deprecated and will be removed from Rails 5.1 #=> "Mozilla/5.0 (Macintosh; Intel Mac OS X...
上記のように、警告が発生します。 修正方法は下記の通り。
request.env['HTTP_USER_AGENT'] #=> "Mozilla/5.0 (Macintosh; Intel Mac OS X...
[Warning] alias_method_chain を Module#prepend に修正
サンプルコードは下記の通り。*1
class User < ApplicationRecord def say puts 'Hello, World' end def say_with_name puts "Hi, #{name}" say_without_name end alias_method_chain :say, :name end
各バージョンで試した結果は下記の通り。
# 4.2.6 User.new(name: 'sinsoku').say #=> Hi, sinsoku #=> Hello, World # 5.0.0 User.new(name: 'sinsoku').say #=> DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. #=> Hi, sinsoku #=> Hello, World
警告メッセージに従い Module#prepend
を使う形に修正しました。
class User < ApplicationRecord def say puts 'Hello, World' end end module SayWithName def say puts "Hi, #{name}" super end end User.prepend(SayWithName)
[Warning] db/migrate/*.rb で ActiveRecord::Migration[4.2] を使うように修正
migration のサンプルコード。
class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :name t.timestamps end end end
これは 5.0.0 で警告が出る。( rails db:migrate
で 警告は出ませんでしたが、直した方が良さそうなので直しておきました)
追記: id:sue445 さんのコメントの指摘通り log/development.log
に WARNING が出ていました。
DEPRECATION WARNING: Directly inheriting from ActiveRecord::Migration is deprecated. Please specify the Rails release the migration was written for: class CreateUsers < ActiveRecord::Migration[4.2] (called from require at bin/rails:4)
これは sed で一斉置換して対応しました。
$ git grep -l ActiveRecord::Migration -- db | xargs sed -i '' "s/ActiveRecord::Migration/ActiveRecord::Migration[4.2]/g"
RSpec
[Warning] HTTP request methods の引数の変更
RSpec.describe UsersController, type: :controller do describe "GET #show" do it "assigns the requested user as @user" do user = User.create! get :show, id: user.to_param #=> ① expect(assigns(:user)).to eq(user) end end end
① の行で警告が表示されます。
DEPRECATION WARNING: ActionController::TestCase HTTP request methods will accept only keyword arguments in future Rails versions. Examples: get :show, params: { id: 1 }, session: { user_id: 1 } process :update, method: :post, params: { id: 1 }
警告メッセージに従って、メソッドの引数を修正しました。
RSpec.describe UsersController, type: :controller do describe "GET #show" do it "assigns the requested user as @user" do user = User.create! - get :show, id: user.to_param + get :show, params: { id: user.to_param } expect(assigns(:user)).to eq(user) end end end
調べる時間がなかった
4.2.6 と 5.0.0 で比較するのに予想以上に時間かかったので、以下は現象だけ紹介。
(あとで再現できたら、追記します)
[Error] routes の helper メソッドの引数の挙動が変
# /users/:user_name user_url(user_name, options)
上記のような書き方をしていた箇所がエラーになるケースがありました。
user_url(options.merge(username: user_name))
こんな感じで対応しました。
[Error] テストでミリ秒まで比較するとテストが落ちる
テストで時刻を比較しているテストが落ちていました。
it { expect(job.published_at).to eq now }
byebug
で調べてみると、どうもミリ秒の部分が異なるのが理由みたい。
弊社では秒単位の比較で十分だったので、下記のように to_i
で対応しました。
it { expect(job.published_at.to_i).to eq now.to_i }
[Error] ActionController::Parameters#slice! がエラー
params[:data].slice!
のように書いていた箇所が動かなくなっていたので修正しました。
def update - data = params[:data].slice! + data = params[:data].to_h.slice! # ... end
[Error] テストで使用していた params でエラー
テストで permit せずにアクセスしていたため、エラーになっていました。
it { expect(controller.params[:q].to_unsafe_h.symbolize_keys).to include(freeword: 'keyword') }
テストなので、 to_unsafe_h
を使って対応しました。
[Error] ActiveRecord#touch が 2回走る?
Rails の Optimistic Locking を使っているコードで ActiveRecord#touch が2回走るような謎の現象に遭遇しました。
弊社では色々とあって、楽観的ロックを使うコードがなくなり、結果として対応が不要になりました。 ただ、発生原因がよく分からなかったので、アプリで使っている人は気をつけた方が良いかも。
[Warning] render :text を render :plain に修正
DEPRECATION WARNING: `render :text` is deprecated because it does not actually render a `text/plain` response. Switch to `render plain: 'plain text'` to render as `text/plain`, `render html: '<strong>HTML</strong>'` to render as `text/html`, or `render body: 'raw'` to match the deprecated behavior and render with the default Content-Type, which is `text/plain`.
まだ対応できていない。
あとがき
Rails 5 のアップグレード、動作検証、リリース、そして長いブログを書いて、本当に疲れた。ビール飲みたい。
そんな Rails 5 にアップグレードされた Forkwell Jobs で、ぜひ Ruby 求人を検索 してみてください。 あと、テストしているので大丈夫だとは思いますが、変な挙動を見つけたらこっそり教えてください。
*1:実際は Rails やその他 gem にモンキーパッチを当てる場合に使ってました