3. Model に関する変更点
2013/10/03 シナジーマーケティング(株) 鈴木 圭
[Rails 4.0] 第3章 Model に関する変更点
- 3.1. attr_accessible から StrongParameters へ
- 3.2. 非推奨となった動的ファインダメソッド
- 3.3. scope には Proc オブジェクトの指定が必須
- 3.4. トランザクションの隔離レベルの指定
- 3.5. ActiveModel::Model モジュール
- 3.6. クエリ API の変更点
- 3.6.1. all が配列ではなく Relation を返す
- 3.6.2. pluck は複数カラムを指定可能
- 3.6.3. update_attributes は update のエイリアス
- 3.6.4. update_column の代わりに update_columns
- 3.6.5. none (ActiveRecord::NullRelation)
- 3.6.6. where.not で否定条件
- 3.6.7. 再代入せずに(破壊的に)条件を追加可能
- 3.6.8. unscope メソッドの追加
- 3.6.9. ActiveRecord::StatementCache
- 3.6.10. 同じ属性に対する scope の組み合わせで発生する問題が解決された
- 3.7. バリデーションの変更
- 3.7.1. validates_absence_of
- 3.7.2. validates に strict オプションが追加
- 3.7.3. ConfirmationValidator のエラーメッセージ
- 3.8. マイグレーションの変更点
- 3.8.1. drop_table, remove_column, change_table が条件付でリバーシブル
- 3.8.2. revert メソッド
- 3.8.3. reversible メソッド
- 3.8.4. create_join_table メソッド
- 3.8.5. PostgreSQL 対応の強化
- 3.9. まとめ
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: '[email protected]')
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: '[email protected]')
また、今まで 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: '[email protected]', email_confirmation: '[email protected]') user.valid? user.errors.keys # => [:email] # Rails4.0 user = User.new(email: '[email protected]', email_confirmation: '[email protected]') 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 に関する変更点を解説します。