ruby-on-rails Rails:只允许一个Rails模型的一条记录具有布尔属性true?

kyvafyod  于 2022-12-24  发布在  Ruby
关注(0)|答案(5)|浏览(173)

在典型的rails(4.2.x)blog应用中,我有一个Post模型,这个post有一个布尔列primary,我想强制一个模型级约束,最多一个post的primary=true,如果用户设置一个新post为primary=true,那么在保存这个post之前,所有其他post都必须标记primary=false。
我可以在控制器中这样做,当一个帖子被创建或更新为primary=true时,通过将所有其他帖子更改为primary=false。

# in posts_controller#create and #update
...
if @post.primary
  [Post.all - self].select(&:primary).each do {|p|p.primary = false; p.save}
end
@post.save!
...

然而,我希望这是一个模型级约束,这样我就可以添加验证、单元测试等,只有一个post的primary=true。如果我使用before_commit这样的回调,那么我可能会陷入无限循环,因为在新post的before_commit中更新旧post会触发旧post的before_commit,等等。
如何在模型级别强制执行此行为?

bvjveswy

bvjveswy1#

ActiveRecord有一些不会触发回调的更新属性方法,如post.update_columnPost.update_all等。因此,您可以在回调中使用这些方法,如

before_save :set_primary

private
def set_primary
  Post.where.not(id: id).update_all(primary: false)
end
ryoqjall

ryoqjall2#

考虑一种稍微不同的方法可能是值得的,在这种方法中,您可以使用一个单例模型--比如Primaries --它有一个“post_id”,设置为主要帖子的ID。您甚至可以将其作为一个外键,以获得额外的优雅和自动反向引用来检测给定的帖子是否是主要的。
(See https://stackoverflow.com/a/12463209/128977是创建ActiveRecord单例模型的一种方法。)
与协调所有Post记录之间的主标志相比,其优点是:

*原子更新--使用“before_保存”将所有其他帖子更新为primary=false在理论上可能会在保存操作上失败,不会留下primary=true记录...或者一次保存多个帖子可能会变得冒险/草率,尽管我不确定ActiveRecord在这里是如何处理线程的。
*可伸缩性--当您使用单个值来指向主帖子时,帖子记录的数量不再重要。当然,您的SQL后端应该可以很好地处理这个问题,但是更新1-2条记录仍然比检查所有记录要快。

9w11ddsr

9w11ddsr3#

您可以使用的一种方法是在模型上实现一个自定义验证器,该验证器可以防止其他主要帖子被保存到DB(如果已经存在)。
然后可以在Post上定义一个类方法,将主Post重置为普通Post,然后将另一个Post设置为主Post。

自定义验证器*(应用程序/验证器/primary_post_validator.rb)*

class PrimaryPostValidator < ActiveModel::Validator
  def validate(record)
    if record.primary
      record.errors[:primary] << "A Primary Post already exists!" if
        Post.where(primary: true).any?
    end
  end
end

发布模型

class Post < ApplicationRecord
  validates_with PrimaryPostValidator

  def self.reset_primary!
    self.update_all(primary: false)
  end
end

架构.rb

create_table "posts", force: :cascade do |t|
  # any other columns you need go here.
  t.boolean  "primary",        default: false, null: false
  t.datetime "created_at",                      null: false
  t.datetime "updated_at",                      null: false
end

这个设置将允许您控制哪个帖子被分配为控制器的主帖子,并处理您需要交换主帖子的情况。我认为允许保存模型影响数据库中的其他记录是一个坏主意,因为您最初请求。
在控制器中处理该逻辑的一种方式是:

def make_primary
  @post = Post.find(params[:id])
  Post.reset_primary!
  @post.update_attributes(primary: true)
end

虽然与公认的答案相比,这看起来有些做作,但我相信它可以让您更好地控制哪些帖子被设置为主要帖子以及何时设置为主要帖子。这个解决方案还可以 * 与 * 验证一起工作,而不是像上面的答案那样跳过验证。

kq4fsx7k

kq4fsx7k4#

请检查这个简单的gem set_as_primary,它做同样的事情。它也支持其他功能。
处理Rails模型的主标志或默认标志的最简单方法。

qvsjd97n

qvsjd97n5#

    • TL; DR**

1.在:primary列上添加限定范围的唯一索引。
1.重写def primary=以忽略错误值,从模型中删除primary唯一性验证(如果有)。
1.添加一个before_save回调函数,以取消其他任何记录的主要属性。

    • 讨论**

这是7年后的要求,但没有一个答案相当击中马克,所以这里去...
OP具有系统的两个期望行为:
1.强制最多有一个post具有primary=true的约束。
1.如果新帖子设置为primary=true,则所有其他帖子必须标记为primary=false
有三个"层面"需要考虑:
1.数据库级数据完整性-最多一个post可以是primary=true
1.模型级验证(或者不验证!)

  1. UI/UX级别-也许我们可以防止无效数据到达应用程序开始。
    一个常见的误解是,Rails***验证***确保了数据完整性,但由于存在.update_all和其他跳过验证的机制,它们充其量也是不可靠的。
    让我们确保数据库本身只支持一个主服务器!这在scoped indexes中很容易实现。不幸的是,只有Postgres支持它们,而MySQL.YMMW不支持。
    在数据库迁移中
add_index(
  :posts,
  "primary",
  where: "(primary IS TRUE)",
  unique: true,
  name: :posts_uniq_primary_idx,
)

这将确保数据的完整性。如果我们试图保存另一个primary,我们将得到这个错误:

ActiveRecord::RecordNotUnique:
   PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "posts_uniq_primary_idx"
  • 通常情况下 *,我们会进行唯一性验证并就此结束,但OP希望支持主记录切换,而验证会阻止这一点,因此让我们通过正常方式禁用取消主记录的功能(update_all和朋友可以回避这一点,但我们对此无能为力)。
def primary=(value)
  return unless value.present?
  
  super(value)
end

并确保将记录创建或更新为新的primary时,所有其他记录都不是主记录:

class Post < ApplicationRecord
  before_save :ensure_one_primary!

  private 

  def ensure_one_primary!
    return unless primary_changed?(to: true)

    Post.where(primary: true).where.not(id: id).update_all(primary: false)
  end
end

用户界面的好处--你可能会把primary渲染成一个单选按钮或者下拉菜单。对于已经是primary的记录,把元素渲染成禁用的,因为表单提交时把它设置成false不会做任何事情。

    • 结束语**

你可能不希望在整个表中只有一个主帖子,因为它的作用域可能是某个账户。如果是这样,唯一索引不仅要在"primary"列上,还要在"primary, account_id"上建立一个复合索引,并且ensure_one_primary!回调需要确保只取消属于新主帖子账户的帖子的主索引。

相关问题