箱のプログラミング日記。

渋谷の自社開発企業でRails書いてます。

assign_attributesなのに保存されてしまう

f:id:y_hakoiri:20191102121704j:plain Railsでのアプリケーション開発で「DBに保存したくないけど値を更新したい」時ってあると思いますが、そんな時に便利なassign_attributesで少し詰まったのでメモ。

update_attributesとassign_attributesの違い

update_attributesとassign_attributesの違いについて言及している記事は結構多いので今回は深掘りしませんが、update_attributesの場合はDBを更新してくれて、assign_attributesはDBには保存せず値のみ更新してくれるというもの。

私の場合もそうでしたが、例えば確認画面とかを実装する時に「まだ保存はしたくないけど更新された値を取得したいなー」みたいな時に使えるかと思います。(もちろんassign_attributesだけではDBへの更新がされないので、そのあとにsaveしてあげる必要があります。)

ちなみにupdateメソッドってupdate_attributesのエイリアスみたいですね。。全然別物だと思ってたわややこしい。

本題:assign_attributesを使ってるのに保存される

今回私は確認画面を実装したかったので、先ほど言及したようにassign_attributesで値を更新したかった。モデルは以下の通りです。

group.rb

class Group < ApplicationRecord
  has_many :group_users, dependent: :delete_all
  has_many :users, through: :group_users
end

user.rb

class User < ApplicationRecord
  has_many :group_users, dependent: :delete_all
  has_many :groups, through: :group_users
end

group_user.rb

class GroupUsers < ApplicationRecord
  belongs_to :group
  belongs_to :user
end

ユーザーとグループが多対多で中間テーブルが存在します。

で、グループを更新するときの確認画面でassign_attributesを使って更新された@ group_userをいったん表示した後保存したくて、コントローラーで以下のように書いたのだけど、確認画面の時点で中間テーブルにのみ保存されてしまう。なぜ。。

group_users_controller.rb

def update
 @group_user.assign_attributes(group_user_params)
 
 if @group_user.save
  redirect_to root_path notice: 'グループを更新しました'
 else
  render :edit
 end
end

原因:中間テーブルには即時で保存されてしまうらしい

調べまくったけどなかなか記事が出てこなくて、やっと出てきたこちら

参考:Railsの罠

has_many throughリレーションをassign_attributesするとその時点でinsertされる

基点となるモデルが保存済みの場合、has_many throughリレーションを代入すると即座にinsertが走る

validationしたいなどの理由によりrollbackする必要がある場合にはtrunsactionを使う必要がある

まさにRailsの罠(笑)

詳しい解説がなくて残念ですが、どうやらassign_attributesを使用した場合でも多対多のアソシエーションの中間テーブルには即時で保存されてしまうらしい。。

解決:トランザクションで処理をまとめる

色々調べたけど、保存されてしまった中間テーブルをロールバックして、さらに途中で処理が終わってしまわないようにトランザクションで囲むしか方法がなさそうだった。

group_users_controller.rb

def update
 ActiveRecord::Base.transaction do
  @group_user.assign_attributes(group_user_params)        #ここで一旦中間テーブルに保存される
  raise ActiveRecord::Rollback                            #手動でロールバック
 end
 
 if @group_user.save
  redirect_to root_path notice: 'グループを更新しました'
 else
  render :edit
 end
end

「手動で」と書いている意図ですが、

トランザクションはひとまとめにする必要のある処理が途中で終わってしまった場合に全てなかったことにするために使用するものですが、例外が起こらない限り(エラーにならない限り)自動的なロールバックはしてくれないため、今回のような場合はきちんと書いてあげなきゃいけないみたいです。

まとめ:assign_attributesは中間テーブルの場合即座にinsertされる

こういうイレギュラーな処理は解決に行きつくまでが大変な分、すごく勉強になりますね。。

特に今回は元々トランザクションを使ったことがなかったので、そちらもかなり勉強になりました。

無事に実装できてめでたし。

参考:【Rails入門説明書】transactionについて解説 | プログラミング入門ならWEBCAMP NAVI