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 🙏