Rails belongs_to 可选时不验证 id

Rails belongs_to not validating id when optional

我是 运行 Rails 5.1.4,我有一个看起来像这样的模型:

class Quota < ActiveRecord::Base
  belongs_to :domain, optional: true
  belongs_to :project, optional: true
end

配额应属于域或项目,但不能同时属于两者(因此设置 optional: true)。

但是,如果提供的项目或域 ID 无效,我似乎无法弄清楚如何使 rails 抛出错误。

事情是这样的:

q = Quota.create!(domain_id: nil, project_id: 'invalid_id')
q.project_id # -> nil

即使我明确传递 project_id,如果它与有效项目不匹配,它也会神奇地清除它。 我尝试添加自定义验证方法,但在调用验证方法时,它已经设置为 nil。它甚至也不使用 project_id= 方法;我检查了。

有没有办法让 Rails 在 ID 无效时引发错误而不是将其设置为 nil? (同时仍然允许 nil 值)

我能想到的最佳解决方案是:

class Quota < ActiveRecord::Base
  belongs_to :domain,  optional: true
  belongs_to :project, optional: true

  validate :validate_associations

  def project_id=(val)
    Project.find(val) unless val.nil?
    super
  end

  def domain_id=(val)
    Domain.find(val) unless val.nil?
    super
  end

  private

  def validate_associations
    errors.add(:base, 'Specify a domain or a project, not both') if domain && project
    errors.add(:base, 'Must specify a domain or a project') if domain.nil? && project.nil?
  end
end

感谢@vane-trajkov 帮助解决问题。我发现我真的需要在设置 domain_id 或 project_id 时使用 find 方法,因为 Rails 很乐意将其设置为无效 ID。使用 project=domain= 可以正常工作,因为它们几乎可以确保 ID 已设置为有效值。

这是一种可能的解决方案

class Quota < ApplicationRecord
  belongs_to :domain, optional: true
  belongs_to :project, optional: true

  validate :present_domain_or_project?
  validates :domain, presence: true, unless: Proc.new { |q| q.project_id.present? }
  validates :project, presence: true, unless: Proc.new { |q| q.domain_id.present? }

  private

  def present_domain_or_project?
    if domain_id.present? && project_id.present?
      errors.add(:base, "Specify a domain or a project, not both")
    end
  end
end

在第一个块中,我们定义了关联并指定了 optional: true 因此我们超越了新的 Rails 5 验证关联存在的行为。

belongs_to :domain, optional: true
belongs_to :project, optional: true

然后,我们做的第一件事就是简单地消除设置关联属性(project_iddomain_id)的情况。这样我们就避免了两次访问数据库,实际上,我们只需要访问一次数据库。

validate :present_domain_or_project?
...
private 

def present_domain_or_project?
  if domain_id.present? && project_id.present?
    errors.add(:base, "Specify a domain or a project, not both")
  end
end

最后一部分是在没有另一个关联的情况下检查关联是否存在(有效)

validates :domain, presence: true, unless: Proc.new { |q| q.project_id.present? }
validates :project, presence: true, unless: Proc.new { |q| q.domain_id.present? }

关于:

Is there a way to get Rails to raise an error if the ID is invalid instead of setting it to nil? (while still allowing a nil value)

使用 create! 方法时,如果验证失败,Rails 会引发 RecordInvalid 错误。应捕获并适当处理异常。

begin
  q = Quota.create!(domain_id: nil, project_id: 'invalid_id')
rescue ActiveRecord::RecordInvalid => invalid
  p invalid.record
  p invalid.record.errors
end

invalid 对象应包含失败的模型属性以及验证错误。请注意,在此块之后,q 的值为 nil,因为属性无效并且没有对象被实例化。这是 Rails.

中的正常预定义行为

另一种方法是结合使用newsave方法。使用 new 方法,可以在不保存的情况下实例化对象,并且调用 save 将触发验证并将记录提交到数据库(如果有效)。

q = Quota.new(domain_id: nil, project_id: 'invalid_id')
if q.save
  # quota model passes validations and is saved in DB
else 
  # quota model fails validations and it not saved in DB
  p q
  p q.errors
end

此处的对象实例 - q 将保存属性值和验证错误(如果有)。