Rails:确保一次只有一个布尔字段设置为true

18
我有一个模型,其中包含name:stringdefault:boolean字段。我希望true值是唯一的,这样数据库中只能有一个项目被设置为true。我应该如何在控制器中设置我的更新和新建操作,以将所有其他项目的值设置为false
假设我在数据库中有以下设置。
name:string | default:boolean |  
Item1       | true            |  
Item2       | false           |  
Item3       | false           |  

如果我将Item2的默认值更改为true,我希望它循环遍历所有项目,并将其余项目设置为false,这样一次只有一个项目为true,就像这样。
name:string | default:boolean |  
Item1       | false           |  
Item2       | true            |  
Item3       | false           | 
9个回答

22

这段代码是从之前的回答中借鉴过来并稍作简化:

def falsify_all_others
  Item.where('id != ?', self.id).update_all("default = 'false'")
end
你可以在模型的before_save回调中使用这种方法。
实际上,更好的做法是只针对值为'true'的记录进行“伪造”,像这样:
Item.where('id != ? and default', self.id).update_all("default = 'false'")

更新:为了保持代码DRY,使用self.class替代Item

self.class.where('id != ? and default', self.id).update_all("default = 'false'")

3
最好使用标准的 SQL 进行 ID 比较,即 'id <> ?'。确保仅在默认值为 true 时才触发 update_all - toxaq
我使用after_save以防保存失败。 - Natus Drew
@MarianoCavallo http://apidock.com/rails/v4.0.2/ActiveRecord/Relation/update_all - 简而言之,update_all不会实例化涉及的模型,也不会触发Active Record回调或验证。 - BenKoshy

5

在你证实他人错误之前,最好先确认你所保存的信息是正确的。否则,如果你保存了一个无效的记录,就会误导所有人。

def falsify_all_others
    if self.default
        self.class.where('id != ? and default', self.id).update_all("default = 'false'")
    end
end

3
我建议您先将所有记录伪造,然后再将其变为真实记录。
add_column :users, :name ,:boolean, default: false

3

在您的控制器代码中,您可以像这样做......请注意,您可能正在使用Item2作为参数[...],因此您可以在下面进行交换

在您的控制器代码中,您可以尝试以下操作......请注意,您可能将Item2作为参数传入[...], 因此您可以在下面进行替换。
@items_to_be_falsified = Item.where('id != ?', Item2.id)

@items_to_be_falsified.each do |item|
  item.default = false
  item.save
end

请注意,在您成功实现此功能后,最好将其移入模型中,将其转换为函数并像下面这样调用Item2.falsify_all_others
def falsify_all_others
  Item.where('id != ?', self.id).each do |item|
    item.default = false
    item.save
  end
end

享受吧!


这个完美地运作了,谢谢。现在我学会了如何在将来做类似的事情。 - ruevaughn
3
这里最好使用update_all。这段代码可能会执行成千上万次查询。 - Mischa

3

好的,你需要准备一些额外的东西。

不要使用默认的字段名称,它通常是保留给数据库的。 如果将记录保存为默认值false,则会将所有记录都设置为false,这不是你想要的。检查一下我们是否正在将这条记录设置为true并进行false化。

  before_save :falsify_all_others
  def falsify_all_others
    if is_default
      self.class.where('id != ?', self.id).where('is_default').update_all(:is_default => false)
    end
  end

我遇到了这个问题。我一直在想为什么我的 falsify_all 会覆盖新的默认值。你是说使用列名“default”会导致这样的问题吗? - Natus Drew

2
如果您使用的是Rails 6,并且是在最近的时间内访问此处,则应该已经在数据库级别以及模型级别上进行了覆盖:
数据库级别:
add_index :items, :default, unique: true, where: '(default IS TRUE)'

模型级别:

class Item < ApplicationRecord
  scope :default, -> { where(default: true) }
  
  validates :default, uniqueness: { conditions: -> { default } }
end

1

如果您想要在创建和更新时使其工作(Rails v4),请注意来自 Rails指南的以下提示:

after_save在创建和更新时都会运行,但始终在更具体的after_create和after_update回调之后运行,无论宏调用的顺序如何。


1

更多使用ActiveRecord,少用原生SQL的决策

after_commit :reset_default, if: :default?

private

def reset_default
  self.class.where.not(id: id).where(default: true).update_all(default: false)
end

我很满意这个,因为它发生在 after_commit 之后,所以你知道一切都已经安定下来了。 - Petercopter

1
class Model < ApplicationRecord
  before_save :ensure_single_default, if: :is_default?

  private

  def ensure_single_default
    self.class.update_all(is_default: false)
  end
end

您不需要检查ID,因为此回调在真值保存之前发生。

我感觉这样可能不会很顺利,因为它会在保存之前进行更新,如果保存未完成,则无法回滚对其他记录所做的更改。不过,它是在验证后发生的,所以可能还好。 - Petercopter
我已经在实际环境中测试过这个功能/并且已经通过了规格测试。 - fabriciofreitag

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接