This post originated from an RSS feed registered with Ruby Buzz
by Obie Fernandez.
Original Post: ActiveRecord Sugar [alias_column]
Feed Title: Obie On Rails (Has It Been 9 Years Already?)
Feed URL: http://jroller.com/obie/feed/entries/rss
Feed Description: Obie Fernandez talks about life as a technologist, mostly as ramblings about software development and consulting. Nowadays it's pretty much all about Ruby and Ruby on Rails.
I was stirring up some ActiveRecord magic today using acts_as_nested_set and single-table inheritance and I found myself wanting an easy way to alias an ActiveRecord property. Rails, in case you haven't heard, automatically adds columns from the database table backing your ActiveRecord model class as property accessors. But when you're using single-table inheritance, there are probably going to be cases where you would do well to put a more semantically meaningful property accessor in place.
Confused? Here's the code...
First up, this is the migration that produced an outline_nodes table in my database:
class AddOutlineNodes < ActiveRecord::Migration
def self.up
create_table "outline_nodes", :force => true do |table|
table.column "type", :string
table.column "raw_text", :text
table.column "html", :text
table.column "parent_id", :integer
table.column "lft", :integer
table.column "rgt", :integer
end
end
def self.down
drop_table "outline_nodes"
end
end
Then our ActiveRecord models (these would normally be in separate files named accordingly):
class OutlineNode < ActiveRecord::Base
acts_as_nested_set
end
class Book < OutlineNode; end
class Chapter< OutlineNode; end
As you can see in the migration code, my OutlineNode class has
a raw_text column which will be exposed as a property of OutlineNode instances, including its subclasses (thank you ActiveRecord single-table-inheritance).
The following unit test passes just fine.
class OutlineNodeTest < Test::Unit::TestCase
fixtures :outline_nodes
def test_outline_nodes
book = outline_nodes(:the_book)
assert_kind_of OutlineNode, book
assert_kind_of Book, book
assert_equal "Title of the Book", book.raw_text
chapter = outline_nodes(:first_chapter)
assert_kind_of Chapter, chapter
assert_equal "Chapter 1", chapter.raw_text
end
end
...and here are the fixtures in outline_nodes.yml
the_book:
id: 1
type: Book
raw_text: "Title of the Book"
first_chapter:
id: 2
type: Chapter
raw_text: "Chapter 1"
What I really want is to be able to refer to this property as book.title instead of book.raw_text, because in this particular case title is much more semantically meaningful and correct. The straightforward way is pretty easy to accomplish.
class Chapter < OutlineNode
def title
raw_text
end
end
You might stop there, but I'm not quite happy with this solution. I'd much rather do a macro-style declaration at the top of the class.
class Chapter < OutlineNode
alias_column :title, :raw_text
end
class Book < OutlineNode
alias_column :title, :raw_text
end
So, let's start by writing another test and we'll see exactly how easy it is to extend ActiveRecord to do your bidding. Notice that in the following test I'm requiring 'extensions', which refers to a new extensions.rb file that I created in the lib directory of my rails app.
require 'extensions'
class ExtensionsTest < Test::Unit::TestCase
fixtures :outline_nodes
def test_alias_column
assert_equal "Title of the Book", outline_nodes(:the_book).title
end
end
And of course it fails...
>testrb test\unit\extensions_test.rb
Loaded suite extensions_test.rb
Started
E
Finished in 0.141 seconds.
1) Error:
test_alias_column(ExtensionsTest):
NoMethodError: undefined method `alias_column' for Book:Class
../vendor/rails/activerecord/lib/active_record/base.rb:1005:in `method_missing'
../app/models/book.rb:2
1 tests, 0 assertions, 0 failures, 1 errors
We add the new macro in the extensions.rb file.
module ActiveRecord
class Base
class << self
def alias_column(logical_name, column_name)
define_method logical_name do
@attributes[column_name.to_s]
end
end
end
end
end
Ah, sweet success... our test passes.
>testrb test\unit\extensions_test.rb
Loaded suite extensions_test.rb
Started
.
Finished in 0.125 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
Now I can refactor my object model a bit and eventually end up with the following class definitions...
class OutlineNode < ActiveRecord::Base
acts_as_nested_set
end
class Book < OutlineNode
alias_column :title, :raw_text
end
class Heading < OutlineNode
alias_column :title, :raw_text
end
class Chapter < Heading
# the alias_column setting is inherited
end
class Paragraph < OutlineNode
alias_column :body, :raw_text
end
How it works
I won't go into specifics of this implementation code, just like I didn't give a bunch of details about how Rails unit test fixtures work, etc. However, I will point you in the right direction by telling you that Ruby lets you open up specific instances of previously defined classes and change their implementation. In fact, it is considered correct style in Ruby to ExtendRatherThanWrap. What I've done is open up the specific singleton instance of the ActiveRecord::Baseclass and add a method to it (that when invoked, adds a method to its instances). Bingo -- a brand-spanking-new macro-style method available on ActiveRecord.
Wicked, huh? By the way, once you get used to coding this way, I guarantee you will really begin to wonder how you ever enjoyed Java programming at all. Especially if you get hooked on learning advanced Ruby techniques like metaclass programming via the zany writing of why_ the lucky stiff? ;)