A Popular Example of Metaprogramming: ORMs
3 min read

A Popular Example of Metaprogramming: ORMs

I’d like to explain metaprogramming by building an ORM from scratch

Metaprogramming

The best definition I found was:

Metaprogramming is code that writes code.

Sometimes, it’s not clear what metaprogramming or just programming is. In this hacker news discussion, there are many people that might not agree with my example on metaprogramming.

Metaprogramming an Example

I’d like to explain metaprogramming by building an ORM (Object Relational Mapping) from scratch. Examples of ORMs are ActiveRecord in Django or Ruby On Rails and Sequelize in NodeJS.

If you are unfamiliar with ORMs, I recommend reading this Stack Overflow answer. But in a nutshell, an ORM is a class or object used to interact with a database. The idea is that developers can use the ORM library for any database schema. The classes and objects created by the ORM code adapt to the data base structure.

For example, if we have a table “users” with a column “email,” the ORM provides a User class and a class method User.getByEmail. Moreover, the properties of a User instance match the columns in the “users” table.

An ORM is a library that creates code that adapts to our database schema.

Class Factory

Let’s start building our ORM. First, let’s define a function that creates classes. We’ll call it define similarly to Sequelize.

The idea is to use it like this:

const User = define(‘users’);

User is a class created by define:

const define = (tableName) => {
  // We won’t use the new “class” keyword for this example
  const Model = function () {};
  return Model;
};

There is no metaprogramming so far. We are not creating code with code yet.

Custom Class Methods

We’d like to add the methods getByXXX, where XXX is each column in the table. Therefore, we add the columns as arguments to our define function.

Assuming our “users” table has the columns “Email,” “Name,” and “Id”:

const User = define(‘users’, [‘Email’, ‘Name’, ‘Id’]);

That means our User should have: User.getByEmail, User.getByName and User.getById. If we generalize this, we get User.getBy{column name}:

// We added the “columns” parameter
const define = (tableName, columns) => {
  const Model = function () {};
  // class methods are properties attached to the Model function
  columns.forEach((column) => {
    const methodName = `getBy${column}`;
    // Adding the class method. A property of the Model function.
    Model[methodName] = function () {
      console.log(‘in da method:’, methodName);
    };
  });
  return Model;
};

Now we use it:

const User = define(‘users’, [‘Email’, ‘Name’, ‘Id’]);
User.getById();

Generalizing SQL Commands

Let’s take a look at the SQL statements for different class methods: User.getByEmail(‘email@test.com’), or Post.getById(10).

Note: I ignore indexes and fancy SQL features.

  • User.getByEmail: SELECT * from “users” where email = “email@test.com”;.
  • Post.getById: SELECT * from “posts” where id = 10;.

We generalize this to: SELECT * from “<table_name>” where “<property>” = “<argument>”;.

Class Methods Functionality

Let’s apply the generalized SQL statements to each class method:

SELECT * from “<table_name>” where “<property>” = “<argument>”;

We want to perform this SQL statement when the class method is called. For example, calling User.getByEmail('email@test.com') makes the following SQL query:

SELECT * from “users” where “Email” = “email@test.com”;

Let’s implement the method:

//…
columns.forEach((column) => {
  const methodName = `getBy${column}`;
  // Add parameter
  Model[methodName] = async function (param) {
    // Prepare SQL statement
    const statement = `SELECT * from “${table}” where “${column}” = “${param}”;`;
    // assuming we created a DB client
    const result = await client.query(statement);
    return result;
  };
});
//...

The key here is the statement variable. Earlier, we generalized the SQL statement, which is what we develop here:

const statement = `SELECT * from “${table}” where “${column}” = “${param}”;`;

Each class method Model.getByXXX makes a different query, adapted to the specific table and column in the database.

Metaprogram

Now let’s take a look at all the code together:

const define = (tableName, columns, dbParams) => {
  const Model = function () {};
  const client = new DBClient(dbParams);
  columns.forEach((column) => {
    const methodName = `getBy${column}`;
    Model[methodName] = async function (param) {
      const statement = `SELECT * from “${table}” where “${column}” = “${param}”;`;
      const result = await client.query(statement);
      return result;
    };
  });
  return Model;
};

This code could be the beginning of our custom ORM—one that creates a new class for each database table.

Where Is The Metaprogramming?

The metaprogramming happens inside the columns loop. In each iteration, we create a different class method. That function is the one that “metaprograms”:

(column) => {
  const methodName = `getBy${column}`;
  // METAPROGRAMMING
  Model[methodName] = async function (param) {
    const statement = `SELECT * from “${table}” where “${column}” = “${param}”;`;
    const result = await client.query(statement);
    return result;
  };
};

Imagine we wrote this as a library, then the library code itself has no way of knowing which class methods the class Model has. So, only when the code is used and executed the class methods are defined.

When the code runs, it writes more code. Hence, code that writes code. Hence, metaprogramming.

Find a gist with the code and interesting new functionality. Can you guess which it is?


If you like this post, consider sharing it with your friends on twitter or forwarding this email to them 🙈

Don't hesitate to reach out to me if you have any questions or see an error. I highly appreciate it.

And thanks to Michal for reviewing this article 🙏

Thanks for reading, don't be a stranger 👋

GIMTEC is the newsletter I wish I had earlier in my software engineering career.

Every other Wednesday, I share an article on a topic that you won't learn at work.

Join more than 3,000 subscribers below.

Thanks for subscribing! A confirmation email has been sent.

Check the SPAM folder if you don't receive it shortly.

Sorry, there was an error 🤫.

Try again and contact me at llorenc[at]gimtec.io if it doesn't work. Thanks!