form object を触っていた時に不思議な挙動に出会いました。
周知の通り、入力値を validation で弾いた時は、DB は更新されません。
しかし、 association だけは更新されてしまいます。
普段は、#update がトランザクションの中で [#assign_attributes, #valid, #save] の更新処理を一括で担ってくれていたことに気づかされました。
もし、#update を使用しない場合の解決策としては、rails の issueにあるように個別にトランザクションを張ると解決されるでしょう。
- 開発環境
- Ruby: 2.4.9
- Rails: 5.1.2
- references
- note: 参考として現在は非推奨の解決法
- note: form object とは?
【本文】
□ 前提条件
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
# ...