Rader on Rails

Dispatches from my web development journey.

Fun With Metaprogramming

If you haven’t read my post on Active Record Jr., this post may not make a lot of sense – so give it a quick glance before you start reading this one.

I’ve heard through the grapevine (reading blogs, watching videos on the topic), that some programmers complain about Rails and Active Record because there’s too much magic. It abstracts a lot of work into methods so simple and intuitive that people with very little experience with programming can manage to throw simple apps together (that was me in the beginning).

While this is true, the ‘magic’ of Active Record isn’t magic at all – it’s just code. Really good code. And it provides a fascinating study in the concept of metaprogramming, which can be defined as “the writing of computer programs that write or manipulate other programs (or themselves) as their data, or that do part of the work at compile time that would otherwise be done at runtime.”.

Active Record implements a lot of metaprogramming. Let’s say you have a table of posts in your database, and each record has the attributes of title, content and author.

After creating your table and model, inheriting from ActiveRecord::Base, you will suddenly have methods that correspond directly to the attributes of your model. For example, if you want to find a post by a particular title, you can run ‘Post.find_by_title()’, passing in a title you know about, and it will return the post(s) with that title. The same goes for looking up records by body and author (author_id).

The magic lies in the fact that these methods only exist once you create your models. How are they created? With metaprogramming tactics.

For example, the dynamic find_by_ methods are defined with methods that look something like this:

1
2
3
4
5
6
7
def define_method_attribute(attr)
  module_eval <<-STR
    def #{attr}
      read_attribute(attr)
    end
  STR
end

This was an example of code given by Anthony Lewis during a talk on (‘Demystifying Active Record’)https://www.youtube.com/watch?v=86BQLyNpybA#t=566; it’s not exactly what’s inside Active Record’s source code, but it illustrates the basic idea.

module_eval() is a method that can be used to add methods to a class. I won’t get into the mechanics of that right now – the important things to note is that it can be used to add methods to a class. Why would you want to do that?

In the case of Active Record, a method like this can be extremely useful in dynamically adding methods to classes that inherit from the base [object relational mapper (ORM)][http://en.wikipedia.org/wiki/Object-relational_mapping] that interacts with your database (in this case, ActiveRecord::Base). As we saw in the Active Record Jr. exercise, it’s not all that hard to evaluate the attributes of any given model and use that to make any class aware of its attributes. If we can make any class aware of its attributes, we can also dynamically generate methods that make it easy to query our database based on those specific attributes.

Going back to Active Record Jr – we had two models – Cohort and Student. Cohorts have names and many students that belong to them. Students have names, birth dates, cohort IDs (of the cohorts they belong to), among other attributes. What if we wanted to be able to get and set any of the attributes we wanted? Specifically, wouldn’t it be nice if we could simply run:

1
2
3
4
5
student = Student.find(1)
student.first_name
# => "Jared"
student.cohort
# => #<Cohort id: 1, name: "Fiddler Crabs">

There is a way, and it can be accomplished with metaprogramming methods.

Defining methods on the fly

Recall in my first post on Active Record Jr, we could access the attributes of a student instance by calling the specific attribute key, i.e. student[:first_name]. We accomplished this using the Class#inherited method, which incvokes a callback whenever a subclass of the current class is created, re: Ruby Docs) to create the attribute names for our models as soon as our models are created:

database_model.rb
1
2
3
4
5
def self.inherited(klass)
  table_name = klass.pluralize_name
  klass.attribute_names = Database::Model.execute("PRAGMA table_info(#{table_name})").map do
    |col| col['name'].to_sym
  end

If we wanted to have the cleaner syntax of calling student.first_name, we need a way to dynamically add these methods on the fly, and Ruby gives us a number of methods that can accomplish this. Here I’ll use define_method():

database_model.rb
1
2
3
4
5
6
7
8
9
def self.inherited(klass)
  table_info = Database::Model.execute("PRAGMA table_info(#{klass.pluralize_name})")
  klass.attribute_names = table_info.map { |col| col['name'].to_sym }

  klass.attribute_names.each do |meth|
    define_method(meth) { @attributes[:"#{meth}"] }
    define_method("#{meth}=") { |other| @attributes[:"#{meth}"] = other }
  end
end

define_method() takes an argument (the method) and a code block telling it what to return. In this case, the method will return the result of @attributes:[:#{meth}], which is how we were accessing the attributes before (student[:first_name]). Now, any attribute we give to students will be available as a method on student instances.

We also create setter methods for each attribute so we can easily change attributes, via define_method("#{meth}=") .... The code block takes a placeholder variable, which I called other, and the attribute is set to the other value.

Now I can write student.first_name = 'Jared' and it will change the first name attribute. Pretty sweet.

Dynamic relations

Using metaprogramming, it’s easy to create an Active Record-like belongs_to and has_many methods we can execute in our classes to easily be able to retrieve related records.

Right now, our Student and Cohort models look like this:

cohort.rb
1
2
3
4
5
6
class Cohort < Database::Model

  def students
    Student.where('cohort_id = ?', self[:id])
  end
end
student.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
class Student < Database::Model

  def cohort
    Cohort.where('id = ?', self[:cohort_id]).first
  end

  def cohort=(cohort)
    self[:cohort_id] = cohort[:id]
    self.save
    cohort
  end

end

We can create some Rails-like methods by adding self.belongs_to() and self.has_many() methods in our Model class:

database_model.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module Database
  class Model
    #...

    def self.belongs_to(arg)
      define_method(arg) do
        Object.const_get(arg.capitalize).where('id = ?', self[:"#{arg}_id"]).first
      end

      define_method("#{arg}=") do |other|
        @attributes[:"#{arg}_id"] = other[:id]
      end
    end

    def self.has_many(arg)
      define_method(arg) do
        arg = arg[0..-2]
        Object.const_get("#{arg.capitalize}").where('cohort_id = ?', self.id).first
      end
    end
  # ...
end

Ruby’s Object.const_get() method takes a string or a symbol and converts it to a constant. Executing belongs_to :cohort will capitalize :cohort to :Cohort and Object.const_get() will look for a constant called Cohort, which must be initialized, or Ruby will complain (you can’t just pass anything you want into it). Now we’ve got Cohort.where('id = ?', self[:"#{cohort}_id"]).first returning the cohort that the student belongs to.

The second method being defined in self.belongs_to() gives one the freedom to set the cohort to another without having to do it by ID; one can simply call student.cohort = Cohort.find(1), and that will set the students cohort_id to the new cohort.

We can use the same strategy with has_many(). To keep it reading nice and smooth, I want to be able to say a cohort has_many :students. This requires making :students singular, which I accomplished somewhat dirtily. Again, ActiveSupport has better methods to switch between singular and plural of words.

Active Record Jr. provided a great way to learn about some very cool aspects of the Ruby language, metaprogramming, and how Active Record accomplishes all of its magic. Of course, Active Record has its detractors, and all the magic of metaprogramming carries along with it some performance costs (think about all the work that has to happen – methods getting defined and returning values, etc), but it provides a great DSL that allows web developers to create apps without having to know SQL in and out.

Comments