Search code examples
ruby-on-railsactiverecordhas-one

Rails: Create association if none is found to avoid nil errors


I have an application where my users can have a set of preferences. Both are stored as ActiveRecord-models as follows:

class User < AR::Base
   has_one :preference_set
end

class PreferenceSet < AR::Base
   belongs_to :user
end

I can now access the preferences of a user:

@u = User.first
@u.preference_set => #<PreferenceSet...>
@u.preference_set.play_sounds => true

But this fails if a preference set is not already created, since @u.preference_set will be returning nil, and I'll be calling play_sounds on nil.

What I want to archive is that User.preference_set always returns a PreferenceSet instance. I've tried defining it like this:

class User < ..
   has_one :preference_set

   def preference_set
     preference_set || build_preference_set
   end
end

This is causing a 'Stack level too deep', since it is calling itself recursively.

My question is this:

How can I ensure that @user.preference_set returns either the corresponding preference_set-record or, if none exists, builds a new one?

I know I could just rename my association (eg. preference_set_real) and avoid recursive calls this way, but for the sake of simplicity in my app, I'd like to keep the naming.

Thanks!


Solution

  • Well the best way to do this is to create the associated record when you create the primary one:

    class User < ActiveRecord::Base
       has_one       :preference_set, :autosave => true
       before_create :build_preference_set
    end
    

    That will set it up so whenever a User is created, so is a PreferenceSet. If you need to initialise the the associated record with arguments, then call a different method in before_create which calls build_preference_set(:my_options => "here") and then returns true.

    You can then just normalise all existing records by iterating over any that don't have a PreferenceSet and building one by calling #create_preference_set.

    If you want to only create the PreferenceSet when it is absolutely needed, then you can do something like:

    class User < ActiveRecord::Base
       has_one :preference_set
    
       def preference_set_with_initialize
         preference_set_without_initialize || build_preference_set
       end
    
       alias_method_chain :preference_set, :initialize
    end