【问题标题】:What is the "rails way" to enforce a has_many but has-only-one-current association?执行 has_many 但只有一个当前关联的“rails 方式”是什么?
【发布时间】:2011-07-11 10:28:11
【问题描述】:

我有一个带有模型项目和阶段的简单 Rails 应用程序。一个项目有许多阶段,但一次只能在一个阶段处于活动状态(即“当前”)。我仍然希望其他阶段可以访问,但当前阶段应该是应用程序的主要锚点。关于如何实现此要求的决定对我如何处理模型访问、验证和视图/表单以进行创建更新具有重大影响。

所以问题是:如何在不增加太多复杂性的情况下实现这种“has_many but has-only-one-current association”?主要目标是:简单地访问当前阶段 + 确保一次不能有超过 1 个活动阶段。

当然,我自己也有一些想法,想出了三个选项,我想在这里介绍一下。任何关于为什么我应该选择一个选项而不是另一个选项的反馈(或更简单的解决方案的建议)将不胜感激:

第一个选项:

[Project] has_many :phases
[Project] has_one  :current_phase, :class_name => "Phase", :conditions => { :current => true }

缺点:我有一个用于创建项目和相应阶段的嵌套表单。似乎没有简单的方法可以将新创建​​的阶段之一设置为活动状态

第二个选项:

[Project] has an attribute "current_phase_id"

[Project] has_many :phases
[Project] belongs_to phase, :foreign_key => "current_phase_id"

缺点:与选项1相同,但我有另一个属性和belongs_to关联,这看起来很奇怪(为什么项目应该属于它的一个阶段?)

第三个选项:

[Phase] has an attribute "active" (boolean)
[Phase] scope :active, :conditions => { :active => true}

# Access to current phase via: project.phases.active

缺点:我必须通过验证确保一次只有一个活动阶段,如果同时创建/编辑多个阶段或从一个阶段切换到另一个阶段,这很难;加:project.phases.active 返回一个数组,如果我没记错的话

非常感谢您的帮助。谢谢!

更新

添加了赏金以鼓励对该主题提出进一步的意见。赏金将奖励给最能实现上述主要目标的解决方案;或者,如果没有提到替代解决方案,则最能解释为什么我应该偏爱给定选项之一的答案。谢谢!

【问题讨论】:

  • 我更喜欢选项 2,它看起来很奇怪,我同意,但是您不需要额外的验证来检查当前的唯一性,也不需要更新数据库中的两条记录来更改当前的。
  • 感谢您的反馈,@ryaz。我在下面的 edgerunner 中获得了最高投票的答案。更加优雅和易于处理。

标签: ruby-on-rails associations has-many


【解决方案1】:

您为什么不向您的Phase 模型添加一个名为activated_at 的日期时间列。然后,只要您想激活某个阶段,就将其设置为当前时间。

在任何给定时间,具有最新 activated_at 值的阶段是当前阶段,因此您可以使用 @project.phases.order('activated_at DESC').first 获取它。只需将其包装在 Project 中的方法中,您就有了一个非常简洁的表示:

# in project.rb
def current_phase
  phases.where("activated_at is NOT NULL").order('activated_at DESC').first
end

【讨论】:

  • 我会改进一点:phases.where("activated_at is NOT NULL").order('activated_at DESC').first
  • 第二个。非常感谢你,edgerunner。 @fl00r:很大的进步——我很高兴看到你们在这里的共同努力如何导致我现在认为是“最佳实践”:)
  • @fl00r,好点子。您也可以将activated_at 设为必填列,但任何一种方法都适用,并且至少必须这样做。
  • 如果我们一次创建三个新阶段,必填字段会搞砸。
  • 在 Rails 6 中,我相信你现在可以做类似phases.where.not(activated_at: nil).order(activated_at: :desc).first
【解决方案2】:

一个很好的问题。我一直在与非常相似的事情作斗争。我最终得到的结果与您的选项 1 类似,但使用的是连接表。

class Project < ActiveRecord::Base
has_many :phases, :through=> :project_phase

has_one :active_project_phase, :class_name => 'ProjectPhase'`

要准确设置一个新创建的阶段处于活动状态,我在控制器中有一些代码使它们全部处于非活动状态,然后如果没有阶段,则添加一个新的活动阶段,或者根据参数选择一个激活阶段传入和一堆规则。它不漂亮,但它有效。我确实先尝试了选项 3,但发现由于您描述的原因,这变得非常混乱

【讨论】:

  • 只需重新阅读我的答案并意识到这实际上是您的选择 1 - 重写它!
  • 呵呵,不过还是谢谢你的想法。没有想到在这里使用连接表,所以你的回答肯定有助于一个可能的解决方案。为此+1。仍然期待看到其他意见。
【解决方案3】:

选项 1 看起来非常原生。您只需要添加验证来验证是否只有一个带有current 标志和project_id 的阶段以及一些用于控制客户端复选框的javascript。

class Project < AR::Base
  has_many :phases
  has_one  :current_phase, :class_name => "Phase", :conditions => { :current => true }
  accepts_nested_attributes_for :phases, :allow_destroy => true
end

class Phase < AR::Base
  belongs_to :project
  validates :project_id, :uniqueness => {:scope => :current}, :if => proc{ self.current }
end

那么,你的看法:

<%= form_for @project do |f| %>
  ...
  <%= f.fields_for :phases do |phase| %>
    <%= phase.text_field :title %> # or whatever
    <%= phase.check_box :current, :class => "current_phase" %>
  <% end %>
  ...
<% end %>

还有小 javascript(实际上是 jQuery)取消选中所有 current 复选框,但您单击了一个。

$(document).ready(function(){
  $(".current_phase").click(function(){
    $(".current_phase").not(this).attr('checked', false);
  }
})

【讨论】:

  • 非常感谢 fl00r - 我感谢您为详细的答案和 javascript 位所做的努力(尽管我会尝试通过单选按钮解决这个问题 - 不幸的是,使用 rails form helpers 并不那么容易...)。我会将正确答案授予 edgerunner,因为他的解决方案简单。
  • 我同意@edgerunner 解决方案更好
  • 为了一致性和查询速度,我还将在数据库级别为project_id添加唯一性约束,特别是对于rails 5,默认情况下为外键创建索引
猜你喜欢
  • 1970-01-01
  • 2021-10-14
  • 2019-04-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多