新手问题 有关 has_one 的一个问题

zyjisdog · 2018年04月21日 · 最后由 theblock24block 回复于 2018年04月22日 · 2511 次阅读

有两个 model 为 has_one 和 belongs_to 关系:

class User < ApplicationRecord
  has_one :second_review, dependent: :destroy
  ...
end
class SecondReview < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true, uniqueness: true  
  ...
end

现在使用user.create_second_review!来创建一条关联记录,如果user.second_review已经存在的话,数据库会报错,然后删除旧的记录,但是不会插入新记录:

2.4.0 :036 > user.create_second_review!
   (0.2ms)  BEGIN
  SecondReview Exists (0.3ms)  SELECT  1 AS one FROM `second_reviews` WHERE `second_reviews`.`user_id` = 2 LIMIT 1
   (0.1ms)  ROLLBACK
   (0.1ms)  BEGIN
  SQL (0.2ms)  DELETE FROM `second_reviews` WHERE `second_reviews`.`id` = 12
   (2.7ms)  COMMIT
ActiveRecord::RecordInvalid: 验证失败: User已经被使用
        from (irb):36

如果用 user.build_second_review.save!,同样会删除旧的记录,但是会插入新的记录:

2.4.0 :038 > user.build_second_review.save!
   (0.2ms)  BEGIN
  SQL (0.3ms)  DELETE FROM `second_reviews` WHERE `second_reviews`.`id` = 13
   (2.8ms)  COMMIT
   (0.2ms)  BEGIN
  SecondReview Exists (0.4ms)  SELECT  1 AS one FROM `second_reviews` WHERE `second_reviews`.`user_id` = 2 LIMIT 1
  SQL (0.3ms)  INSERT INTO `second_reviews` (`user_id`, `created_at`, `updated_at`) VALUES (2, '2018-04-21 08:41:17', '2018-04-21 08:41:17')
   (2.1ms)  COMMIT
 => true

如果在 model 中去掉dependent: :destroy,则不论是create还是build.save都不会有这种问题:

2.4.0 :003 > user.create_second_review!
   (0.3ms)  BEGIN
  SecondReview Exists (0.4ms)  SELECT  1 AS one FROM `second_reviews` WHERE `second_reviews`.`user_id` = 2 LIMIT 1
   (0.1ms)  ROLLBACK
   (0.1ms)  BEGIN
  SecondReview Exists (0.4ms)  SELECT  1 AS one FROM `second_reviews` WHERE `second_reviews`.`user_id` IS NULL AND (`second_reviews`.`id` != 22) LIMIT 1
   (0.3ms)  ROLLBACK
ActiveRecord::RecordNotSaved: Failed to remove the existing associated second_review. The record failed to save after its foreign key was set to nil.
        from (irb):3
2.4.0 :004 > user.build_second_review.save!
   (0.4ms)  BEGIN
  SecondReview Exists (0.4ms)  SELECT  1 AS one FROM `second_reviews` WHERE `second_reviews`.`user_id` IS NULL AND (`second_reviews`.`id` != 22) LIMIT 1
   (0.1ms)  ROLLBACK
ActiveRecord::RecordNotSaved: Failed to remove the existing associated second_review. The record failed to save after its foreign key was set to nil.
        from (irb):4

我的问题是: 1、既然 SecondReview 类里已经有 validation,为什么user_id相同的时候依然会执行数据库操作(删掉旧记录)? 2、为什么create_second_review!方法在删除旧的记录后不会插入新记录?

对于这个问题,如果 Rails 的机制就是这样的,是不是我就只能写诸如@user.create_second_review! if [email protected]_review 这样丑陋的代码?😂

有人跟你一样提出了相同的疑问 rails/issues/20514

但是 Rails 的官方人员认为这并不是一个 bug。。

如果要用@user.create_second_review!,就把 validation 给去掉。或者用SecondReview.create(user: user)根据业务逻辑自己写

hw676018683 回复

如果去掉 validation 的话,如何确保User只有一条second_review呢?

create_second_review!是先 save(因为你加了 validate,会 save 不了),然后再替换(删除原关联)

build_second_review.save!是先替换(在 build 这一步),然后再 save(在save!这一步)

源码分别是 activerecord-5.1.2/lib/active_record/associations/has_one_association.rb 里的_create_recordbuild

谢谢!那么对于这个问题,代码一般应该怎么写?

zyjisdog 回复

这样吧

SecondReview.new(user: user).save!

但难免团队里有人用回你说的那种,然后就悲剧了

zyjisdog 关闭了讨论。 05月06日 14:58
需要 登录 后方可回复, 如果你还没有账号请 注册新账号