我有一个带有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
有可能做我想做的事吗?
我试过@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_profile
和user
关联,我会得到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)>'
所以我假设用户模型中的更改不起作用。另请注意,我从users表中删除了user_profile_id
。
答案 0 :(得分:9)
当模型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
,让UserProfile
知道其User
对象的存在,甚至在其中任何一个被保留并分配了ID之前。
最后,您可以在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模型中获得了什么价值。鉴于它们如此紧密,以至于没有另一个就不能存在,它是否有意义(并且可能简化了很多代码)将它们组合成一个模型?
另一方面,如果你真的发现有两个独立的模型是有价值的,也许你不应该使用验证来维持它们的共存。在我看来,模型验证应该通常用于让用户知道他们提交的数据有他们需要修复的错误。但是,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
,而不仅仅是抱怨,你可以更进一步,只需构建它。任何一方都不需要验证。
{{1}}
答案 1 :(得分:0)
您无法验证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
答案 2 :(得分:0)
这个轨道强制转换为嵌套表单,(同时创建user / user_profile)。 http://railscasts.com/episodes/196-nested-model-form-part-1您需要做一些修改,因为它涵盖了has_many,但您应该能够弄明白。
答案 3 :(得分:0)
这里有一个嵌套问题。可以通过在用户表单上嵌套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