LoginSignup
42
42

More than 5 years have passed since last update.

Rails 5 速習会

Last updated at Posted at 2016-07-23

本記事は、Wantedly Rails 5 速習会の資料として作成されたものです。
同時にこちらの資料とコードも参照してください。

対象

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

速習会のゴール

Rails5の新機能や変更点を学び、実際に触れるようになる。

Rails 5

Ruby 2.2.2+ required

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

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

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

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

Action Cable

  • WebSocketを扱うフレームワーク
  • 開発環境のサーバーがWEBrickからpumaへ
    • Rack socket hijacking API1への対応
    • アプリケーションと同一プロセスで処理(もちろん本番環境で別にできる)
  • クライアント、サーバーサイドの両方のフレームワークを提供
  • 公式のサンプルアプリケーション

API mode

  • RailsでJSONを返すAPIとして特化
  • 不要なgemをgemfileから省いている
  • 不要なRack middlewareを省いている
  • ApplicationControllerActionController::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: リクエストパラメータ_methodPUTとかすると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 scaffoldgenerate 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
  • 特に初学者の混乱を防ぐため

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

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

attributeで型を指定する。

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

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

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

cast type

  • DB側の型とAR側の型を変換しているのがActiveModel::Type::Valueを継承した以下のクラスたち
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を呼んでいます。

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

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

ApplicationRecord

  • モデルの継承元がActiveRecord::BaseでなくApplicationRecord
  • 全体で共有したいロジックをまとめられるように
  • ActiveRecord::Baseにモンキーパッチするのではなく一階層挟む形に
app/models/book.rb
class Book < ApplicationRecord
end
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のメソッドがつかえる!
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_torequiredオプションが追加されましたが、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となります。

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>

実装

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

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_weekdayDate, Datetimeに追加4
  • ActionMailer#deliverおよび#deliver!

まとめ

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

参考

42
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
42