- Read Tutorial
- Watch Guide Video
So far, we have seen:
- A basic overview of metaprogramming
- How we can use it in a custom class
- How to use it on existing Ruby classes, such as the String class.
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
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.
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.