SQLAlchemy, metaclasses and declarative_base: configure DB model’s classes

Intro

Disclaimer: This article not about ‘better’ way how to define, but about how you can customize SQLAlchemy classes definition. And any classes…

On one beautiful day, I played with a pet-project and created some kind of PoC for my idea. I started to define declarative base classes for SQLAlchemy (https://docs.sqlalchemy.org/) and thought that I don’t like them if I need some quick schema defining. I’m too lazy wrote a lot of copy/paste and oblivious things.

I mean, this is how usual looks the Class for SQLAlchemy:

I thought, that will be cool to have something like this:

Type annotation ‘cut’ class

Of course, from the box we will get an error about primary keys and table name (as you see I did not define it).

So, I want to have this:

  • If no primary keys — use ‘id’ field as key by default and generated it, for example, with uuid4() (very not optimal, but simple)
  • If no table name — please, just use class name and add ‘s’ at the end or ‘es’, or ‘ies’..

Declarative_base

Great news!

Every time when you use SQLAlchemy and define a model by classes you use a declarative_base method

from sqlalchemy.ext.declarative import declarative_base

That returns ‘Base’ class for classes that SQLAlchemy will use as a DB model.

Do you remember

class User(Base)

So this is ‘Base’ class to inherit all DB models classes.

To get it you use

Base = declarative_base()

And declarative_base has a parameter — metaclass=.

And this param take 1-to-1 as it named, so it’s take your custom metaclass and uses it as a metaclass for you SQLAlchemy ‘Base’ class.

Metaclasses

If you are reading this article and see for the first time ‘metaclass’ word (if no — just skip) :

In python there exists a mechanism/special object type that is called ‘metaclass’. It is used everyday in your code, but you don’t think about it. Instances of classes — objects. Instances of metaclasses — classes.

When you define in Python 3.7 your class like

class A:
pass

It means the same like

class A(object):
pass

And object has a metaclasstype (it’s always little bit surprise, when before you use it only as function to get object type)

If you want to define your own Metaclass you need to inherit your class from type. Very simple. To define metaclasses you use the same keyword ‘class’.

And to set custom metaclass to your class you use metaclass= keyword in class definition, for example:

This way we define a new metaclass, that before call method __new__ for our class will print messages in log.

More useful example — one of the ways to implement Singleton pattern in Python:

class SingletonMeta(type):    _instances = {}    def __call__(cls, *args, **kwargs):        if cls not in cls._instances:            cls._instances[cls] = super(SingletonMeta,cls).__call__(*args, **kwargs)       return cls._instances[cls]

If you want good articles about metaclasses, I can recommend this, it’s pretty full and informative: https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/ or https://www.datacamp.com/community/tutorials/python-metaclasses. And PEP https://www.python.org/dev/peps/pep-3115/.

Okay.

SQLAlchemy metaclass: DeclarativeMeta

First of all, we need to inherit it from DeclarativeMeta class to get all features that we want from SQLAlchemy (because we want just add something, not broke or remove)

from sqlalchemy.ext.declarative import DeclarativeMeta

You need to define all changes that you want to implement in __init__ because of the logic how it is used inside SQLAlchemy. If you interested — you can check the logic of declarative_base method in source code.

That we see here? In ‘cls’ argument we got class, that used our metaclass, as base. So this is your DB Model’s classes.

And we just simple autoset argument __tablename__ to it. If you can see, I did not add check ‘if not getattr(…’ to check if such attr already exist or not, so add if you need.

This solved only a wish to have auto generated table names in SQLAlchemy.

Now, get features for quickly defining fields with annotations.

I special separate it in another metaclass (maybe some time in future I will want to use it):

all code samples at the end of article in repo

What’s going inside? I just iterate on class annotations and setattr for class with same field name.

So, this is:

name: str

just class annotation, but SQLAlchemy wait for class attribute with link to the object of type Column inside,

so in method ‘get_type’ I convert this ‘name: str’ to this -> Column(SQLAlchemy_type, args ...)

detailed code you can check here (how quick to test and play with code samples -> at the end of the article): https://github.com/xnuinside/sqlalchan/blob/master/sqlalchan/base_meta.py

Final

To use our metaclasses just let define out ‘Base’ class with argument, so it will be

Base = declarative_base(metaclass=BaseMetaDefaultPrimary)

Now, let’s get some test classed, that we will inherit from Base:

class Location(Model):
long: float
lat: float


class Country(Model):
name: str
code: str


class City(Model):
name: str
location: Location
country: Country

... and etc.

And let’s create insert value operation, to test that all works well:

session = Session(bind=engine)
session.add(
Country(
name='China',
code='CH'
)
)
session.commit()

Now, let’s time to check result:

all our tables exists

Now check, that insert works fine:

Great! Thats it. I hope it was useful.

All code samples available here: https://github.com/xnuinside/sqlalchan

For easy-run-and-play you can find ‘examples’ folder in project, that contains pre-set. To run it in main source folder exists docker-compose file for quick up PostgreSQL DB and run SQLAlchemy create_engine sample with customized classes. Enjoy!

Take care of your and don’t forget get new knowledge, experiment and optimize your work!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store