【问题标题】:Rails: has_one and belongs_to with presence validation on both foreign keysRails:has_one 和 belongs_to 对两个外键进行存在验证
【发布时间】:2012-11-02 21:16:53
【问题描述】:

我有一个带有users 表和user_profiles 表的应用程序。一个用户has_one 用户配置文件和一个用户配置文件belongs_to 一个用户。

我想确保关联场景始终为真,因此我对两个外键的存在进行了验证。问题是我遇到了“鸡和蛋”的情况。当我创建用户时,它不起作用,因为用户配置文件还不存在,当我创建用户配置文件时,它也不起作用,因为用户还不存在。所以我需要在创建用户的过程中创建用户配置文件。更复杂的是,当我创建客户端时,我还在after_create 回调中创建了一个用户。说够了(或读/写),这里有一些代码:

class User < ActiveRecord::Base
    has_one :user_profile
    validates :user_profile_id, presence: true
end

class UserProfile < ActiveRecord::Base
  belongs_to :user
  validates :user_id, presence: true
end

class Client < ActiveRecord::Base
  after_create :create_client_user

  private

  def create_client_user
    User.create!(
      email: "admin@example.com",
      password: "admin",
      password_confirmation: "admin",
      client_id: self.id
      # I need to create a user profile dynamically here
    )
  end
end

有没有可能做我想做的事?

更新

我尝试了@cdesrosiers 建议的解决方案,但我的规格无法通过。我主要有三个错误。首先让我向您展示更新后的模型:

class User < ActiveRecord::Base
  has_one :user_profile, inverse_of: :user
  before_create { build_user_profile }

  validates :user_profile, presence: true

  def client=(client)
    self.client_id = client.id
  end

  def client
    current_database = Apartment::Database.current_database
    Apartment::Database.switch
    client = Client.find(self.client_id)
    Apartment::Database.switch(current_database)
    client
  end
end

class UserProfile < ActiveRecord::Base
  belongs_to :user

  validates :user, presence: true
end

class Client < ActiveRecord::Base
  attr_accessible :domain, :name

  after_create :create_client_database
  after_create :create_client_user
  after_destroy :drop_client_database

  # Create the client database (Apartment) for multi-tenancy
  def create_client_database
    Apartment::Database.create(self.domain)
  end

  # Create an admin user for the client
  def create_client_user
    Apartment::Database.switch(self.domain)

    User.create!(
      email: "admin@example.com",
      password: "admin",
      password_confirmation: "admin",
      client: self
    )

    # Switch back to the public schema
    Apartment::Database.switch
  end

  def drop_client_database
    Apartment::Database.drop(self.domain)
  end
end

我正在使用 FactoryGirl 创建工厂,这是我的工厂文件:

FactoryGirl.define do
  factory :client do
    sequence(:domain) { |n| "client#{n}" }
    name              Faker::Company.name
  end

  factory :user do
    sequence(:email)      { |n| "user#{n}@example.com"}
    password              "password"
    password_confirmation "password"
    client
    #user_profile
  end

  factory :credentials, class: User do
    email       "user@example.com"
    password    "password"
  end

  factory :user_profile do
    forename       Faker::Name.first_name
    surname        Faker::Name.last_name
    birthday       (5..90).to_a.sample.years.ago
    #user
  end
end

如果我分别取消注释用户和用户配置文件工厂中的 user_profileuser 关联,我会得到一个 WARNING: out of shared memory

现在,当我创建其中一个工厂时,我得到了以下三个错误之一:

Failure/Error: @user = create(:user)
     ActiveRecord::RecordInvalid:
       Validation failed: User profile A user profile is required
     # ./app/models/client.rb:41:in `create_client_user'
     # ./spec/controllers/users_controller_spec.rb:150:in `block (4 levels) in <top (required)>'

Failure/Error: create(:user_profile).should respond_to :surname
    ActiveRecord::RecordInvalid:
      Validation failed: User A user is required
    # ./spec/models/user_profile_spec.rb:29:in `block (4 levels) in <top (required)>'

Failure/Error: let(:client) { create(:client) }
     ActiveRecord::RecordInvalid:
       Validation failed: User profile A user profile is required
     # ./app/models/client.rb:41:in `create_client_user'
     # ./spec/controllers/sessions_controller_spec.rb:4:in `block (2 levels) in <top (required)>'
     # ./spec/controllers/sessions_controller_spec.rb:7:in `block (2 levels) in <top (required)>'

所以我假设 User 模型中的更改不起作用。另请注意,我从 users 表中删除了 user_profile_id

【问题讨论】:

    标签: ruby-on-rails associations belongs-to has-one


    【解决方案1】:

    当模型Ahas_one模型B时,这意味着B将外键存储到A中,就像模型Chas_many模型D意味着D将外键存储到C一样。has_one关系只是表示您希望仅允许 B 中的一条记录将特定外键保存到 A 中。鉴于此,您应该从 users 模式中删除 user_profile_id,因为它没有被使用。仅使用来自UserProfileuser_id

    您仍然可以让User 检查UserProfile 的存在,但改用validates_presence_of :user_profile。这将检查用户对象是否具有关联的 user_profile 对象。

    您的 UserProfile 对象不应直接检查 user_id,因为在创建新的 user-user_profile 对时此 ID 还不存在。而是使用validates_presence_of :user,它将在保存UserProfile 之前检查它是否具有关联的User 对象。然后在User 中写入has_one :user_profile, :inverse_of =&gt; :user,这让UserProfile 知道它的User 对象的存在,甚至在它们被持久化并分配一个id 之前。

    最后,您可以在User 中包含before_create 块,以在创建新用户时构建关联的UserProfile。 (我相信)它会在构建新的 user_profile 后运行验证,所以这些应该会通过。

    总之,

    class User < ActiveRecord::Base
        has_one :user_profile, :inverse_of => :user
        validates_presence_of :user_profile
    
        before_create { build_user_profile }
    end
    
    class UserProfile < ActiveRecord::Base
      belongs_to :user
      validates_presence_of :user
    end
    

    更新

    我弄错了验证回调顺序。验证在调用before_create 回调之前运行,这意味着User 甚至在构建之前检查UserProfile 的存在。

    一种解决方案是问问自己,拥有单独的 user 和 user_profile 模型可以获得什么价值。鉴于它们是如此紧密地绑定在一起,以至于一个不能没有另一个存在,将它们组合成一个模型是否有意义(并且可能会简化很多代码)?

    另一方面,如果您真的发现拥有两个独立模型的价值,也许您不应该使用验证来维持它们的相互存在。在我看来,模型验证应该一般用于让用户知道他们提交的数据有需要修复的错误。但是,他们的user 对象中缺少user_profile 并不是他们 可以解决的。因此,也许更好的解决方案是让user 对象构建一个user_profile(如果没有的话)。如果user_profile 不存在,您不只是抱怨,而是更进一步并构建它。任何一方都不需要验证。

    class User < ActiveRecord::Base
      has_one :user_profile
    
      before_save { build_user_profile unless user_profile }
    end
    
    class UserProfile < ActiveRecord::Base
      belongs_to :user
    end
    

    【讨论】:

    • 非常感谢您的回答,它让我更新了代码和问题本身。不幸的是,由于某种原因,您的解决方案对我不起作用。有关详细信息,请参阅更新后的问题。
    • 再次感谢您的回答。我更新了我的代码,我结合了 User 和 UserProfile 模型。这一变化大大简化了我的代码。
    • 我想你想要的回调是before_validation :build_user_profile, on: :create
    【解决方案2】:

    您无法验证 user_profile_id 是否存在,因为它不存在。 has_one 的意思是另一个模型有一个外键引用。

    我通常确保您所追求的行为的方式是在创建被引用的模型时有条件地创建具有外键引用的模型。在您的情况下,将为用户创建个人资料after_create,如下所示:

    class User < ActiveRecord::Base
      ...
      after_create :create_profile
      private
      def create_profile
        self.user_profile.create
      end
    end
    

    【讨论】:

      【解决方案3】:

      这个 rail cast 会创建嵌套表单(同时创建 user/user_profile)。 http://railscasts.com/episodes/196-nested-model-form-part-1 您需要进行一些修改,因为它涵盖了 has_many,但您应该能够弄清楚。

      【讨论】:

        【解决方案4】:

        您在这里遇到了嵌套问题。可以通过将 user_profile 的表单字段嵌套在用户表单上并同时创建两者来解决。

        class User
          accepts_nested_attributes_for :user_profile
          attr_accesible :user_profile_attributes
          validates_presence_of :user_profile_attributes
        
        #user/new.haml
        form_for @user do |f|
          fields_for @user.user_profile do |fields|
            fields.label "Etc"
            #......
        

        http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-fields_for

        【讨论】:

        • 这并没有解决基本上无法解决的问题,即尝试验证一个不存在的字段是否存在。
        • validates_presence_of :user_profile_attributes 确保配置文件字段存在,user_profile.rb 下的任何验证也将运行,因此您无法创建没有配置文件的用户。什么意思?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多