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 10

4 ответа:

Когда модель 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 имеет связанный объект 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