How Rails Uses Metaprogramming for the find_by Method
So far, we have seen a basic overview of metaprogramming, how we can use it in a custom class and how to use it on existing Ruby classes, such as the String class. In this lesson, we are going to see how it can be used in more advanced ways, typically like how other Ruby programmers use it.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

So far, we have seen:

In this guide we are going to see how metaprogramming can be used in real world applications.

I'm going to open up a Ruby on Rails application because Rails leverages metaprogramming as well as any program I've ever seen. We're going to walk through how the Rails framework generates database lookup methods on the fly.

Here is the database schema file for the Rails application we're going to look at:

  create_table "contacts", force: true do |t|
    t.string   "name"
    t.string   "email"
    t.text     "message"
    t.string   "category"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "customers", force: true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string   "email"
    t.text     "logo"
  end

  create_table "location_posts", force: true do |t|
    t.integer  "post_id"
    t.integer  "location_id"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

We're going to use the customers table as our case study for this guide.

In the Rails console (which gives us the ability to perform database queries), I can bring up the record of the last customer with the command: Customer.last

large

Now, what happens if we only know the name of the customer and no other details, and we want to pull up a record based on the name.

To do that, the command is:

Customer.find_by_name("Warren Cat")

If you run this code you'll see that it returns the same record as before.

The cool thing about this method is that Rails only has a method called find_by and it uses metaprogramming to allow users to dynamically enter the database's field. In this sense name is simply an argument for the find_by method, though it may look like a complete method by itself.

If you go to the Rails source code, you can see this method:

    # Finds the first record matching the specified conditions. There
    # is no implied ordering so if order matters, you should specify it
    # yourself.
    #
    # If no record is found, returns <tt>nil</tt>.
    #
    #   Post.find_by name: 'Spartacus', rating: 4
    #   Post.find_by "published_at < ?", 2.weeks.ago
    def find_by(arg, *args)
      where(arg, *args).take
    rescue RangeError
      nil
    end

If you see, this method references the first argument, and it takes on any number of arguments.

Now, to prove that the find_by method takes other parameters too, let us pull up with a record based on the email:

Customer.find_by_email("yadeco2@yahoo.com")

The output brings up the exact same record. Let's take this a bit further, let's look at the second table in our above list of tables, which is locations. There is no name attribute in this table at all. So, if you use name here, it will throw an error. The code would be:

Location.find_by_name("Midland")

The application throws an undefined method error.

large

Rails is intelligent enough to know that it can only utilize existing attributes for its find_by method, and not just any random attribute.

This goes to show that find_by_name is not a method by itself, rather Rails takes the first argument and creates a method dynamically using metaprogramming. You may also have noticed that I circles the method_missing message in the terminal. method_missing is going to be the topic of what we're going to explore in future guides. As I've mentioned before method_missing is the core component that allows for Ruby's most powerful metaprogramming features.