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

2

19

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!

Desorb answered 28/9, 2010 at 12:29 Comment(0)
L
30

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
Lutz answered 28/9, 2010 at 13:1 Comment(1)
Be advised that alias_method_chain has been deprecated in favor of Module#prepend. More info here: github.com/rails/rails/pull/19434Rupp
P
63

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.

Predicate answered 24/7, 2013 at 5:38 Comment(1)
This is what I was looking for.Nixie
L
30

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
Lutz answered 28/9, 2010 at 13:1 Comment(1)
Be advised that alias_method_chain has been deprecated in favor of Module#prepend. More info here: github.com/rails/rails/pull/19434Rupp

© 2022 - 2024 — McMap. All rights reserved.