【问题标题】:Rails custom validation that limits the number of has_many :through associations allowed?Rails自定义验证限制了has_many的数量:允许通过关联?
【发布时间】:2019-09-13 23:40:04
【问题描述】:

我有一个带有“产品变体”表单的 Rails 项目。产品变体模型称为Variant,在Variant 表单上,用户应该能够为每个可用选项选择一个选项。例如,一件 T 恤可能有一个名为“尺寸”的“选项”,其中“选择”为小、中或大,另一个“选项”名为“颜色”,“选择”为红色、绿色、蓝色。因此,创建的Variant 是一个独特的 SKU,例如“T 恤 — 尺寸:小号,颜色:绿色”。或者,如果产品有 3 个选项而不是 2 个选项,则该变体将需要每个选项 3 个选项,例如“吉他背带 - 尺寸:长款,面料颜色:红色,皮革颜色:棕色”。

我不知道如何编写只允许用户为每个选项保存一个选项的自定义验证。每个选项应该只为每个变体选择一个选项。这是一个插图。

这是我的模型与相关的关联...

models/variant.rb

class Variant < ApplicationRecord    
  has_many :selections
  has_many :choices, through: :selections

  validate :one_choice_per_option

  private
    def one_choice_per_option
      # can't figure out how to do this custom validation here
    end

end

models/choice.rb

class Choice < ApplicationRecord
  has_many :variants, through: :selections

  belongs_to :option
end

models/selection.rb

class Selection < ApplicationRecord
  belongs_to :choice
  belongs_to :variant
end

models/option.rb

class Option < ApplicationRecord
  has_many :choices, dependent: :destroy

  accepts_nested_attributes_for :choices, allow_destroy: true
end

我设法做的最好的事情是在models/variant.rb 中进行此自定义验证

def one_choice_per_option
  self.product.options.each do |option|
    if option.choices.count > 1
      errors.add(:choice, 'Error: select one choice for each option')
    end
  end
end

但这只允许一个Choice 总数通过变体形式。我想要做的是让每组选项都有一个选择。

我知道这可以在 UI 中使用 Javascript 来完成,但这对于保持数据库清洁和防止用户错误至关重要,所以我认为它应该是模型级别的 Rails 验证。

进行此类自定义验证的“Railsy”方式是什么?我应该尝试对Selection 模型进行自定义验证吗?如果有,怎么做?


更新

基于 cmets 中的讨论。看来我需要结合Active Record querying 来完成这项工作。 @sevensidemarble 下面的“EDIT 2”更接近,但这给了我这个错误:Type Error compared with non class/module

如果我将错误的行为保存到数据库中,然后在控制台中调用Variant.last.choices,那感觉就像我越来越接近了:

所以本质上,如果有多个Selection 具有相同的option_id,我需要做的是不允许保存Variant 表单。除非option_id 对关联的Variant 是唯一的,否则不应保存选择。

我正在尝试做这样的事情:

validate :selections_must_have_unique_option

  private

    def selections_must_have_unique_option
      unless self.choices.distinct(:option_id)
        errors.add(:options, 'can only have one choice per option')
      end
    end

但该代码不起作用。它只是保存表单,就好像验证不存在一样。

【问题讨论】:

  • 您的第一个答案本身对我来说似乎没问题。我只是不确定这里的关系。您是否定义了 variant-belongs_to-product、product-has_many-options 关系?如果不是,请告诉我们这里的关系层次结构,因为我在“产品”模型方面感到困惑。

标签: ruby-on-rails forms validation activerecord simple-form


【解决方案1】:

这类似于唯一性约束,但 Rails 的内置 validates_uniqueness_of 无法处理此问题:所需的验证在每个 Selection 对象上,但它是由选项决定的,而 selections 表没有 有一个option_id 列来约束

您也不能在数据库中轻松做到这一点,因为唯一索引不会跨越表边界。

我建议您让每个 Selection 对象查找冲突的兄弟,并在结果上使用 absence validation。像这样的:

class Variant < ApplicationRecord    
  has_many :selections
  has_many :choices, through: :selections
end

class Selection < ApplicationRecord
  belongs_to :choice
  belongs_to :variant

  validates_absence_of :conflicting_selection

  protected
    def option
      choice.option
    end

    def conflicting_selection
      variant.selections.excluding(self).detect { |other| option == other.option }
    end
end

鹰眼会注意到我使用了数组方法而不是 ActiveRecord 查询。这不是避免数据库往返的俗气的噱头。它确保验证在未保存的选择以及那些持久的选择上正常工作,这对于表单处理可能是必不可少的。我通常想写成has_one :option, through: :choiceSelection#option 方法与此类似。

如果你需要的话,带有单元测试的示例要点here

【讨论】:

  • 哇!非常感谢您提供如此详尽的答案。那肯定奏效了。在VariantsControllerviews/variants/_edit 中处理此类异常的惯用“Rails”方式是什么?我尝试在我的variants#update 操作中添加类似If ActiveRecord::RecordInvalid 的内容,但这不起作用。因为这是 Selection 模型而不是 Variant 表单上的错误,这些错误应该如何与 Variant 表单一起使用?
  • 另外,您建议不要使用这个validates_absence_of,而是应该将has_one :option, through: :choice 关联添加到我的Selection 模型中吗?
  • 希望您永远不会在视图中看到ActiveRecord::RecordInvalid 异常!这表明视图尝试了数据库写入。习惯上来说,异常不应该用于流控制:标准的方法是让#save#update 返回一个虚假的响应。因此,在您的更新操作中,您编写 if @variant.update(secure_params); ...; else; 并且 else 块处理错误情况。这基本上如标准脚手架和Getting Started guide 中所述。
  • 对于错误显示,我会在选择中使用 fields_for nested form。标准 Rails 表单构建器将为每个有冲突的选择提供一个 field_with_errors 包装 div,您可以将其用于样式。
  • has_one 关联不会取代缺席验证。它将取代 #option 方法。但是在这种情况下,它是相反的。
【解决方案2】:

这样的事情应该可以工作:

def one_choice_per_option
      errors.add(:choice, "can't be more then one per selection") if selections.any { |selection| selection.choices.count > 1 }
end

或者,我怀疑你也可以只选择has_one :choice,然后你的生活可能会更简单。


编辑:

根据 cmets 的要求,我认为会按照要求进行更新:

def one_choice_per_option
      errors.add(:choice, "can't be more then one per selection") if selections.joins(:choice).where(selections: :choice).count > 3
end

我认为这会得到你想要的。如果不是,我们只需稍微弄乱joinswhere 部分,请告诉我。

编辑 2:


好的,经过再次讨论,我对要求的内容更加清楚了。抱歉,花了一些时间,如果不在您自己的应用程序中看到它,就很难理解架构。

这是我认为适用于指定的内容。我是根据Product 的上下文在这里写的,但它也可以在Option 上稍作改动。

def one_choice_per_option
  errors.add(:options, "can't be more then one per selection") if (options.joins(:choices).group("options.id").having("COUNT(1) > 1") > 1)
end

我认为这应该可行,如果不是,它真的很接近您的需要。这个SQL是基本思想。有时,在这些查询上致电 .to_sql 会有所帮助,以查看确切的活动记录抽出什么。


编辑 3:

我正在考虑您在 Variant 上的原始代码,并想出了这个:

class Variant < ApplicationRecord
  has_many :selections
  has_many :choices, through: :selections

  validate :one_choice_per_option

  private
  def one_choice_per_option
    if (choices.joins(:option).group("options.id").having("COUNT(1) > 1") > 1)
      errors.add(:choices, "can't be more then one per selection") 
    end
  end
end

【讨论】:

  • 谢谢!我能够接近errors.add(:choice, "can't be more then one per selection") if self.selections.any? { |selection| selection.choices.count &gt; 1 },但这仍然给我一个undefined method 'choices' 错误。这与我在尝试编写此验证时遇到的其他错误类似。如果我将选择模型has_one :choice 用作Varianthave_many :choices, through: :selection 的连接模型,可以选择模型吗?
  • 我相信你也能做到。您也可以将belongs_to 更改为has_many 以使我发布的代码正常工作,但是如果这有意义的话,您需要移动外键。
  • 尝试将has_one :choice 添加到selection 模型同时也将其用作连接表对我不起作用。我不明白你在哪里建议“将belongs_to 更改为has_many”。您的意思是将Selection 模型更改为“has_one”选项吗?我需要它来将选择加入变体,那么这将如何工作?
  • 给我一点时间,我将再次查看您的架构,看看是否有更好的方法。我认为我们可以使用当前模式进行验证,但验证略有不同。我认为我们可以使用 joins 子句和 where 子句来做到这一点。
  • 好的,我已经为您调整了,我认为这种方法可以在不更改架构的情况下工作。如果没有,请告诉我,但我认为这是我会采取的方法。
猜你喜欢
  • 2014-07-06
  • 2013-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多