LoginSignup
1
1

More than 3 years have passed since last update.

【Tips】 assign_attributes は association を更新する!?

Posted at

form object を触っていた時に不思議な挙動に出会いました。

周知の通り、入力値を validation で弾いた時は、DB は更新されません。

しかし、 association だけは更新されてしまいます。

普段は、#update がトランザクションの中で [#assign_attributes, #valid, #save] の更新処理を一括で担ってくれていたことに気づかされました。

もし、#update を使用しない場合の解決策としては、rails の issueにあるように個別にトランザクションを張ると解決されるでしょう。



【本文】

□ 前提条件

app/models/user.rb
# rails チュートリアルの user model より抜粋
class User < ApplicationRecord
  #...
  has_many :following,
    through: 'active_relationships',
     source: 'followed'
  has_many :followers,
    through: 'passive_relationships',
     source: 'follower'
  #...
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  #...
end

□ 動作結果

■ association が更新される!?

# 登録情報
> user = User.first

> user.email
=> "example@railstutorial.org"

> user.follower_ids.size
=> 38

# email が invalid となる params を与えてみる
> invalid_params = { email: "invalid", follower_ids: [] }
=> {:email=>"invalid", :follower_ids=>[]}

> user.assign_attributes(invalid_params)
  User Load (0.9ms)  SELECT `users`.* FROM `users` WHERE 1=0
  User Load (1.2ms)  SELECT `users`.* FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`followed_id` = 1
   (0.4ms)  SAVEPOINT active_record_1
  SQL (2.8ms)  DELETE FROM `relationships` WHERE `relationships`.`followed_id` = 1 AND `relationships`.`follower_id` IN (4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41)
   (1.1ms)  RELEASE SAVEPOINT active_record_1
=> {:email=>"invalid", :follower_ids=>[]}

> user.save!
   (1.0ms)  SAVEPOINT active_record_1
  User Exists (1.0ms)  SELECT  1 AS one FROM `users` WHERE LOWER(`users`.`email`) = LOWER('invalid') AND (`users`.`id` != 1) LIMIT 1
   (0.7ms)  ROLLBACK TO SAVEPOINT active_record_1
ActiveRecord::RecordInvalid: Validation failed: Email is invalid
from /usr/local/bundle/gems/activerecord-5.1.2/lib/active_record/validations.rb:78:in `raise_validation_error`

# invalid_params の割り当て前にインスタンスの状態を戻す
> user.reload
  User Load (0.6ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User:0x000055a87fa97978...

# 戻したはずなのに DB が更新されてる!?
> user.follower_ids.size
=> 0

■ 対策でトランザクションを張る

> user = User.first

> invalid_params = { email: "invalid", follower_ids: [] }

> User.transaction do
    user.assign_attributes(invalid_params)
    user.save!
  end

> user.reload

# DB の値は更新されない
> user.follower_ids.size
=> 38

■ 参考: #update のソースコード

rails/activerecord/lib/active_record/persistence.rb
#...
def update(attributes)
  # The following transaction covers any possible database side-effects of the
  # attributes assignment. For example, setting the IDs of a child collection.
  with_transaction_returning_status do
    assign_attributes(attributes)
    save
  end
end
#...
1
1
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
1
1