Rails:has_one和belongs_to,在两个外键上都有状态验证

Rob*_*udi 10 ruby-on-rails associations has-one belongs-to

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

我想确保关联方案始终为true,因此我对两个外键的存在进行了验证.问题是我遇到了"鸡蛋和鸡蛋"的情况.当我创建用户时,它不起作用,因为用户配置文件尚不存在,当我创建用户配置文件时,它也不起作用,因为用户尚不存在.所以我需要在创建用户的过程中创建用户配置文件.更复杂的是,当我创建客户端时,我还在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
Run Code Online (Sandbox Code Playgroud)

有可能做我想做的事吗?

更新

我尝试了@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
Run Code Online (Sandbox Code Playgroud)

我正在使用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
Run Code Online (Sandbox Code Playgroud)

如果我分别取消注释用户和用户配置文件工厂中的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)>'
Run Code Online (Sandbox Code Playgroud)

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

cde*_*ers 9

当模型A has_one模型B时,这意味着B将外键存储到A中,就像模型C has_many模型D表示D将外键存储到C中一样.该has_one关系简单地表达了您希望只允许B中的一个记录到保持一个特定的外键A.鉴于这种情况,你应该摆脱user_profile_idusers架构,因为它不被使用.只有user_idUserProfile被使用.

您仍然可以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 => :userUser,它可以让UserProfile知道它的存在User物,甚至无论是之前已经坚持并分配一个ID.

最后,您可以在创建新用户时包含一个before_createUser来构建关联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
Run Code Online (Sandbox Code Playgroud)

UPDATE

我错误地认为验证回调顺序.验证在before_create调用回调之前运行,这意味着在构建之前User检查是否存在UserProfile.

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

另一方面,如果你真的发现有两个独立的模型是有价值的,也许你不应该使用验证来维持它们的共存.在我看来,模型验证通常应该用于让用户知道他们提交的数据有他们需要修复的错误.然而,user_profile他们的user对象缺少一个他们无法修复的东西.所以,也许更好的解决方案是让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
Run Code Online (Sandbox Code Playgroud)

  • 我认为你想要的回调是`before_validation:build_user_profile,on :: create` (2认同)