83

What's the best practice to create has_one relations?

For example, if I have a user model, and it must have a profile...

How could I accomplish that?

One solution would be:

# user.rb
class User << ActiveRecord::Base
  after_create :set_default_association

  def set_default_association
    self.create_profile
  end
end

But that doesn't seem very clean... Any suggestions?

Null
  • 1,950
  • 9
  • 30
  • 33
BvuRVKyUVlViVIc7
  • 11,641
  • 9
  • 59
  • 111

8 Answers8

134

Best practice to create has_one relation is to use the ActiveRecord callback before_create rather than after_create. Or use an even earlier callback and deal with the issues (if any) of the child not passing its own validation step.

Because:

  • with good coding, you have the opportunity for the child record's validations to be shown to the user if the validations fail
  • it's cleaner and explicitly supported by ActiveRecord -- AR automagically fills in the foreign key in the child record after it saves the parent record (on create). AR then saves the child record as part of creating the parent record.

How to do it:

# in your User model...
has_one :profile
before_create :build_default_profile

private
def build_default_profile
  # build default profile instance. Will use default params.
  # The foreign key to the owning User model is set automatically
  build_profile
  true # Always return true in callbacks as the normal 'continue' state
       # Assumes that the default_profile can **always** be created.
       # or
       # Check the validation of the profile. If it is not valid, then
       # return false from the callback. Best to use a before_validation 
       # if doing this. View code should check the errors of the child.
       # Or add the child's errors to the User model's error array of the :base
       # error item
end
Larry K
  • 47,808
  • 15
  • 87
  • 140
  • Could that be also managed with a single line? -> before_filter :build_profile ? – BvuRVKyUVlViVIc7 Sep 28 '10 at 03:28
  • 2
    @Lichtamberg: Yes, but I'd add a comment: "Builds default profile. MUST always validate." NOTE: it'd be "before_create :build_profile" not 'before_filter'. If it didn't validate then you'd get a very confusing error msg to the user. Or it would NOT in fact be created which would mean you'd end up with a User without a profile. You should also test the corner cases in your tests. – Larry K Sep 28 '10 at 03:40
  • I tried this code but it failed, I had to do profile = build_profile – jakeonrails Feb 24 '11 at 22:33
  • Thanks for the tip with the before_create, I was using with after_create. – Rafael Oliveira Mar 24 '15 at 21:01
  • 1
    Then how do you add this object to the new `user`, since this is `before_create`? – Meekohi Feb 26 '16 at 21:11
  • 10
    Just be aware with Rails 5 using before_create to create the dependent record not be possible without overriding the defaults for the belongs_to record. The default now expects the belongs_to record to exist else an error is thrown. – Viet Jul 22 '16 at 23:57
30

Your solution is definitely a decent way to do it (at least until you outgrow it), but you can simplify it:

# user.rb
class User < ActiveRecord::Base
  has_one      :profile
  after_create :create_profile
end
sites
  • 21,417
  • 17
  • 87
  • 146
Bo Jeanes
  • 6,294
  • 4
  • 42
  • 39
29

If this is a new association in an existing large database, I'll manage the transition like this:

class User < ActiveRecord::Base
  has_one :profile
  before_create :build_associations

  def profile
    super || build_profile(avatar: "anon.jpg")
  end

private
  def build_associations
    profile || true
  end
end

so that existing user records gain a profile when asked for it and new ones are created with it. This also places the default attributes in one place and works correctly with accepts_nested_attributes_for in Rails 4 onwards.

inopinatus
  • 3,597
  • 1
  • 26
  • 38
  • This is a brilliant solution, but we're using super || create_profile(avatar: "anon.jpg") to make sure the new profile is persisted imediatelly. – sandre89 Dec 04 '18 at 23:28
  • 1
    @sandre89 you are risking a callback nightmare, validation exceptions, and incorrectly persisted records, and more besides, by using a persistence method in a before_create callback. Avdi Grimm wrote a /four-part series/ on RubyTapas covering the many catastrophes that await you from having the model layer do any kind of self-save. I wish you the best, but I fear the worst. – inopinatus Dec 06 '18 at 00:05
9

Probably not the cleanest solution, but we already had a database with half a million records, some of which already had the 'Profile' model created, and some of which didn't. We went with this approach, which guarantees a Profile model is present at any point, without needing to go through and retroactively generate all the Profile models.

alias_method :db_profile, :profile
def profile
  self.profile = Profile.create(:user => self) if self.db_profile.nil?
  self.db_profile
end
Andrew Vilcsak
  • 3,429
  • 2
  • 25
  • 14
5

Here's how I do it. Not sure how standard this is, but it works very well and its lazy in that it doesn't create extra overhead unless it's necessary to build the new association (I'm happy to be corrected on this):

def profile_with_auto_build
  build_profile unless profile_without_auto_build
  profile_without_auto_build
end

alias_method_chain :profile, :auto_build

This also means that the association is there as soon as you need it. I guess the alternative is to hook into after_initialize but this seems to add quite a bit of overhead as it's run every time an object is initialized and there may be times where you don't care to access the association. It seems like a waste to check for its existence.

Brendon Muir
  • 4,540
  • 2
  • 33
  • 55
  • I think this answer is better solution then others, because avoid problem with validation in Profile model. Thanks – ole Mar 30 '13 at 00:25
  • you can also have autosave: `has_one :profile, :autosave => true` – montrealmike Mar 11 '14 at 19:41
  • @montrealmike, does that deal with a missing profile to begin with? I.e. if one hasn't already executed build_profile, would this create one on save? I've also come across this: https://github.com/phildionne/associates which might provide another way around multi-model forms. – Brendon Muir Mar 12 '14 at 07:12
  • @BrendonMuir no it won't build a missing profile. It's just something you can add to your solution above so that it automatically saves the profile when you save the user (and also validate the profile) – montrealmike Mar 12 '14 at 13:08
1

There is a gem for this:

https://github.com/jqr/has_one_autocreate

Looks like it is a bit old now. (not work with rails3)

linjunhalida
  • 4,538
  • 6
  • 44
  • 64
0

I had an issue with this and accepts_nested_attributes_for because if nested attributes were passed in, the associated model was created there. I ended up doing

after_create :ensure_profile_exists
has_one :profile
accepts_nested_attributes_for :profile


def ensure_profile_exists
  profile || create_profile
end
kkelleey
  • 21
  • 3
0

If you need the has_one association to exist before saving the object (when testing, for instance), you should use the after_initialize callback instead. Here is how it could be applied to your use case:

class User < ActiveRecord::Base
  has_one :profile
  after_initialize :build_profile, unless: :profile
end
Goulven
  • 777
  • 9
  • 20