Rails: имеет один и принадлежит к с проверкой присутствия на обоих внешних ключах
У меня есть приложение с таблицей 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_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)>'
Поэтому я предполагаю, что изменение в пользовательской модели не сработало. Также обратите внимание, что я удалил user_profile_id
из таблицы users.4 ответа:
Когда модель A
has_one
модель B, это означает, что B хранит внешний ключ в A, точно так же, как модель Chas_many
Модель D означает, что D хранит внешний ключ в C. отношениеhas_one
просто выражает ваше желание разрешить только одной записи в B хранить определенный внешний ключ в A. учитывая это, вы должны избавиться отuser_profile_id
из схемыusers
, потому что она не используется. Используется толькоuser_id
изUserProfile
.Вы все еще можете иметь
User
проверить наличиеUserProfile
, но использоватьvalidates_presence_of :user_profile
вместо. Это позволит проверить, что объект user имеет связанный объект user_profile.Ваш объект
Наконец, вы можете включить блокUserProfile
не должен проверять непосредственноuser_id
, так как этот идентификатор еще не будет существовать при создании новой пары user-user_profile. Вместо этого используйтеvalidates_presence_of :user
, который проверит, чтоUserProfile
имеет связанный объектUser
перед его сохранением. Затем запишитеhas_one :user_profile, :inverse_of => :user
вUser
, что позволяет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 и user_profile. Учитывая, что они настолько тесно связаны, что одно не может существовать без другого, имеет ли смысл (и, возможно, упростить большую часть вашего кода) просто объединить их в единую модель?
С другой стороны, если вы действительно находите, что есть ценность в наличии двух отдельных моделей, возможно, Вам не следует использовать проверки для поддержания их взаимного существования. На мой взгляд, модельные валидации должны обычно используется для того, чтобы пользователи знали, что данные, которые они представили, содержат ошибки, которые им необходимо исправить. Однако отсутствиеuser_profile
у ихuser
объекта не является чем-то, что они могут исправить. Поэтому, возможно, лучшим решением будет построить объектuser
, если его нет. Вместо того чтобы просто жаловаться, что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_profile_id, потому что он не существует. Has_one означает, что другая модель имеет ссылку на внешний ключ.
Способ, которым я обычно обеспечиваю поведение, которое вы ищете, заключается в условном создании модели со ссылкой на внешний ключ, когда создается модель, на которую ссылаются. В вашем случае это будет создание профиля
after_create
для пользователя следующим образом:class User < ActiveRecord::Base ... after_create :create_profile private def create_profile self.user_profile.create end end
Это приведение рельса идет над созданием вложенных форм, (чтобы создать оба пользователя/user_profile вместе). http://railscasts.com/episodes/196-nested-model-form-part-1 есть некоторые модификации, которые вам нужно сделать, так как он охватывает has_many, но вы должны быть в состоянии понять это.
У вас здесь проблема с гнездованием. Это можно решить, вложив поля формы для 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