本記事は、Wantedly Rails 5 速習会の資料として作成されたものです。
同時にこちらの資料とコードも参照してください。
対象
現在Rails4を使用しており、Rails5の新機能や変更点が知りたい方向け。
速習会のゴール
Rails5の新機能や変更点を学び、実際に触れるようになる。
Rails 5
Ruby 2.2.2+ required
Rails5では2.2.2またはそれより新しいバージョンのRubyが求められます。
if RUBY_VERSION < '2.2.2' && RUBY_ENGINE == 'ruby'
desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})"
abort <<-end_message
Rails 5 requires Ruby 2.2.2 or newer.
You're running
#{desc}
Please upgrade to Ruby 2.2.2 or newer to continue.
end_message
end
理由としては以下の2点です。
- SymbolがGCできるようになったため
- Incremental GCによるメモリ消費削減をしたい
Action Cable
- WebSocketを扱うフレームワーク
- 開発環境のサーバーがWEBrickからpumaへ
- Rack socket hijacking API1への対応
- アプリケーションと同一プロセスで処理(もちろん本番環境で別にできる)
- クライアント、サーバーサイドの両方のフレームワークを提供
- 公式のサンプルアプリケーション
API mode
- RailsでJSONを返すAPIとして特化
- 不要なgemをgemfileから省いている
- 不要なRack middlewareを省いている
-
ApplicationController
がActionController::Base
ではなくActionController::API
を継承- 不要なモジュールを省いている2
- Generatorでviewやhelperを生成しない
- スループット(Request/sec)でいうと5%くらいの改善みたい3なので速くなるといってもそれくらい
不要なgemをgemfileから省いている
フロントエンドで必要な以下のgemは不要なので省かれています。
- sass-rails
- uglier
- coffee-rails
- therubyracer
- query-rails
- turbolinks
- web-console
不要なRack middlewaureを省いている
-
Rack::MethodOverride
: リクエストパラメータ_method
にPUT
とかするとputできるようにしてくれる -
Sprockets::Rails::QuietAssets
: アセットへのリクエストログを省いている -
WebConsole::Middleware
: 標準のデバッグツール -
ActionDispatch::Cookies
: クッキー管理 -
ActionDispatch::Session::CookieStore
: クッキーセッション管理 -
ActionDispatch::Flash
: フラッシュメッセージ(flash[:error]
とか)を扱う
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
-use Rack::MethodOverride
use ActionDispatch::RequestId
-use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
-use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
-use ActionDispatch::Cookies
-use ActionDispatch::Session::CookieStore
-use ActionDispatch::Flash
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
Generatorでviewやhelperを生成しない
generate scaffold
やgenerate controller
でルーティングやモデル、コントローラ、コントローラテストが生成されており、viewやhelperが生成されていないのが確認できます。
$ rails g scaffold Project title:string started_at:datetime
Running via Spring preloader in process 95950
invoke active_record
create db/migrate/20160719152731_create_projects.rb
create app/models/project.rb
invoke test_unit
create test/models/project_test.rb
create test/fixtures/projects.yml
invoke resource_route
route resources :projects
invoke scaffold_controller
create app/controllers/projects_controller.rb
invoke test_unit
create test/controllers/projects_controller_test.rb
$ rails g controller issues index show
Running via Spring preloader in process 97967
create app/controllers/issues_controller.rb
route get 'issues/show'
route get 'issues/index'
invoke test_unit
create test/controllers/issues_controller_test.rb
railsコマンドへの統一
- rakeコマンドがrailsコマンドに統一されます
- e.g.)
rake db:migrate
=>rails db:migrate
- e.g.)
- 特に初学者の混乱を防ぐため
ActiveRecord Attributes API
- DBの型とActiveRecord側の型との対応を変更できる
- DBではdecimalで扱っているがActiveRecordではIntegerとして扱いたい場合など
- ActiveRecord側でデフォルト値の設定ができる
型の変換
以下のようなbooks
テーブルおよびBook
モデルがある場合
# migration class
class CreateBooks < ActiveRecord::Migration[5.0]
def change
create_table :books do |t|
t.string :title
t.decimal :price_in_cents # <= decimal型でstoreしている
t.timestamps
end
end
end
# model
class Book < ActiveRecord::Base
end
price_in_cents
はBigDecimal
クラスのインスタンスが返ってくる。
book = Book.new(title: 'Hello Ruby', price_in_cents: 10.1)
book.price_in_cents #=> #<BigDecimal:7ffb42d7db48,'0.101E2',18(36)>
attributeで型を指定する。
class Book < ActiveRecord::Base
# attribute(name, cast_type, **options)
attribute :price_in_cents, :integer
end
そうすると、Integerで返ってくるようになります。
book = Book.new(title: 'Hello Ruby', price_in_cents: 10.1)
book.price_in_cents #=> 10
cast type
- DB側の型とAR側の型を変換しているのが
ActiveModel::Type::Value
を継承した以下のクラスたち
module ActiveRecord
# ...
module Type
Helpers = ActiveModel::Type::Helpers
BigInteger = ActiveModel::Type::BigInteger
Binary = ActiveModel::Type::Binary
Boolean = ActiveModel::Type::Boolean
Decimal = ActiveModel::Type::Decimal
DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale
Float = ActiveModel::Type::Float
Integer = ActiveModel::Type::Integer
String = ActiveModel::Type::String
Text = ActiveModel::Type::Text
UnsignedInteger = ActiveModel::Type::UnsignedInteger
Value = ActiveModel::Type::Value
register(:big_integer, Type::BigInteger, override: false)
register(:binary, Type::Binary, override: false)
register(:boolean, Type::Boolean, override: false)
register(:date, Type::Date, override: false)
register(:datetime, Type::DateTime, override: false)
register(:decimal, Type::Decimal, override: false)
register(:float, Type::Float, override: false)
register(:integer, Type::Integer, override: false)
register(:string, Type::String, override: false)
register(:text, Type::Text, override: false)
register(:time, Type::Time, override: false)
end
end
ActiveRecord::Type::Value
特に見るべき部分は#serialize
と#deserialize
と#cast
です。
#serialize
はAR側からDB側への変換です。
引数valueを受け取りDBで受け取れる型の値を返します。
String, Numeric, Date, Time, Symbol, true, false, nilのいずれかである必要があります。
#deserialize
はDB側からAR側への変換です。
引数valueを受け取りAR側で取り扱いたい型に変換して返します。
デフォルトでは#cast
を呼んでいるだけです。
#cast
は上の#deserialize
で呼ばれるだけでなくフォームビルダーや通常のセッターで呼ばれることもあります。つまり引数valueはstring以外の可能性もあります。
また、#cast
ではvalueがnilでない場合にcast_value
を呼んでいます。
module ActiveModel
module Type
class Value
attr_reader :precision, :scale, :limit
def initialize(precision: nil, limit: nil, scale: nil)
@precision = precision
@scale = scale
@limit = limit
end
def type # :nodoc:
end
# Converts a value from database input to the appropriate ruby type. The
# return value of this method will be returned from
# ActiveRecord::AttributeMethods::Read#read_attribute. The default
# implementation just calls Value#cast.
#
# +value+ The raw input, as provided from the database.
def deserialize(value)
cast(value)
end
# Type casts a value from user input (e.g. from a setter). This value may
# be a string from the form builder, or a ruby object passed to a setter.
# There is currently no way to differentiate between which source it came
# from.
#
# The return value of this method will be returned from
# ActiveRecord::AttributeMethods::Read#read_attribute. See also:
# Value#cast_value.
#
# +value+ The raw input, as provided to the attribute setter.
def cast(value)
cast_value(value) unless value.nil?
end
# Casts a value from the ruby type to a type that the database knows how
# to understand. The returned value from this method should be a
# +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
# +nil+.
def serialize(value)
value
end
# Type casts a value for schema dumping. This method is private, as we are
# hoping to remove it entirely.
def type_cast_for_schema(value) # :nodoc:
value.inspect
end
# These predicates are not documented, as I need to look further into
# their use, and see if they can be removed entirely.
def binary? # :nodoc:
false
end
# Determines whether a value has changed for dirty checking. +old_value+
# and +new_value+ will always be type-cast. Types should not need to
# override this method.
def changed?(old_value, new_value, _new_value_before_type_cast)
old_value != new_value
end
# Determines whether the mutable value has been modified since it was
# read. Returns +false+ by default. If your type returns an object
# which could be mutated, you should override this method. You will need
# to either:
#
# - pass +new_value+ to Value#serialize and compare it to
# +raw_old_value+
#
# or
#
# - pass +raw_old_value+ to Value#deserialize and compare it to
# +new_value+
#
# +raw_old_value+ The original value, before being passed to
# +deserialize+.
#
# +new_value+ The current value, after type casting.
def changed_in_place?(raw_old_value, new_value)
false
end
def map(value) # :nodoc:
yield value
end
def ==(other)
self.class == other.class &&
precision == other.precision &&
scale == other.scale &&
limit == other.limit
end
alias eql? ==
def hash
[self.class, precision, scale, limit].hash
end
def assert_valid_value(*)
end
private
# Convenience method for types which do not need separate type casting
# behavior for user and database inputs. Called by Value#cast for
# values except +nil+.
def cast_value(value) # :doc:
value
end
end
end
end
ActiveModel::Type::Integer
module ActiveModel
module Type
class Integer < Value # :nodoc:
include Helpers::Numeric
# Column storage size in bytes.
# 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc.
DEFAULT_LIMIT = 4
def initialize(*)
super
@range = min_value...max_value
end
def type
:integer
end
def deserialize(value)
return if value.nil?
value.to_i
end
def serialize(value)
result = cast(value)
if result
ensure_in_range(result)
end
result
end
protected
attr_reader :range
private
def cast_value(value)
case value
when true then 1
when false then 0
else
value.to_i rescue nil
end
end
def ensure_in_range(value)
unless range.cover?(value)
raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}"
end
end
def max_value
1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
end
def min_value
-max_value
end
def _limit
self.limit || DEFAULT_LIMIT
end
end
end
end
ApplicationRecord
- モデルの継承元が
ActiveRecord::Base
でなくApplicationRecord
に - 全体で共有したいロジックをまとめられるように
-
ActiveRecord::Base
にモンキーパッチするのではなく一階層挟む形に
class Book < ApplicationRecord
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
ActiveRecord::Relation#in_batches
-
#find_in_batches
ではブロックの引数がArray
-
#in_batches
ではブロックの引数がActiveRecord::Relation
- ActiveRecord::Relationのメソッドがつかえる!
User.in_batches.class #=> ActiveRecord::Batches::BatchEnumerator
User.in_batches.update_all(verified: true)
User.in_batches.each do |relation|
relation.class #=> User::ActiveRecord_Relation
relation.update_all(verified: true)
sleep 10
end
ActiveRecordの#saveにtouchオプションが追加
#save
メソッドでtouchオプションを指定するとタイムスタンプを更新しないようにすることができます。
タイムスタンプを更新したくないケースは割りとあると思いますが、より簡単にできるようになりました。
irb(main):013:0> book = Book.find(1)
Book Load (0.2ms) SELECT "books".* FROM "books" WHERE "books"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<Book id: 1, title: "foo", price_in_cents: nil, created_at: "2016-07-20 02:26:37", updated_at: "2016-07-20 02:26:37", published_at: "2016-07-20 02:26:37", author_id: nil>
irb(main):014:0> book.updated_at
=> Wed, 20 Jul 2016 02:26:37 UTC +00:00
irb(main):015:0> book.title = "new title!"
=> "new title!"
irb(main):016:0> book.save(touch: false)
(0.2ms) begin transaction
SQL (0.4ms) UPDATE "books" SET "title" = ? WHERE "books"."id" = ? [["title", "new title!"], ["id", 1]]
(10.9ms) commit transaction
=> true
#or
ついにorができます!これは見た通りの挙動をします。
irb(main):025:0> User.where(id: [1,2]).or(User.where(name: "User 0"))
User Load (1.1ms) SELECT "users".* FROM "users" WHERE ("users"."id" IN (1, 2) OR "users"."name" = 'User 0')
=> #<ActiveRecord::Relation [#<User id: 1, name: "User 0", email: "email_0@mail.com", created_at: "2016-07-18 14:13:23", updated_at: "2016-07-18 14:13:23">, #<User id: 2, name: "User 1", email: "email_1@mail.com", created_at: "2016-07-18 14:13:23", updated_at: "2016-07-18 14:13:23">]>
#left_outer_joins
と#left_joins
以下のとおりLEFT OUTER JOINができます。
irb(main):042:0> Author.left_outer_joins(:books)
Author Load (0.3ms) SELECT "authors".* FROM "authors" LEFT OUTER JOIN "books" ON "books"."author_id" = "authors"."id"
=> #<ActiveRecord::Relation [#<Author id: 1, name: "Mark", created_at: "2016-07-20 07:26:57", updated_at: "2016-07-20 07:26:57">, #<Author id: 1, name: "Mark", created_at: "2016-07-20 07:26:57", updated_at: "2016-07-20 07:26:57">]>
beforeコールバックでの明示的な停止
rails4まではbeforeコールバックでfalseを返すと停止することができました。
rails5では:abortを明示的に投げることで停止させます。
class Book < ApplicationRecord
before_save :authorize_title
private
def authorize_title
!title.include?('ng') # 停止したいケースでfalseを返す
end
end
これだとcommitされてしまうことが確認できます。
irb(main):003:0> Book.create(title: 'foo ng bar')
(0.1ms) begin transaction
SQL (1.3ms) INSERT INTO "books" ("title", "created_at", "updated_at", "published_at") VALUES (?, ?, ?, ?) [["title", "foo ng bar"], ["created_at", 2016-07-20 02:42:05 UTC], ["updated_at", 2016-07-20 02:42:05 UTC], ["published_at", 2016-07-20 02:42:05 UTC]]
(1.0ms) commit transaction
=> #<Book id: 3, title: "foo ng bar", price_in_cents: nil, created_at: "2016-07-20 02:42:05", updated_at: "2016-07-20 02:42:05", published_at: "2016-07-20 02:42:05">
そこで明示的に:abortを投げます。
class Book < ApplicationRecord
before_save :authorize_title
private
def authorize_title
throw(:abort) if title.include?('ng') # 停止したいケースでthrow(:abort)する
end
end
rollback transaction
とあり、コミットがされていないことが確認できます。
irb(main):001:0> Book.create(title: 'foo ng bar')
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> #<Book id: nil, title: "foo ng bar", price_in_cents: nil, created_at: nil, updated_at: nil, published_at: "2016-07-20 02:38:13">
belongs_toのrequiredがデフォルトでtrueに
rails 4.2.0でbelongs_to
にrequired
オプションが追加されましたが、rails5ではデフォルトでrequireはtrueになり、optionalオプションが追加されました。
この仕様はrails 5でrails newした場合でデフォルトで有効になります。
つまり以下の2つのコードは同じに意味になります。
class Project
belongs_to :company, required: true
end
class Project
belongs_to :company
end
後者で実行するとerrorが追加されてコミットできないことが確認できます。
irb(main):001:0> Book.new(title: 'hello ruby', author: nil).save
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> false
irb(main):002:0> book = Book.new(title: 'hello ruby', author: nil)
=> #<Book id: nil, title: "hello ruby", price_in_cents: nil, created_at: nil, updated_at: nil, published_at: "2016-07-20 03:23:15", author_id: nil>
irb(main):003:0> book.save
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> false
irb(main):004:0> book.errors.full_messages
=> ["Author must exist"]
またnilを許容する場合はoptionalオプションをtrueにする必要があります。
class Project
belongs_to :company, optional: true
end
以下のとおりoptionalの場合はコミットされていることが確認できます。
irb(main):011:0> book = Book.new(title: 'hello ruby', author: nil)
=> #<Book id: nil, title: "hello ruby", price_in_cents: nil, created_at: nil, updated_at: nil, published_at: "2016-07-20 03:26:36", author_id: nil>
irb(main):012:0> book.save
(0.2ms) begin transaction
SQL (1.1ms) INSERT INTO "books" ("title", "created_at", "updated_at", "published_at") VALUES (?, ?, ?, ?) [["title", "hello ruby"], ["created_at", 2016-07-20 03:26:39 UTC], ["updated_at", 2016-07-20 03:26:39 UTC], ["published_at", 2016-07-20 03:26:36 UTC]]
(4.0ms) commit transaction
=> true
irb(main):013:0> book.author
=> nil
Enumerable#pluck, #without
Enumerableモジュールにpluckとwithoutが追加されました。
#pluck
はActiveRecordではおなじみでしたが、Enumerableにも追加されました。
[{name: 'foo', point: 12}, {name: 'bar', point: 55}].pluck(:name)
# => ["foo", "bar"]
#without
は引数で渡したオブジェクトを取り除いて返します。
["apple", "chocolate", "milk", "cookie"].without("apple", "milk")
# => ["chocolate", "cookie"]
Turbolinks 5
- Turobolinksはページ間の遷移をXHR化しbodyの書き換えだけにして高速化を図る機能
- Turbolinks 5で大きく実装が書き換えられた
- railsに依存しない形になった(他の言語、他のフレームワークで使うことを想定)
- jQueryにも依存しない形になった
- iOS, Androidでも使えるようになった
Sprockets 3
プリコンパイル対象の指定方法が変更になりました。
Rails4まではconfig/initializers/assets.rb
でしたが、Rails5ではapp/assets/config/manifest.js
となります。
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
link_directory
ディレクティブ
指定したパスのディレクトリ直下のファイルすべてをプリコンパイル対象にします。
2番目の引数として許可する拡張子を指定します。.js
であればcoffee
も、.css
であればscss
も対象に入ります。
//= link_directory <path> <accepted_extension>
実装
def process_link_directory_directive(path = ".", accept = nil)
path = expand_relative_dirname(:link_directory, path)
accept = expand_accept_shorthand(accept)
link_paths(*@environment.stat_directory_with_dependencies(path), accept)
end
link_tree
ディレクティブ
link_directory
と同様ですが、こちらは再帰的です。サブティレクトリも含まれます。
//= link_tree <path> <accepted_extension>
非推奨
rails 5.1で廃止される機能です。
renderメソッドの:nothingオプションが非推奨に
何も返さない場合は以下の2通りの書き方をすることができます。
しかしnothingオプションは廃止されheadメソッドに統一されます。
# :nothingオプションを使う
render nothing: true, status: 204
# headメソッドを使う
head status: 204
理由としては以下のコードがデフォルトでテンプレートを探しに行くことがわかりづらいからです。
render status: 204
*_filter
コールバックをすべて非推奨に指定。今後は*_action
コールバックを使用
もしまだ*_filter
を使っている場合は書き換えてください。
class UsersControler
before_filter :authorize # これです
def show
end
private
def authorize
reject_if_not_authorized
end
end
class UsersControler
before_action :authorize # 書き換えるだけ
# ...
end
その他
-
protected_attributes
gemがrails 5よりサポート外に - イニシャライザを読み込み順に表示してくれる
rails initializers
コマンドが追加 - has_secure_tokenが本体にマージ
-
#on_weekend?
,#next_weekday
,#prev_weekday
がDate
,Datetime
に追加4 -
ActionMailer
の#deliver
および#deliver!
まとめ
- ActiveRecordまわりの新機能改善がたくさんあるので大いに活用していきたい
- もちろん他にもたくさんの機能があるので是非CHANGELOGを追いかけてみてください