More on Using Enums For Constant Data in Rails

So I got around to rollowing tip #3 on using Enums in AR, and found it worked…. mostly.  The problem comes in where the value isn’t already set.  I started with this in my model:

# game.rb
  # at the top of the file, define the list of genders
  GENDERS = %w( boy girl coed )
  # and validations for it
  validates_inclusion_of :gender,   :in => Game::GENDERS, :on => :create, :message => "extension %s is not included in the list"
 
  # finally define the gender as a symbol for lookups
  def gender
     read_attribute(:gender).to_sym
  end
  def gender=(value)
    write_attribute(:gender, value.to_s)
  end

This works fine until you have a nil value for gender.  OK, next step, just check if the value is nil before you read it and return nil if it is.  Only thing is that if you were to add something like

if self.gender.nil? return nil

But then you get an ugly “stack level too deep” error, because when you call ‘self.gender’ it’s calling the gender method, which checks to see if self.gender is nil, which calls the gender method, which… well, you get the picture.

Took a bit of looking, and I’m not sure if this is the “correct” solution, but it does work properly.  I just modified the gender method as such:

#game.rb
  def gender
    attributes = attributes_before_type_cast
    if attributes["gender"]
      read_attribute(:gender).to_sym
    else
      nil
    end
  end

This uses the attributes_before_type_cast grabs all attributes into a hash (before they are mangled by whatever ActiveRecord does), checks to see if the ‘gender’ attribute is filled in and either returns it or nil.  Depending on if you’re learning or not, you may want to just check out the activerecord_symbolize plugin though :)

All working, and ready to commit to the main branch.

Related posts (maybe):

  1. Using Enums For Constant Data in Rails and ActiveRecord
  2. Better Way To Do Dynamic Methods

5 Responses to “More on Using Enums For Constant Data in Rails”

  1. Andrew Vit Says:

    Note: you can use super to avoid the recursion when calling gender inside your gender method.

    The question here becomes whether nil is an accepted value for your enum. I would say that for the case of gender it isn’t, so you’d want to ensure that you set a value before saving the model the first time.

    There’s a few ways to do this.

    1. Validation. Throw it back with an error if nothing was set. Easy, and most often preferred.

    2. Callback before_validate and set a default value. Probably not appropriate for a gender, but in some cases, VALID_OPTIONS.first is the appropriate thing.

    3. Initialization. If you need to have a value defined on the first use of the model before it’s even saved, you can define an after_initialize method. This is most often not the right call because it’s more expensive for all other uses of the model.

    Why do you need to check attributes_before_type_cast? Couldn’t you do something like this:

    def gender
    attr = read_attribute(:gender)
    attr.to_sym if attr.present?
    end

  2. Andrew Vit Says:

    One more thing: check out the attribute_normalizer plugin. I have a fork at http://github.com/avit/attribute_normalizer which adds options to transform attribute values on read or write.

  3. alan Says:

    Nice stuff Andrew, going to look at that on the way home tonight. I figured that nil is not a valid option, but that’s only applicable when dealing with data already entered (course, that’s more an exercise for me than a 100% valid concern).

    I’ll definitely be looking at read_attribute and the callbacks (callbacks are an area I haven’t ventured into yet).

    Thanks again for the great tips!

  4. eAlchemist Says:

    Check out https://github.com/electronick/enum_column for Rails 3

  5. Damon Says:

    Sorry, this is a little bit old, but I stumbled on your blog trying to do an enum for gender field =)

    Can I do this?

    def gender
    read_attribute(:gender).to_sym unless read_attribute(:gender).nil?
    end

    I’m a noob so I don’t know if doing so creates any other problem.

Leave a Reply