- 入力フォームから値を受け取り、バリデーションを行ってから保存したい、みたいな時に一旦インスタンスの変数を更新するために
assign_attributes
を利用したら思わぬ結果になった - rails の中間テーブルにおいては、
assign_attributes
実行時にDBインサートが走り、コミットまでされてしまう- そのため、modelのバリデーションエラーで保存に失敗した際に、中間テーブルが保存されっぱなしの状態になってしまった
- 以下のように理解していたが、これは正確ではなさそう
-
update_attributes
はインスタンス変数の更新を行い、DBの更新まで行う -
assign_attributes
はインスタンス変数の更新を行うが、DBの更新は行わない
-
- つまり、
assign_attributes
してからバリデーションにかかったり、何らかの理由で保存されたくない時は明示的にロールバックするか、先にインサートしないという工夫が必要になる
ユースケース
- 以下のようなユースケースを想定
-
User
とGroup
が多対多で、中間テーブルのモデルとしてGroupUsers
が存在 -
User
を保存する際に、同時にGroupUsers
も保存したい - view から受け取ったパラメータをセットし保存を行うが、バリデーションエラーの時は保存せずエラーを返す
-
groups.rb
class Group < ApplicationRecord
has_many :group_users
has_many :users, through: :group_users
end
users.rb
class User < ApplicationRecord
has_many :group_users
has_many :groups, through: :group_users
validates :name, presence: true
end
group_users.rb
class GroupUsers < ApplicationRecord
belongs_to :group
belongs_to :user
end
- User に以下のフィールドが生えるが、そのフィールドに値をセットするとインサートが走ってしまう
- groups
- group_ids
- group_users
- group_user_ids
案1 transaction を張る
-
assign_attributes
からsave
までを transaction で囲んで、保存できない時はロールバックするようにする
users_controller.rb
class UsersController < ApplicationController
def update
@user = Users.new
begin
ActiveRecord::Base.transaction do
@user.assign_attributes(user_params) #ここで一旦中間テーブルに保存されるが、save!でエラーならロールバックされる
@user.save!
end
p "更新成功"
rescue
p "更新失敗"
end
end
end
案2 after_save を利用する
-
after_save
は、データベースへの COMMIT の直前に実行される- つまり、model のバリデーションなどを実行後に、そのメソッドを呼び出してくれる
- 自動で生えるリレーション用のフィールドとは別の attribute 名で view からパラメータを送り、model のバリデーション後にそのパラメータの値を保存したい中間テーブルの attribute にセットして保存することで、バリデーション通過後に中間テーブルが保存されるようにする
-
assign_attributes
実行時にはリレーションを意味するフィールドには何もセットせず、after_save
で実行するメソッドの中で、値をセットするようにする
-
users.rb
class User < ApplicationRecord
has_many :group_users
has_many :groups, through: :group_users
validates :name, presence: true
after_save :save_group_user
attr_accessor :g_ids
# User のバリデーション実行後、コミットする直前に呼ばれる
def save_group_user
self.group_ids = @g_ids
end
end
- model には
after_save
を定義し、実行するメソッドの中で中間テーブル用のgroup_ids
をセットするようにしておく - また view からきたパラメータをセットしておく用の attribute として
attr_accessor :g_ids
を定義しておく - view からは
g_ids
のように適当なフィールド名でパラメータを送り、after_save
のメソッドでgroup_ids
にセットすることで中間テーブルを保存する
users_controller.rb
class UsersController < ApplicationController
def update
@user = Users.new
@user.assign_attributes(user_params)
@user.save ? (redirect_to root_path notice: '更新成功') : (render :edit)
end
end
- コントローラーでは普通に save を実行して保存すれば良い
ちなみに
- 新規作成時など、User が id フィールドが nil の状態であれば、中間テーブル用のフィールドに値をセットしても自動でインサートが走ることはないので、あくまでも更新時に気をつければいいっぽい