Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Overload ORM safely or Black magic of metaprogramming
Overload your ORM safely or Black magic of metaprogramming
==========================================================
Introduction
------------
Ruby provides the great set of metaprogramming features.
Metaprogramming is a bit like magic, which makes something astonishing possible.
But there are two kinds of magic: white magic, which does good things, and black magic, which can do nasty things.
And unfortunately sometimes it is so easy to fall into the dark side of metaprogramming.
Scope
-----
- Ruby ORM libraries (ActiveRecord and Sequel);
- ORM methods are the methods dynamically provided to you by your ORM based on the attributes in the table.
E.g.: class Project < ActiveRecord::Base; end; Project.first.title # Project#title is the ORM method
White side of magic (metaprogramming)
-------------------------------------
ActiveRecord is probably one of the best examples that implies the majority of ruby's metaprogramming tricks.
Everything is simple - you just subclass the ActiveRecord::Base class. You don't need to specify the table name.
Even better, you don't need to write the boring accessors such as title(), name() and so on for your models.
ActiveRecord defines them automatically, after looking their names from the database schema.
ActiveRecord::Base reads the schema at runtime, discovers the columns in the table and defines the accessors for the attributes.
So everyting just works...
E.g.: project = Project.create; project.name = 'New project'; project.name # => 'New project'
Black side of magic (metaprogramming)
-------------------------------------
Let's take a look at a very common situation when we want to overload one of the ORM methods.
We have:
class User < ActiveRecord::Base; end # We also have `users` table with `name` column
We want:
describe 'User' do
it 'should return name capitalized' do
u = User.new(:name => 'bob')
u.name.should == 'Bob'
end
end
A very common mistake is to go with something like this:
class User < ActiveRecord::Base
def name
self.name.to_s.capitalize
end
end
The obvious bug will cause "SystemStackError" error.
Another common mistake is to go with:
class User < ActiveRecord::Base
def name
super.to_s.capitalize
end
end
The wrong use of super keyword is so obvious that we want to close the eyes, but look at the test result:
.
Finished in 0.09188 seconds
1 example, 0 failures
Actually this is the revealed example of "black magic" of meta-programming. Something that doesn't make sense, makes our test to pass and returns a "pseudo" correct result.
To prove that we got something weird here lets take a look at this example:
def name
'hi ' + super.to_s.capitalize
end
We want:
describe 'User' do
it 'should return hi with name capitalized' do
u = User.new(:name => 'bob')
u.name.should == 'hi Bob'
end
end
But we got:
Failure/Error: name.should == 'hi Bob'
expected: "hi Bob"
got: "hi Hi bob" (using ==)
Let's take a look at how it could happen:
<<Ruby trusts you. Ruby treats you as a grown-up programmer. It gives you great power such as meta-programming. But you need to remember that with great power comes great responsibility.>>
-- Matz - creator of MRI, October 2009
@metaskills
Copy link

metaskills commented Apr 28, 2011

You are not supposed to use super when defining getters/setters for AR attributes. The documented form is like so.

class User < ActiveRecord::Base
  def name
    self[:name].to_s.capitalize
  end
end

There are tons of good docs on this and even hooking into the dirty and xxx_will_change! methods.

@ipoval
Copy link
Author

ipoval commented Apr 28, 2011

I'm totally agree, and as I mentioned it is a mistake to use the "super" in that context. However, the technical result of using the "super" is probably not such a big mistake. IMHO: when we use "super" there we make a bigger semantic mistake than technical.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment