Rails 限制 has_many 数量的自定义验证:允许通过关联?
Rails custom validation that limits the number of has_many :through associations allowed?
我有一个带有 "Product Variant" 表单的 Rails 项目。产品变体模型称为 Variant
,在 Variant
表单上,用户应该能够 select 为每个可用选项选择一个。例如,一件 T 恤可能有一件名为 "Size" 的 "Option" 带有 "Choices" 小号、中号或大号,而另一件 "Option" 名为 "Color" 带有"Choices" 红、绿、蓝。因此,创建的 Variant
是一个独特的 SKU,例如 "T-shirt — Size: Small, Color: Green." 或者如果它是具有 3 个选项而不是 2 个选项的产品,则变体每个选项需要 3 个选项,例如 "Guitar Strap - Size: Long, Fabric Color: Red, Leather Color: Brown".
我不知道如何编写只允许用户为每个选项保存一个选择的自定义验证。对于每个变体,每个选项应该只有一个选择 selected。这是一个例子。
这是我与相关协会的模型...
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
模型进行自定义验证吗?如果是,怎么做?
更新
基于评论中的讨论。看来我需要做一些 Active Record querying 的组合才能完成这项工作。下面@sevensidedmarble 的 "EDIT 2" 更接近,但这给了我这个错误:Type Error compared with non class/module
如果我将错误的行为保存到数据库中,然后在控制台中调用 Variant.last.choices
,感觉就像我越来越接近:
所以基本上,如果有多个 Selection
具有相同的 option_id
,我需要做的是不允许 Variant
表格保存。 selection 不应保存,除非 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
但该代码不起作用。它只是保存表单,就好像验证不存在一样。
像这样的东西应该可以工作:
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
,这样您的生活可能会更简单。
编辑:
根据评论中的要求,这里有一个更新我认为会完全按照要求进行:
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
我想这会得到你想要的。如果不是,我们只需要稍微弄乱 joins
和 where
部分,让我知道。
编辑 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
可以帮助准确查看 activerecords 输出的内容。
编辑 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
它类似于唯一性约束,但 Rails 的内置 validates_uniqueness_of
无法处理此问题:所需的验证是在每个 Selection 对象上进行的,但它是可选的,并且selections
table 没有 要约束的 option_id
列。
您也不能在数据库中轻易做到这一点,因为唯一索引不会跨越 table 边界。
我建议您让每个 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: :choice
的 Selection#option
方法与此类似。
带有单元测试的示例要点 here 如果需要。
我有一个带有 "Product Variant" 表单的 Rails 项目。产品变体模型称为 Variant
,在 Variant
表单上,用户应该能够 select 为每个可用选项选择一个。例如,一件 T 恤可能有一件名为 "Size" 的 "Option" 带有 "Choices" 小号、中号或大号,而另一件 "Option" 名为 "Color" 带有"Choices" 红、绿、蓝。因此,创建的 Variant
是一个独特的 SKU,例如 "T-shirt — Size: Small, Color: Green." 或者如果它是具有 3 个选项而不是 2 个选项的产品,则变体每个选项需要 3 个选项,例如 "Guitar Strap - Size: Long, Fabric Color: Red, Leather Color: Brown".
我不知道如何编写只允许用户为每个选项保存一个选择的自定义验证。对于每个变体,每个选项应该只有一个选择 selected。这是一个例子。
这是我与相关协会的模型...
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
模型进行自定义验证吗?如果是,怎么做?
更新
基于评论中的讨论。看来我需要做一些 Active Record querying 的组合才能完成这项工作。下面@sevensidedmarble 的 "EDIT 2" 更接近,但这给了我这个错误:Type Error compared with non class/module
如果我将错误的行为保存到数据库中,然后在控制台中调用 Variant.last.choices
,感觉就像我越来越接近:
所以基本上,如果有多个 Selection
具有相同的 option_id
,我需要做的是不允许 Variant
表格保存。 selection 不应保存,除非 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
但该代码不起作用。它只是保存表单,就好像验证不存在一样。
像这样的东西应该可以工作:
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
,这样您的生活可能会更简单。
编辑:
根据评论中的要求,这里有一个更新我认为会完全按照要求进行:
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
我想这会得到你想要的。如果不是,我们只需要稍微弄乱 joins
和 where
部分,让我知道。
编辑 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
可以帮助准确查看 activerecords 输出的内容。
编辑 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
它类似于唯一性约束,但 Rails 的内置 validates_uniqueness_of
无法处理此问题:所需的验证是在每个 Selection 对象上进行的,但它是可选的,并且selections
table 没有 要约束的 option_id
列。
您也不能在数据库中轻易做到这一点,因为唯一索引不会跨越 table 边界。
我建议您让每个 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: :choice
的 Selection#option
方法与此类似。
带有单元测试的示例要点 here 如果需要。