I have an application with a users table and a user_profiles table. A user has_one user profile and a user profile belongs_to a user.
I want to make sure that the association scenario is always true, so I’ve put a validation for the presence of both foreign keys. The problem is that I hit a “chicken and egg” situation. When I create a user, it doesn’t work because the user profile doesn’t exist yet, and when I create a user profile, it doesn’t work either because the user doesn’t exist yet. So I need to create the user profile within the creation of the user. To complicate things, when I create a client, I also create a user in an after_create callback. Enough talking (or reading/writting), here is some code:
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
Is it possible to do what I want to do?
Update
I tried the solution @cdesrosiers suggested, but I can’t make my specs pass. I am mainly having three errors. First let me show you the updated models:
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
I am using FactoryGirl to create factories, here is my factories file:
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
If I uncomment the user_profile and user associations in the user and user profile factories respectively, I get a WARNING: out of shared memory.
Now, when I create one of those factories, I get one of those three errors:
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)>'
So I assume that the change in the User model didn’t work. Also note that I removed the user_profile_id from the users table.
When model A
has_onemodel B, this means that B stores the foreign key into A, in the same way that model Chas_manymodel D means that D stores the foreign key into C. Thehas_onerelation simply expresses your desire to allow only one record in B to hold a particular foreign key into A. Given that, you should get rid ofuser_profile_idfrom theusersschema, because it isn’t used. Onlyuser_idfromUserProfileis used.You can still have
Usercheck for the presence ofUserProfile, but usevalidates_presence_of :user_profileinstead. This will check that the user object has an associated user_profile object.Your
UserProfileobject should not check directly for auser_idsince this id won’t yet exist when creating a new user-user_profile pair. Instead usevalidates_presence_of :user, which will check that theUserProfilehas an associatedUserobject before saving it. Then writehas_one :user_profile, :inverse_of => :userinUser, which lets theUserProfileknow about the presence of itsUserobject, even before either has been persisted and assigned an id.Finally, you can include a
before_createblock inUserto build the associatedUserProfilewhen creating a new user. (I believe) it will run validations after building a new user_profile, so these should pass.In summary,
UPDATE
I was mistaken about the validation-callback order. The validation runs before the
before_createcallback is called, which meansUseris checking for the presence of aUserProfilebefore one is even built.One solution is to ask yourself what value you get from having separate user and user_profile models. Given that they are so tightly bound that one cannot exist without the other, would it make sense (and perhaps simplify a lot of your code) to just combine them into a single model?
On the other hand, if you really find that there is value in having two separate models, perhaps you shouldn’t use validations to maintain their mutual existence. In my opinion, model validations should generally be used to let users know that the data they have submitted have errors they need to fix. However, the absence of a
user_profilefrom theiruserobject is not something they can fix. So, perhaps the better solution is to have theuserobject build auser_profileif there isn’t one. Instead of just complaining if auser_profiledoesn’t exist, you take it a step further and just build it. No validation required on either side.