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):
April 20th, 2010 at 2:07 pm
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
April 20th, 2010 at 2:09 pm
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.
April 20th, 2010 at 2:15 pm
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!
March 17th, 2011 at 12:03 am
Check out https://github.com/electronick/enum_column for Rails 3
January 11th, 2012 at 8:47 pm
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.