【问题标题】:Rails: How to specify valid values for fields in my model via another modelRails:如何通过另一个模型为我的模型中的字段指定有效值
【发布时间】:2012-03-05 02:55:14
【问题描述】:

我想做的是定义一个有效值列表,然后在相关表上添加新角色时根据该值列表进行验证。

让我举一个具体的例子:

假设我有一个“就业”表,其中包含以下字段:

user_id (tied to a user table)
employer_id (tied to an employer table)
position_id (tied to a position table)
details
efbegdt
efenddt

当用户向该表添加新行时,我想确保其他表中已经存在雇主 ID 和职位 ID,并且如果在任一情况下都不是这种情况,则不允许保存。

目前我看到的解决方案是这样的:

class Employment < ActiveRecord::Base
  EMPLOYERS = ['Google', 'Yahoo', 'Microsoft']
  POSITIONS = ['Web Developer', 'Database Admin', 'QA']
  validates_inclusion_of :employer_id, :in => EMPLOYERS
  validates_inclusion_of :position_id, :in => POSITIONS
end

但这种方法不够灵活,无法容纳潜在的数千名雇主和职位,如果用户当前不存在雇主,它也不能提供一种简单的方法来允许用户添加新的有效条目。

我也见过这种方法:

class Employment < ActiveRecord::Base
  validate :employer_exists

  protected

  def employer_exists
    ids = Employer.all.map(&:id)
    if !employer_id.blank? && !ids.member?(employer_id)
      errors.add(:employer_id, "invalid employer")
    end
  end
end

这更接近我想要的,但是当我使用 rspec 进行测试时,检查雇主表上的新行是否有效失败:

Failure/Error: it { should be_valid }
  expected valid? to return true, got false

这个问题有“最佳实践”解决方案吗?

更新

只需添加另一个示例,详细说明所有设置。在此示例中,用户可以将多个电子邮件地址存储在电子邮件表中,但每种类型(个人、工作、学校等)限制为一个地址。另一个表 email_dfn 定义了所有有效类型:

迁移文件

class CreateEmailDfns < ActiveRecord::Migration
  def change
    create_table :email_dfns do |t|
      t.string :short_description
      t.string :long_description

      t.timestamps
    end
  end
end

class CreateEmails < ActiveRecord::Migration
  def change
    create_table :emails do |t|
      t.integer :user_id
      t.integer :email_dfn_id
      t.string :value
      t.text :notes

      t.timestamps
    end
    add_index :emails, [:user_id, :email_dfn_id]
  end
end

型号

class Email < ActiveRecord::Base
  attr_accessible :value, :notes, :email_dfn_id
  belongs_to :user
  belongs_to :email_dfn

  validates_associated :email_dfn

  valid_email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :value, presence: true,
                    length: { maximum: 256 },
                    format: { with: valid_email_regex },
                    uniqueness: { case_sensitive: false }

  validates :user_id, presence: true
  validates :email_dfn_id, presence: true
end

class EmailDfn < ActiveRecord::Base

  attr_accessible :short_description,
                  :long_description,
  validates_uniqueness_of :short_description,
                          :long_description

  has_many :emails
end

测试

require 'spec_helper'

describe Email do

  let(:user) { FactoryGirl.create(:user) }
  before { @email = user.emails.build(email_dfn_id: 1,
                                      value: "personal_email@test.com",
                                      notes: "My personal email address") }

  subject { @email }

  it { should respond_to(:value) }
  it { should respond_to(:notes) }
  it { should respond_to(:email_dfn_id) }
  it { should respond_to(:user_id) }
  it { should respond_to(:user) }
  its(:user) { should == user }

  it { should be_valid }

  describe "when user id is not present" do
    before { @email.user_id = nil }
    it { should_not be_valid }
  end

  describe "when email id is invalid" do
    before { @email.email_dfn_id = 999 }
    it { should_not be_valid }
  end
end

在当前设置中,最后一次测试(设置 email_dfn_id = 999,无效代码)失败。

【问题讨论】:

    标签: ruby-on-rails model controller


    【解决方案1】:

    使用validate_associated

    class Employment < ActiveRecord::Base
      belongs_to :employee
    
      validates_associated :employee
    end
    

    阅读文档here

    【讨论】:

    • 我也想过这样做......我用这种方法遇到的问题是以下 rspec 测试将失败:'describe "when employees_id is invalid" do before { @employment.employer_id = 999 } it { should_not be_valid } end' 其中,employer_id = 999 目前不是雇主表上的有效 ID
    • 您应该在测试前预先创建一个Employer
    • 是的 - 我也这样做了,虽然我在最初的一些工作中没有意识到,我得到的自动递增 ID 比我想象的要高。重置测试数据库后,我能够针对正确的 ID 1 进行验证(请参阅主帖中的新示例),但是这个“validates_associated”设置似乎仍然不足以要求验证 ID 是否有效(即测试ID = 999 仍然返回“有效”,但它不应该返回)
    【解决方案2】:

    怎么样,

    validates :employer_id, presence: true, inclusion: { in: Employer.all.map(&:id), message: 'must be a valid employer.' }
    

    【讨论】:

    • employees 表有 1000 行时,您的解决方案可能会很昂贵,因为您会将所有 id 加载到内存并为每次验证执行数组查找。除此之外,在生产模式下,此解决方案将不包括启动服务器后添加的员工。
    • 我认为不会那么贵。我也不确定你的第二点是否正确。我尝试在生产模式下启动我的控制台,并且 ModelName.all 在创建新记录时确实更新了。
    • 这似乎要求 ID 有效,但我担心缩放。我用这种方法遇到的另一个问题是新行有效的检查返回失败。 (请参阅上面我的帖子的更新......它 { should be_valid } 在这种情况下会失败)
    • Model.all 调用没有捕捉到我所指的场景。加载类时会初始化验证列表。一旦初始化,列表就不会改变。您可以通过尝试针对添加到数据库的新行来验证您的代码来测试这一点。如果要为每次验证重新计算列表,则必须提供 lambda 而不是静态列表。
    • 谢谢。我不知道。当定义静态列表不起作用时,我确实使用 lambdas 进行一些验证,但我不知道为什么会这样。
    【解决方案3】:

    我假设user belongs_to employer 在您的关系设置中。

    在这种情况下,也许您可​​以使用关联方法来简化代码。

    如果您使用的是默认表单生成器,

    <% f.select("user", "employer_id", Employer.all) %> 
    

    这为您提供了一个选择框,其中包含已定义的所有可能的雇主。但是,您的用户仍然可以欺骗创建并插入不存在的新雇主 ID。 (旁注:我不知道他们为什么会这样做)。

    我会尽可能推荐使用相关方法而不是编写自定义验证函数等,因为这样可以让其他人更容易理解您的代码。

    如果您只需要验证,其他答案可能更好 =)

    在您的创建操作中,您可以再做一项检查。

    def create
      @user = User.new(params[:id])
      @user.employer = Employer.find(params[:user][:employer_id])
    
      ..... # Standard save code or your own. 
    
    end
    

    如果更多与授权和安全有关,您可以考虑实施像 cancan 这样的 gem 来缩小用户可以选择的范围。

    用于授权和其他权限目的

    例如,您不希望人们选择“禁用”/“预览”条目。如果你用过cancan,可以有这样的方法。

    <% f.select("user", "employer_id", Employer.accessible_by(current_ability) %> 
    

    如果他们的能力被正确定义,试图欺骗系统最终会导致访问被拒绝。

    【讨论】:

    • 这里有好主意 - 但就像你说的那样,如果可能的话,我真的很想通过关联方法来实现它。我在主帖中发布了另一个(非常具体的)示例 - 关于为什么当前设置没有强制执行 email_dfn 模型中定义的电子邮件 ID 为“有效”的要求的任何想法?
    猜你喜欢
    • 2019-09-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-02
    • 1970-01-01
    • 2021-04-30
    • 2021-08-31
    • 1970-01-01
    相关资源
    最近更新 更多