zargony.com

#![desc = "Random thoughts of a software engineer"]

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).