目次へ

3. Model に関する変更点

2013/10/03 シナジーマーケティング(株) 鈴木 圭

3.1. attr_accessible から StrongParameters へ

Model に対する一括代入の制御を行うために使用してきた attr_accessible/attr_protected ですが、Rails4.0 では protected_attributes というライブラリに切り出されました。

Rails4.0 からは一括代入の問題に対応するために、新機能である StrongParameters を使用します。StrongParameters はコントローラのレイヤで受け付けるリクエストパラメータを制御する機能を提供します。詳しくは後の章でご紹介します。

3.2. 非推奨となった動的ファインダメソッド

Rails4.0 では find_all_by_name のような xxx_by_yyy 形式の動的ファインダメソッドが非推奨となりました。xxx_by_yyy 形式の呼び出し方は、where メソッドを使用して書き換えることができます。

以下に非推奨コードを書き換える方法を示します。

find_all_by_XXX(...)
→ where(XXX: …)

find_last_by_XXX(...)
→ where(XXX: …).last

scoped_by_XXX(...)
→ where(XXX: …)

find_or_initialize_by_XXX(...)
→ where(XXX: …).find_or_initialize

find_or_create_by_XXX(...)
→ where(XXX: …).find_or_create または find_or_create_by(XXX: …)

find_or_create_by_XXX!(...)
→ where(XXX: …).find_or_create! または find_or_create_by!(XXX: …)

xxx_by_yyy 形式の呼び出し方は、内部的には method_missing で処理されていました。いわゆる黒魔術などと呼ばれることもある手法です。しかし現在の Rails では where メソッドに置き換えることができるため、xxx_by_yyy の必要性は無くなったと言えるでしょう。

非推奨となった機能は activerecord-deprecated_finders というライブラリに切り出されています。Rails4.0 では依存関係が設定されているため xxx_by_yyy 形式の呼び出し方を行うこともできます(警告が出ます)が、Rails4.1 ではデフォルトでは使用できなくなる予定のため、新しく書くコードでは where メソッドを使う書き方に統一しましょう。

3.3. scope には Proc オブジェクトの指定が必須

Rails4.0 では scope の引数に Proc オブジェクトを指定することが必須になりました。

Rails3 までは以下のように書くことができていたものが、

scope :recent, where(‘created_at > ?’, 7.days.ago)

Rails4.0 では次のように書く必要があります。

scope :recent, lambda { where(‘created_at > ?’, 7.days.ago) }

制限が増えたわけですが、これは非常に嬉しい変更です。

なぜこのような制限が加えられたのかを理解するために、もう一度最初のコード(Rails3 までは許されていた書き方)を見てみましょう:

class User < ActiveRecord::Base

  # 7 日以内に登録された User を求める.
  scope :recent, where(‘created_at > ?’, 7.days.ago)

end

resent と名づけられた scope の条件は「 where(‘created_at > ?’, 7.days.ago) 」となっています。7 日以内に登録された User を取得したいわけですが、これでは意図通りの動作とはなりません。条件に指定している「 7.days.ago 」はクラスが読み込まれたときを基準とした「 7.days.ago 」であり、resent が使われた時点が基準ではないからです。

例えばアプリケーションが起動された日が 2013-06-25 だとすると「 User.resent 」は 2013-06-18 以降に登録された User を取得します。そしてそのまま 1 日が経過して 2013-06-26 になったとしても「 User.resent 」は 2013-06-18 以降に登録された User を取得します。これは意図した動作ではありません。このような問題はアプリケーションを 1 日以上起動し続けて始めて発覚するため、開発中は気づきにくいものです。

意図した動作にするには、次のように書き換えます。

class User < ActiveRecord::Base

  # 7 日以内に登録された User を求める.
  scope :recent, lambda { where(‘created_at > ?’, 7.days.ago) }

end

scope の引数に Proc オブジェクトを指定すると、それが呼び出された時点で Proc オブジェクトが call されるため、「 7.days.ago 」は Proc オブジェクトが呼び出された時点を基準として 7 日前となります。

Rails4.0 ではこのような問題を未然に防ぐために、scope には Proc オブジェクトを指定することが必須となりました。

3.4. トランザクションの隔離レベルの指定

データベースがサポートしていることが条件になりますが、トランザクションごとに隔離レベルを指定可能になりました。

隔離レベルは以下のように transaction メソッドのオプション isolation で指定します。

User.transaction(isolation: :serializable) do
  User.count
end

指定可能な値は次の 4 つです。

  • :read_uncommitted
  • :read_committed
  • :repeatable_read
  • :serializable

PostgreSQL を使用しているときに上記コードを実行すると、次の SQL が実行されます。

(0.1ms)  BEGIN
(0.2ms)  SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
(0.3ms)  SELECT COUNT(*) FROM "users"
(0.1ms)  COMMIT

3.5. ActiveModel::Model モジュール

Rails3 が登場したときに、モデルの機能がモジュール分割され、再利用可能となりました。

ということで分割されたモジュールを再利用してモデルの力を手に入れようとすると、まずまずのボリュームのコードを書かなければなりませんでした。モジュールごとに include すれば良いのか extend すれば良いのか、とても覚え切れません。

# Rails3
class YourModel

  extend ActiveModel::Naming
  extend ActiveModel::Translation
  include ActiveModel::Validations
  include ActiveModel::Conversion

  def initialize(params={})
    params.each do |attr, value|
      self.public_send(“#{attr}=”, value)
    end if params
  end

  def persisted?
    false
  end

end

Rails4.0 で追加された ActiveModel::Model モジュールを使うと、次のように書き換えることができます。

class YourModel

  include ActiveModel::Model

end

モデルの機能の再利用が非常に簡単に行えるようになりました。

3.6. クエリ API の変更点

3.6.1. all が配列ではなく Relation を返す

Rails4.0 では all メソッドの戻り値が配列から Relation オブジェクトに変更されました。

# Rails3
User.all.class # => Array

# Rails4.0
User.all.class # => ActiveRecord::Relation::ActiveRecord_Relation_User

明示的に配列が欲しい場合は「 User.all.to_a 」のように to_a メソッドを使用します。

3.6.2. pluck は複数カラムを指定可能

Rails4.0 では pluck メソッドに複数のカラムを指定可能になりました。

# Rails3
User.pluck(:id, :name) # => ArgumentError: wrong number of arguments (2 for 1)

# Rails4.0
User.pluck(:id, :name) # => [[1, "taro"], [2, "jiro"], ...]

pluck はモデルオブジェクトを生成せずに値だけを返してくれるため、パフォーマンスが気になるところで使うことが多いメソッドです。今までは 1 しかカラムを指定できませんでしたが、Rails4.0 からは複数のカラムを指定することができます。

3.6.3. update_attributes は update のエイリアス

update メソッドが導入され、update_attributes は update のエイリアスとなりました。
update_attributes は非推奨というわけではないため、好きな方を使用すれば良いと思います。

user = User.find(1)
user.update(name: 'taro', email: 'taro@example.com')

3.6.4. update_column の代わりに update_columns

Rails4.0 では update_columns というメソッドが追加されました。update_columns は update/update_attributes とは異なり、バリデーションやコールバックは実行せずに値の更新を行います。

user = User.find(1)

# バリデーションやコールバックは実行されない.
user.update_columns(name: 'taro', email: 'taro@example.com')

また、今まで update_column という一つのカラムの値を更新するメソッドがありましたが、こちらは非推奨となりました。

まとめると、バリデーションやコールバックを行わずにカラムの値を更新したい場合は update_columns を使用します。

3.6.5. none (ActiveRecord::NullRelation)

Rails4.0 では ActiveRecord::NullRelation というものが追加されました。

これは結果が空であることを表すリレーションで、以下のように none メソッドを使用します。

User.none

none を使用すると DB に対して SQL が発行されることなく空の配列が返されます。つまり、結果が 0 件になると分かっている場合は none を使用することで DB に対する無駄なクエリを削減することができます。特定の条件を満たす場合はデータを見せたくないという場合に none を活用すると良いでしょう。

articles = Article.where(...)

# 特定の条件を満たす場合は結果を 0 件にする.
articles = articles.none if limited?

articles.each do |article|
  ...
end

Rails3 では、同様のことを実現するために where(‘FALSE’) のような条件を追加することで対応できますが、DB に対して SQL が発行されてしまいます。

3.6.6. where.not で否定条件

Rails4.0 では「 where.not(…) 」という記法で否定条件を指定することができるようになりました。

# Rails3
User.where('name != ?', 'たろう')
# => SELECT "users".* FROM "users"  WHERE (name != 'たろう')

# Rails4
User.where.not(name: 'たろう')
# => SELECT "users".* FROM "users"  WHERE ("users"."name" != 'たろう')

3.6.7. 再代入せずに(破壊的に)条件を追加可能

Rails4.0 では where! などで破壊的に条件を追加することができます。

user = User.all
user.where!(name: 'たろう')
user.where!(status: '有効')
user.order!(:created_at)
user.limit!(777)

注意としては、破壊的に条件を追加できるのは SQL が発行される前までです。SQL が発行されオブジェクトがロードされた後に破壊的に条件を追加しようとすると ActiveRecord::ImmutableRelation という例外が発生します。

3.6.8. unscope メソッドの追加

Rails4.0 では unscope という except よりも柔軟なメソッドが追加されました。

except とは、以下のように指定した条件を取り消すことができるメソッドです。

User.where(name: 'Taro', status: 'OK')
# => SELECT "users".* FROM "users"  WHERE "users"."name" = 'Taro' AND "users"."status" = 'OK'

User.where(name: 'Taro', status: 'OK').except(:where)
# => SELECT "users".* FROM "users"

unscope は except よりも細かい粒度で条件を取り消すことができます。以下のコードを見てください。

User.where(name: 'Taro', status: 'OK').unscope(where: :name)
# => SELECT "users".* FROM "users"  WHERE "users"."status" = 'OK'

except とは異なり、「where で指定した条件のうち、name だけを取り消す」ということができます。

3.6.9. ActiveRecord::StatementCache

Rails4.0 では ActiveRecord::StatementCache というクラスが追加されました。以下のようにクエリをキャッシュすることができます。

cache = ActiveRecord::StatementCache.new do
  User.where(created_on: Date.today).where(rating: 'good').where(status: 'registered').order(:id).limit(100)
end

結果が必要な場合は execute メソッドを呼び出します。

cache.execute

内部的にはコンストラクタのブロックで指定された「User.where(name: ‘taro’)」が単純にキャッシュされ、execute を呼び出すたびに dup.to_a が呼び出されます。「User.where(created_on: Date.today).where(rating: ‘good’).where(status: ‘registered’).order(:id).limit(100)」のような条件は内部的に AST (Abstract Syntax Tree) に変換されますが、同じ条件で何度も検索する場合、AST への変換を何度も行うことは非効率です。そのような場合は ActiveRecord::StatementCache を使うことで AST への変換を 1 度だけに抑えることができます。

3.6.10. 同じ属性に対する scope の組み合わせで発生する問題が解決された

以下のコードを見てください。

class User < ActiveRecord::Base
  scope :taro, lambda { where(name: 'たろう') }
  scope :jiro, lambda { where(name: 'じろう') }
end

User というモデルがあり、taro と jiro という scope が定義されています。

ここで次のコードを実行するとどのような SQL が生成されるでしょうか。

User.taro.jiro.to_sql

Rails3 ではこのような SQL が生成されていました。

# Rails3
User.taro.jiro.to_sql
# => SELECT "users".* FROM "users"  WHERE "users"."name" = 'じろう'

よく見ると taro という scope で指定されている条件がまったく反映されていません。

Rails4.0 では次のような動作となり、それぞれの scope で指定した条件が反映されます。

# Rails4.0
User.taro.jiro.to_sql
# => SELECT "users".* FROM "users"  WHERE "users"."name" = 'たろう' AND "users"."name" = 'じろう'

ちなみに Rails3 でこの問題を回避するには、where の条件でハッシュを使わないように書き換えます。

# Rails3 での回避法
class User < ActiveRecord::Base
  scope :taro, lambda { where('name=?', 'たろう') }
  scope :jiro, lambda { where('name=?', 'じろう') }
end

User.taro.jiro.to_sql
# => SELECT "users".* FROM "users"  WHERE (name='たろう') AND (name='じろう')

3.7. バリデーションの変更

3.7.1. validates_absence_of

Rails4.0 では Object#blank? が true であることを検証する validates_absence_of (AbsenceValidator) が追加されました。

class User < ActiveRecord::Base

  validates :name, absence: true

end

3.7.2. validates に strict オプションが追加

validates メソッドに strict というオプションが追加されました。

class User < ActiveRecord::Base

  validates :name, presence: true, strict: true

end

strict に true を指定すると、検証エラーのときに例外 ActiveModel::StrictValidationFailed が raise されるようになります。

3.7.3. ConfirmationValidator のエラーメッセージ

ConfirmationValidator のエラーメッセージが設定される属性が変更されました。

Rails4.0 では「属性名_confirmation」にエラーメッセージが設定されます。

以下のコードを見てください。

class User < ActiveRecord::Base

  attr_accessor :email_confirmation
  validates :email, confirmation: true

end

Rails3 と Rails4.0 の違いは以下の通りです。

# Rails3
user = User.new(email: 'taro@example.com', email_confirmation: 'jiro@example.com')
user.valid?
user.errors.keys # => [:email]

# Rails4.0
user = User.new(email: 'taro@example.com', email_confirmation: 'jiro@example.com')
user.valid?
user.errors.keys # => [:email_confirmation]

3.8. マイグレーションの変更点

3.8.1. drop_table, remove_column, change_table が条件付きでリバーシブル

Rails4.0 では drop_table, remove_column, change_table が条件付きでリバーシブルになりました。

drop_table と remove_column については削除するテーブルやカラムの情報を指定すること、change_table についてはブロック内で remove などのリバーシブルではないメソッドを使用しないことが条件となります。

例として以下のコードを見てください。

class DropUsers < ActiveRecord::Migration

  def change
    drop_table :users
  end

end

これは users テーブルを削除するためのマイグレーションですが、このままではリバーシブルにはなりません。次のように削除するテーブルのカラムの情報も指定しておくことでリバーシブルになります。

class DropUsers < ActiveRecord::Migration

  def change
    drop_table :users do |table|
      table.string :name
      table.string :email
    end
  end

end

remove_column も同様に、削除するカラムの情報を指定しておくことでリバーシブルとなります。また、remove_column で複数のカラムをまとめて削除する場合はリバーシブルにすることはできません。

Rails4.0 では remove_columns という複数カラムをまとめて削除するメソッドも追加されています。リバーシブルにする場合は remove_column、そうではない場合は remove_columns という具合に使い分けると良いでしょう。

3.8.2. revert メソッド

マイグレーションを元に戻す revert メソッドが追加されました。revert メソッドにはブロックを指定するか、引数にマイグレーションのクラスを指定します。

以下のようにブロックを指定した場合は、その中で行われる処理 (add_column) が取り消されます。つまり、users テーブルから status カラムが削除されます。

class RemoveStatusFromUsers < ActiveRecord::Migration

  def change
    revert do
      add_column :users, :status, :integer
    end
  end

end

引数にマイグレーションクラスを指定する場合は次のようになります。

require_relative '20130625131127_add_status_to_users'

class RemoveStatusFromUsers < ActiveRecord::Migration

  def change
    revert AddStatusToUsers
  end

end

3.8.3. reversible メソッド

change メソッドの中でマイグレーションの up と down それぞれの処理を細かく制御するための reversible メソッドが追加されました。

以下のように使用します。

class AddFullNameToUsers < ActiveRecord::Migration

  def change
    add_column :users, :full_name, :string
    User.reset_column_information

    reversible do |direction|
      direction.up do
        User.find_each do |user|
          user.full_name = [user.family_name, user.given_name].join(' ')
          user.save!
        end
      end

      direction.down do
        User.find_each do |user|
          user.family_name, user.given_name = user.full_name.split(' ')
          user.save!
        end
      end
    end

    revert do
      add_column :users, :family_name, :string
      add_column :users, :given_name, :string
    end
  end

end

参考までに上記マイグレーションを up/down メソッドを分けて書くと次のようになります。

class AddFullNameToUsers < ActiveRecord::Migration

  def up
    add_column :users, :full_name, :string
    User.reset_column_information

    User.find_each do |user|
      user.full_name = [user.family_name, user.given_name].join(' ')
      user.save!
    end

    remove_column :users, :family_name
    remove_column :users, :given_name
  end

  def down
    add_column :users, :family_name, :string
    add_column :users, :given_name, :string
    User.reset_column_information

    User.find_each do |user|
      user.family_name, user.given_name = user.full_name.split(' ')
      user.save!
    end

    remove_column :users, :full_name
  end

end

元々マイグレーションに change メソッドが追加された理由を振り返ると、簡単なマイグレーションコードであれば down メソッドを書かずに自動的にリバーシブルになってほしい、という要求を満たすために生まれたものです。そのため、change メソッドが複雑になりすぎてしまっては本末転倒です。マイグレーションコードの複雑さを考慮のうえ、change メソッドの中で reversible メソッドなどを使うのか、それとも up/down メソッドに分けるのか判断すると良いでしょう。

3.8.4. create_join_table メソッド

多対多 (HABTM: Has And Belongs To Many) の中間テーブルを作成するための create_join_table というメソッドが追加されました。

例えば「ユーザ (users)」と「習い事 (lessons)」があるとして、その中間テーブルを作成する場合は、次のように create_join_table を使います。

create_join_table :users, :lessons

これを実行すると users_lessons というテーブルが作成されます。

3.8.5. PostgreSQL 対応の強化

Rails4.0 では PostgreSQL の対応も強化されました。

  • 配列型、範囲型、UUID 型がサポートされた
  • HSTORE 型がサポートされた(PostgreSQL 側で HSTORE のモジュールを導入する必要あり)
  • INET 型、CIDR 型が IPAddr にマッピングされるようになった
  • JSON 型に対して自動的にエンコード/デコードされるようになった
  • 部分インデックスがサポートされた

具体的な使い方は以下のコードをご覧いただければ分かりやすいと思います。

# マイグレーション.
class CreateUsers < ActiveRecord::Migration

  def change
    create_table :users do |table|
      table.string :name
      table.string :email

      # INET 型のサポート.
      table.inet :ip

      # JSON 型のサポート.
      table.json :settings_json

      # UUID 型のサポート.
      table.uuid :uuid

      # 配列型のサポート.
      table.string :favorites, array: true

      # 範囲型のサポート.
      table.daterange :valid_term

      table.timestamp :deleted_at
    end

    # 部分インデックスのサポート.
    add_index :users, :email, where: 'deleted_at IS NULL'
  end

end

各データ型を扱うコードは次のようになります。

user = User.new

# INET 型のサポート.
user.ip = IPAddr.new('127.0.0.1')

# JSON 型のサポート.
user.settings_json = {experimental: true, professional: false}

# UUID 型のサポート.
user.uuid = '67c00a4a-1e17-11e3-8fd9-001ec97d2e19'

# 配列型のサポート.
user.favorites = %w(野球 サッカー)

# 範囲型のサポート.
user.valid_term = Date.parse('2013-02-24') .. Date.parse('2013-06-25')

PostgreSQL の機能を使い込む場合は使用を検討すると良いでしょう。

データベースの種類に依存せずに動作するアプリケーションにしたい場合は、これらの機能は使用しないようにしましょう。

3.9. まとめ

Model に関する機能は使用頻度が高いので、きちんと変更内容を把握した上で活用したいですね。

次回は View に関する変更点を解説します。

↑このページの先頭へ

こちらもチェック!

PR
  • XMLDB.jp
  • シナジーマーケティング研究開発グループブログ