Symbolize attribute values in ActiveRecord
Update: I put together the code below and created a rails plugin from it. It's called activerecord_symbolize on Github.
ActiveRecord does not natively suppport column types of ENUM or SET. If you want an attribute to act like an ENUM, you'll most probably use a string and restrict it to certain values using validates_inclusion_of
. However, once you've got used to Ruby, you'd probably prefer to have symbols as values for these attributes. Here's a small and easy-to-use snippet that can be used to symbolize values of any ActiveRecord attribute.
Simply drop this code snippet into your Rails application (e.g. put it into the lib directory and add a require
-statement to config/environment.rb):
module ActiveRecord
class Base
# Specifies that values of the given attributes should be returned
# as symbols. The table column should be created of type string.
def self.symbolize (*attr_names)
attr_names.each do |attr_name|
attr_name = attr_name.to_s
class_eval("def #{attr_name}; read_and_symbolize_attribute('#{attr_name}'); end")
class_eval("def #{attr_name}= (value); write_attribute('#{attr_name}', value.to_s); end")
end
end
# Return an attribute's value as a symbol
def read_and_symbolize_attribute (attr_name)
value = read_attribute(attr_name)
value.blank? ? nil : value.to_sym
end
end
end
The above code will enhance ActiveRecord::Base with a class method named symbolize
. Calling this method will create a getter and a setter method for each specified attribute. The new getter will retrieve the string value of an attribute return it as a symbol (using tosym. a blank string will become nil). The new setter converts any value to a string (using tos) before passing it to ActiveRecord.
Now, your pseudo-enums can be symbolized and look more rubyish:
class User < ActiveRecord::Base
symbolize :status
validates_inclusion_of :status, :in => [ :unconfirmed, :active, :disabled ]
end
You can now use symbols instead of strings for attribute values almost everywhere, because attributes are usually accessed through the getter and setter methods (e.g. the above validation works fine). Plus you can still access the original value with read_attribute
.
However, there are some cases, where using a symbol does not work properly, e.g. when specifying a :scope for validates_uniqueness_of
. (and probably at other places, where ActiveRecord builds a SQL filter for a symbolized attribute). To build the SQL query filter, ActiveRecord gets the quoted value of a value by calling the quoted_id
method. A symbol (which is an object of class Symbol) does not have this method and therefore ActiveRecord converts the value to YAML instead. This results in queries like this:
... AND status = '--- :active\n'
The solution is not hard... Simply add a quoted_id
method to Symbol
:
class Symbol
def quoted_id
# Since symbols always contain save characters (no backslash or apostrophe), it's
# save to skip calling ActiveRecord::ConnectionAdapters::Quoting#quote_string here
"'#{self.to_s}'"
end
end
ActiveRecord now quotes symbolic values correctly:
... AND status = 'active'
Actually, I'd prefer to add the quoted_id
method only to symbols that are returned by read_and_symbolize_attribute
, but unfortunately, Symbol
is an immediate value and therefore, you cannot add a singleton method to symbols. However I didn't encounter any side effects yet by applying quoted_id
globally to Symbol
.
The above code is just a small hack I found to be useful in my applications. If you need more enumeration features than just symbolized attribute values, there's e.g. an enum-column plugin for rails (however, I didn't check it out and I don't know if it works with edge rails).