Rails belongs_to在可选时不验证id

时间:2017-12-11 23:35:28

标签: ruby-on-rails activerecord ruby-on-rails-5 rails-models

我正在运行Rails 5.1.4,我的模型看起来像这样:

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

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

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

以下是发生的事情:

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

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

如果ID无效而不是将其设置为nil,是否有办法让Rails引发错误? (虽然仍允许零值)

2 个答案:

答案 0 :(得分:1)

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

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已经设置为有效值。

答案 1 :(得分:0)

这是一种可能的解决方案

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? }

关于:

  

如果ID无效,是否有办法让Rails引发错误   而不是将其设置为零? (虽然仍允许零值)

使用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将保留属性值和验证错误(如果有)。