Let’s assume we have a rails app that has a model entity needing a discrete property. As an example, let’s pretend it’s a Person needing a gender. In C# or Java we could accomplish this by using an Enumeration. Unfortunately, in Ruby there’s not a built-in enumeration type. There are ways to work around this. This post explains one way to accomplish this in Rails 3 (some adjustments may be needed for Rails <= 2).
The Person Class
So far, all we have is an empty Person class as generated by Rails scaffolding.
class Person < ActiveRecord::Base end
What we’ll need to do is add a gender property to it.
Some solutions I ran into online solve this situation by persisting a String, which in this case would define the gender value (“Male” or “Female”). I don’t like that approach for 2 reasons: first it is using a String to represent a discrete value that should only have 2 statuses, and second it will require more resources to persist these data unnecessarily. And that’s just the beginning – think about internationalization just to give you more arguments against it.
Other solutions call for adding a totally new entity (Gender) in our rails model and creating a foreign relationship to it. Once again, this wastes unnecessary resources (why do we need a new Gender DB table just to hold two constant values? Moreover, Gender is not really an entity in our domain model.) Not a good solution, IMHO.
In short, what we’re aiming for is an int that represents our gender that we can easily persist. We’ll also take care of showing the right string in the Views.
Adding the Gender Constants and The Appropriate Validation
If you’ve already added a gender property (as an int) to your Person entity via scaffolding, the first thing we’ll do is define our Gender constants. We’ll define them as Hashes in order to also store their corresponding display name (you’ll see why when we adjust the Views). We’ll also add the validation of the gender property. Here’s what our Person class looks like now:
class Person < ActiveRecord::Base
MALE = { :value => 0, :display_name => "Male" }
FEMALE = { :value => 1, :display_name => "Female" }
validates_inclusion_of :gender, :in => [MALE[:value], FEMALE[:value]]
end
The validates_inclusion_of definition in our class will make sure that only 0 or 1 are assigned/persisted to/with our Person instances. The next step is to adjust our Views to use our new convention.
Fixing our Views
Let’s go ahead and add a couple of methods in our Person class to help us with the displaying of gender in our Views. The first one is a class method that will give us the list of options to be displayed when we want to render a dropdown list for our new property. Add this method to your Person class:
def self.get_genders_dropdown
options = {
MALE[:display_name] => MALE[:value],
FEMALE[:display_name] => FEMALE[:value]
}
end
The second method will provide us with the corresponding gender string for the Person instance we’re handling at any point in time. Here it is:
def gender_displayname
return gender == MALE[:value] ?
MALE[:display_name] :
FEMALE[:display_name]
end
Now that we’re armed with these “helper” methods (which we can actually unit test), let’s adjust our views. First, in the _form.html.erb file generated by scaffolding, find the line for our gender input field and replace it with this:
<%= f.select(:gender, Person.get_genders_dropdown) %>
Note: This will generate a dropdown. You could use radio buttons instead, which would be better in this case. I’ll leave that to you as an exercise.
The last step is to go ahead and display the right string for the instance gender on the Views where the user is shown info about the Person gender. In particular, look for index.html.erb and show.html.erb and use this code line to replace “person.gender”:
<%= person.gender_displayname %>
That’s it. This will give us the ability to store gender as 0 or 1 in our DB and display it as Male or Female (correspondingly) for our Person entity.
Refactoring: Going the extra mile
You may have noticed that our Person class is a little “unclean.” Besides, what if we have another entity in our domain model (e.g., Animal) that also has a gender. We don’t want to have to retype all that for our second (third, fourth…nth) entity. In this Stackoverflow post, Jaime Bellmyer explains a neat way to do this using Rails’ plugin paradigm. Here’s a summary of the steps:
1) Extracting the common code out of our Person class: Basically add a new module in your rails’ project lib folder that will contain the common code (I called the file acts_as_gendered.rb):
module ActsAsGendered
MALE = { :value => 0, :display_name => "Male" }
FEMALE = { :value => 1, :display_name => "Female" }
def self.included(base)
base.extend(ActsAsGenderedMethods)
end
module ActsAsGenderedMethods
def acts_as_gendered
extend ClassMethods
include InstanceMethods
validates_inclusion_of :gender, :in => [MALE[:value], FEMALE[:value]]
end
end
module ClassMethods
def get_genders_dropdown
options = {
MALE[:display_name] => MALE[:value],
FEMALE[:display_name] => FEMALE[:value]
}
end
end
module InstanceMethods
def gender_displayname
return gender == MALE[:value] ?
MALE[:display_name] :
FEMALE[:display_name]
end
end
end
As you can tell some of the code applies to the class, whereas others to an instance of it; hence the need to extend/include the ClassMethods/InstanceMethods inner modules. The self.included method will just take care of the wiring when this module is mixed-in.
2) The next step is to automatically mix-in this new module on all our ActiveRecord::Base classes. This will save us some manual work when we want to add gender to an entity. To do this, add a new file under config/initializers (I called it gender.rb) with this code:
require 'acts_as_gendered' ActiveRecord::Base.send(:include, ActsAsGendered)
Note: This is only called during the environment initialization, so if you already have a server instance running, you’d need to restart it before being able to use it (i.e., a simple update of an entity and reload of its views in rails won’t suffice).
3) The last step is to actually enable this new code in the appropriate entities. In our example, simple refactor your Person class to have this code:
class Person < ActiveRecord::Base acts_as_gendered end
As you can see the acts_as_gendered() method is the only thing we need now to add gender to our entities. Don’t forget to use the get_genders_dropdown() and the gender_displayname() methods in the appropriate views and you are set.