2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

rails の assign_attributes で中間テーブルが保存されてしまう

Last updated at Posted at 2020-08-14
  • 入力フォームから値を受け取り、バリデーションを行ってから保存したい、みたいな時に一旦インスタンスの変数を更新するために assign_attributes を利用したら思わぬ結果になった
  • rails の中間テーブルにおいては、assign_attributes 実行時にDBインサートが走り、コミットまでされてしまう
    • そのため、modelのバリデーションエラーで保存に失敗した際に、中間テーブルが保存されっぱなしの状態になってしまった
    • 以下のように理解していたが、これは正確ではなさそう
      • update_attributes はインスタンス変数の更新を行い、DBの更新まで行う
      • assign_attributes はインスタンス変数の更新を行うが、DBの更新は行わない
  • つまり、assign_attributes してからバリデーションにかかったり、何らかの理由で保存されたくない時は明示的にロールバックするか、先にインサートしないという工夫が必要になる

ユースケース

  • 以下のようなユースケースを想定
    • UserGroup が多対多で、中間テーブルのモデルとして 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 の状態であれば、中間テーブル用のフィールドに値をセットしても自動でインサートが走ることはないので、あくまでも更新時に気をつければいいっぽい
2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?