retour

Estoult: Not An ORM

thumbnail
6 min read from development on 2020-09-09

Concluding this three part series(1) on why I hate object-oriented programming, today we're going to look at how you can do less of it. Ideally, you would just not use an object-oriented language, although I must admit sometimes that isn't possible. Either way you should be limiting the amount of objects you make.

The first step to this is to not use an ORM. I mean, it's called an "Object Relational Mapper", obviously you'd want to avoid anything with "object" in the name.

(1) Yes, this was supposed to be a series but I forgot to talk more about OOP in the last post and just wanted to talk about Rust.

But now I hear you ask.

How am I supposed to structure my queries to the database now? I don't want to write SQL in strings! Yuck!

Well have I got the answer (if you use Python) for you! For the past month, I've been building Estoult, a Python toolkit for data mapping SQL databases with an integrated query builder. Originally started as a port of Elixir's Ecto, I decided that was too much effort (and probably wouldn't work anyway) and just did my own thing. Now you can define schemas and queries to help structure your code without the baggage of l'objet horreur.

Here are some of the awesome features of Estoult from the readme:

  • Not an ORM […]

WOAH! That's all I needed to hear! That alone makes it a 5 star man library!

To show you the amazing abilities of not using an ORM, let's do what every other ORM does for a tutorial and build a blog.

Except I can't be bothered to build a blog right now (I already spent enough time on this one), so we're just going to build the stuff you need Estoult for in a blog.

I'm going to assume you have PostgreSQL setup, so we are going to use that for our database.

You can install Estoult from pip:

pip install estoult

First, start of by adding a Database and importing all the stuff we'll need.

import re
from estoult import *
from datetime import datetime

db = PostgreSQLDatabase(
    database="blog",
    user="postgres",
    password="postgres"
)

I honestly have no idea why people choose to write a blog to show off their ORMs, it's not representative of what you'd use them for at all. Blogs are really simple and aren't a good introduction to the features of any ORM or for showing why you'd want to use them.

Anyway, we only need to make one Schema, the Post which will have the following columns:

  • slug: a URL-friendly representation of the title (and also our primary key).
  • title: the title…
  • content: content of the post (could be Markdown or HTML, I don't care)
  • timestamp: the time the entry was created.
class Post(db.Schema):
    __tablename__ = "posts"

    slug = Field(str, "slug", primary_key=True)
    title = Field(str, "title")
    content = Field(str, "content")
    timestamp = Field(datetime, "timestamp")

    @classmethod
    def insert(cls, row):
        row["slug"] = re.sub('[^\w]+', '-', row["title"].lower())
        row["timestamp"] = datetime.now()
        return super().insert(row)

We are also overwriting the insert function of the schema to automatically generate a timestamp and create a slug from the title so we won't need to do it manually. This way we only need to worry about entering the title and content. Overwriting classmethods is encouraged in Estoult, so use it where you can.

Let's make methods for adding and getting posts.

def add_post(post):
    Post.insert(post)

def get_posts():
    return Query(Post).select().execute()

def get_post(slug):
    return Query(Post).get().where(Post.slug == slug).execute()

Oh that's it… That's kinda all you need for a blog. Alright then, guess we're done.

No but actually, I seriously hate writing tutorials.(2) Instead I want to talk about the stuff I learnt while writing Estoult. I've actually had to deal with some of the Python internals and they're actually really interesting. I've never been super happy with Python before but after this it's been starting to somewhat impress (or maybe I just like it more because I understand it better).

(2) Even though those were the first posts of this blog, maybe because of it.

In some object-oriented languages a metaclass is a class whose instances are classes. Just as an ordinary class defines the behaviour of certain objects, a metaclass defines the behavior of certain classes and their instances.[1]

Python is one of the languages to feature metaclasses, and as such is capable of a decent amount of metaprogramming (well, as much as an object-oriented language can be metaprogrammed).

One of the things this has allowed me to do is to imperatively write classmethods with loops. A good example of this is adding join functions to the Query class. This is what it looks like currently.

class QueryMetaclass(type):

    sql_joins = [
        "inner join",
        "left join",
        "left outer join",
        "right join",
        "right outer join",
        "full join",
        "full outer join",
    ]

    @staticmethod
    def make_join_fn(join_type):
        def join_fn(self, schema, on):
            q = f"{str(on[0])} = {str(on[1])}"
            self._query += f"{join_type} {schema.__tablename__} on {q}\n"

            return self

        return join_fn

    def __new__(cls, clsname, bases, attrs):
        for join_type in cls.sql_joins:
            attrs[join_type.replace(" ", "_")] = QueryMetaclass.make_join_fn(join_type)

        return super(QueryMetaclass, cls).__new__(cls, clsname, bases, attrs)

class Query(metaclass=QueryMetaclass):
    [...]

Since all the joins are the same structure wise, writing them as individual classmethods would become tedious and result in a lot of repeated code. Almost every class in Estoult is created by a metaclass to do things like this. Schema uses a metaclass to add metadata about itself to each of it's fields, and Field uses a metaclass to overload Python operators with our query operators.

I never realised Python had these kinds of metaprogramming abilities before, but if everything is an object then I guess you'd expect some crazy stuff.

Don't get me wrong, OOP still stucks. Metaprogramming is much, much easier and better in Elixir or even Rust, but I think it's neat that Python isn't as rigid as I thought it was.

So now that you're convinced that you should immediately remove the ORM from your 900 thousand lines of code enterprise Python web application and replace it with Estoult, you might have one more question.

What the hell is an "Estoult"?

The answer lies in here.

Anyway, this has been a fun month. I think it's been the longest time I've continuously talked about programming in a long time.


[1]: https://en.wikipedia.org/wiki/Metaclass