我有一个带有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: "[email protected]",
      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: "[email protected]",
      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       "[email protected]"
    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_profile_id

最佳答案

当模型A的has_one模型B时,这意味着B将外键存储到A中,就像模型C的has_many模型D意味着D将外键存储到C中一样。has_one关系只是表达了您只允许一个记录的愿望。在B中保留一个特定的外键到A中。鉴于此,您应该从user_profile_id模式中删除users,因为它没有被使用。仅使用user_id中的UserProfile

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

您的UserProfile对象不应直接检查user_id,因为在创建新的user-user_profile对时该id尚不存在。而是使用validates_presence_of :user,它将在保存之前检查UserProfile是否具有关联的User对象。然后,在has_one :user_profile, :inverse_of => :user中写入User,即使在其中一个持久化并分配了ID之前,也可以让UserProfile知道其User对象的存在。

最后,您可以在before_create中包括一个User块,以在创建新用户时构建关联的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_profile

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

另一方面,如果您真的发现拥有两个单独的模型有其值(value),那么也许您不应该使用验证来维持它们的共同存在。我认为,通常应使用模型验证来让用户知道他们提交的数据存在需要修复的错误。但是,他们无法解决user对象中缺少user的问题。因此,也许更好的解决方案是让user_profile对象构建一个user_profile(如果没有的话)。您不仅可以提示ojit_code不存在,还可以进一步发展并构建它。两侧均无需验证。
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

关于ruby-on-rails - Rails : has_one and belongs_to with presence validation on both foreign keys,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/13203159/

10-13 04:49