Rails 5 速習会

本記事は、[Wantedly Rails 5 速習会](http://wantedly.connpass.com/event/35924/)の資料として作成されたものです。
同時に[こちらの資料とコード](https://github.com/ngtk/sokushu-rails5)も参照してください。

# 対象
現在Rails4を使用しており、Rails5の新機能や変更点が知りたい方向け。

# 速習会のゴール
Rails5の新機能や変更点を学び、実際に触れるようになる。

# Rails 5
- https://github.com/rails/rails
- https://github.com/rails/rails/tree/v5.0.0


# Ruby 2.2.2+ required
Rails5では2.2.2またはそれより新しいバージョンのRubyが求められます。

```rb:railties/lib/rails/ruby_version_check.rb
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
```

https://github.com/rails/rails/blob/v5.0.0/railties/lib/rails/ruby_version_check.rb

理由としては以下の2点です。

- SymbolがGCできるようになったため
- Incremental GCによるメモリ消費削減をしたい

# Action Cable
- WebSocketを扱うフレームワーク
- 開発環境のサーバーがWEBrickからpumaへ
    - [Rack socket hijacking API](https://github.com/rack/rack/pull/481)[^hijacking_api]への対応
    - アプリケーションと同一プロセスで処理(もちろん本番環境で別にできる)
- クライアント、サーバーサイドの両方のフレームワークを提供
- [公式のサンプルアプリケーション](https://github.com/rails/actioncable-examples)


[^hijacking_api]: http://old.blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/

# API mode
- RailsでJSONを返すAPIとして特化
- 不要なgemをgemfileから省いている
- 不要なRack middlewareを省いている
- `ApplicationController`が`ActionController::Base`ではなく`ActionController::API`を継承
    - 不要なモジュールを省いている[^api]
- Generatorでviewやhelperを生成しない
- スループット(Request/sec)でいうと5%くらいの改善みたい[^throughput]なので速くなるといってもそれくらい


[^api]: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/api.rb#L111-L139
[^throughput]: http://blog.willnet.in/entry/2016/03/07/103933

## 不要な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]`とか)を扱う

```diff
 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が生成されていないのが確認できます。

```sh
$ 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`
- 特に初学者の混乱を防ぐため

# ActiveRecord Attributes API
- DBの型とActiveRecord側の型との対応を変更できる
  - DBではdecimalで扱っているがActiveRecordではIntegerとして扱いたい場合など
- ActiveRecord側でデフォルト値の設定ができる


## 型の変換

以下のような`books`テーブルおよび`Book`モデルがある場合

```rb
# 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`クラスのインスタンスが返ってくる。

```rb:pry
book = Book.new(title: 'Hello Ruby', price_in_cents: 10.1)
book.price_in_cents #=> #<BigDecimal:7ffb42d7db48,'0.101E2',18(36)>
```

attributeで型を指定する。

```rb:app/models/book.rb
class Book < ActiveRecord::Base
  # attribute(name, cast_type, **options)
  attribute :price_in_cents, :integer
end
```

そうすると、Integerで返ってくるようになります。

```rb:pry
book = Book.new(title: 'Hello Ruby', price_in_cents: 10.1)
book.price_in_cents #=> 10
```

## cast type
- DB側の型とAR側の型を変換しているのが`ActiveModel::Type::Value`を継承した以下のクラスたち

```rb:activerecord/lib/active_record/type.rb
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`を呼んでいます。


```rb:activemodel/lib/active_model/type/value.rb
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
```rb:activemodel/lib/active_model/type/integer.rb
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
```


https://github.com/rails/rails/blob/v5.0.0/activerecord/lib/active_record/type.rb#L47-L70

# ApplicationRecord
- モデルの継承元が`ActiveRecord::Base`でなく`ApplicationRecord`に
- 全体で共有したいロジックをまとめられるように
- `ActiveRecord::Base`にモンキーパッチするのではなく一階層挟む形に


```rb:app/models/book.rb
class Book < ApplicationRecord
end
```

```rb:app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end
```

# ActiveRecord::Relation#in_batches
- `#find_in_batches`ではブロックの引数が`Array`
- `#in_batches`ではブロックの引数が`ActiveRecord::Relation`
- ActiveRecord::Relationのメソッドがつかえる！


```rb
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
```

https://github.com/rails/rails/pull/20933


# ActiveRecordの#saveにtouchオプションが追加
`#save`メソッドでtouchオプションを指定するとタイムスタンプを更新しないようにすることができます。
タイムスタンプを更新したくないケースは割りとあると思いますが、より簡単にできるようになりました。

```irb
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
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
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を明示的に投げることで停止させます。

```rb
class Book < ApplicationRecord
  before_save :authorize_title

  private

  def authorize_title
    !title.include?('ng') # 停止したいケースでfalseを返す
  end
end
```

これだとcommitされてしまうことが確認できます。

```rb
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を投げます。

```rb
class Book < ApplicationRecord
  before_save :authorize_title

  private

  def authorize_title
     throw(:abort) if title.include?('ng') # 停止したいケースでthrow(:abort)する
  end
end
```

`rollback transaction`とあり、コミットがされていないことが確認できます。

```rb
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`オプションが追加](https://github.com/rails/rails/pull/16056)されましたが、rails5ではデフォルトでrequireはtrueになり、optionalオプションが追加されました。
この仕様はrails 5でrails newした場合でデフォルトで有効になります。

つまり以下の2つのコードは同じに意味になります。

```rb
class Project
  belongs_to :company, required: true
end
```

```rb
class Project
  belongs_to :company
end
```

後者で実行するとerrorが追加されてコミットできないことが確認できます。

```irb
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にする必要があります。

```rb
class Project
  belongs_to :company, optional: true
end
```

以下のとおりoptionalの場合はコミットされていることが確認できます。

```irb
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にも追加されました。

```rb
[{name: 'foo', point: 12}, {name: 'bar', point: 55}].pluck(:name)
#=> ["foo", "bar"]
```

`#without`は引数で渡したオブジェクトを取り除いて返します。

```rb
["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`となります。

```js:app/assets/config/manifest.js
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
```


### `link_directory`ディレクティブ
指定したパスのディレクトリ直下のファイルすべてをプリコンパイル対象にします。
２番目の引数として許可する拡張子を指定します。`.js`であれば`coffee`も、`.css`であれば`scss`も対象に入ります。

```js
//= link_directory <path> <accepted_extension>
```


実装

```rb:lib/sprockets/directive_processor.rb
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
```
https://github.com/rails/sprockets/blob/0860f263761dec47e04569689d48a00d46c644fa/lib/sprockets/directive_processor.rb#L315-L319


### `link_tree`ディレクティブ
`link_directory`と同様ですが、こちらは再帰的です。サブティレクトリも含まれます。

```js
//= link_tree <path> <accepted_extension>
```

https://github.com/rails/sprockets#the-link_directory-directive

# 非推奨
rails 5.1で廃止される機能です。

### renderメソッドの:nothingオプションが非推奨に

何も返さない場合は以下の2通りの書き方をすることができます。
しかしnothingオプションは廃止されheadメソッドに統一されます。

```rb
# :nothingオプションを使う
render nothing: true, status: 204
# headメソッドを使う
head status: 204
```

理由としては以下のコードがデフォルトでテンプレートを探しに行くことがわかりづらいからです。

```rb
render status: 204
```


### `*_filter`コールバックをすべて非推奨に指定。今後は`*_action`コールバックを使用
もしまだ`*_filter`を使っている場合は書き換えてください。

```rb
class UsersControler
  before_filter :authorize # これです

  def show
  end

  private

  def authorize
    reject_if_not_authorized
  end
end
```

```rb
class UsersControler
  before_action :authorize # 書き換えるだけ

  # ...
end
```


# その他
- `protected_attributes` gemがrails 5よりサポート外に
- イニシャライザを読み込み順に表示してくれる`rails initializers`コマンドが追加
- [has_secure_token](https://github.com/robertomiranda/has_secure_token)が本体にマージ
- `#on_weekend?`, `#next_weekday`, `#prev_weekday`が`Date`, `Datetime`に追加[^weekend]
- `ActionMailer`の`#deliver`および`#deliver!`

[^weekend]: https://github.com/rails/rails/pull/18335

## まとめ
- ActiveRecordまわりの新機能改善がたくさんあるので大いに活用していきたい
- もちろん他にもたくさんの機能があるので是非CHANGELOGを追いかけてみてください

## 参考