Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 761
Mattias Avatar asked Sep 28 '10 12:09

Mattias


2 Answers

There's an elegantly simple form:

class User < ApplicationRecord
  has_one :preference_set
  
  def preference_set
    super || build_preference_set
  end
end

ActiveRecord intentionally enables such use of super to override the behaviour of association methods.

like image 158
inopinatus Avatar answered Oct 12 '22 11:10

inopinatus


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
like image 26
Bo Jeanes Avatar answered Oct 12 '22 10:10

Bo Jeanes