How to dynamically define an SQLModel class

Question:

Context

SQLModel is heavily based on Pydantic. The latter has the create_model function allowing you to create/define a model class at runtime, by passing the field definitions as arbitrary keyword arguments.

There seems to be no special version of the create_model function built into SQLModel and there is no mention of dynamic model creation in the documentation.

Question

Is it possible to leverage the Pydantic create_model function to define a fully-functioning SQLModel class?

Requirements

Functioning in the sense that it will properly serve as an ORM class, play nice with a database engine, and its instances can be added to and refreshed by database sessions, just like any statically defined SQLModel subclass.

As a proof of concept, it should be possible to dynamically construct a working equivalent of the following statically defined model and perform the aforementioned operations with it:

from typing import Optional

from sqlmodel import Field, SQLModel

class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: Optional[int] = None
Asked By: Daniil Fajnberg

||

Answers:

The answer is yes!

The create_model function has the optional __base__ parameter (as mentioned in the docs), which accepts any subclass (or sequence of subclasses) of the Pydantic BaseModel. The SQLModel base class happens to directly inherit from BaseModel and can thus be passed here.

However, this is not sufficient to have a model that maps to a table. The SQLModelMetaclass requires table=True to be passed as a keyword argument during subclassing of SQLModel. Luckily, there is a solution for this built into Pydantic as well.

While this is mentioned nowhere on Pydantic’s documentation website, the create_model function (source here) has a __cls_kwargs__ parameter for being able to pass arbitrary keyword arguments to the metaclass during class creation.

These two components, together with the actual field definitions, are actually all we need to dynamically create our ORM class. Here is a full working example:

from typing import Optional

from pydantic import create_model
from sqlmodel import Field, Session, SQLModel, create_engine

field_definitions = {
    "id": (Optional[int], Field(default=None, primary_key=True)),
    "name": (str, ...),
    "secret_name": (str, ...),
    "age": (Optional[int], None),
}

Hero = create_model(
    "Hero",
    __base__=SQLModel,
    __cls_kwargs__={"table": True},
    **field_definitions,
)

if __name__ == '__main__':
    sqlite_url = "sqlite:///test.db"
    engine = create_engine(sqlite_url, echo=True)
    SQLModel.metadata.create_all(engine)
    session = Session(engine)
    hero = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
    session.add(hero)
    session.commit()
    session.refresh(hero)
    print(hero)

That print statement gives the following output:

secret_name='Pedro Parqueador' id=1 age=None name='Spider-Boy'

That demonstrates that the id was created by the database upon insertion.

The SQL statements printed to stdout by the engine show that everything went as planned:

CREATE TABLE hero (
    id INTEGER NOT NULL, 
    name VARCHAR NOT NULL, 
    secret_name VARCHAR NOT NULL, 
    age INTEGER, 
    PRIMARY KEY (id)
)

...

INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
('Spider-Boy', 'Pedro Parqueador', None)

...

SELECT hero.id, hero.name, hero.secret_name, hero.age 
FROM hero 
WHERE hero.id = ?
(1,)

So far, I have not encountered any caveats to this approach, beyond those that apply to dynamic model creation in Pydantic as well, such as the obvious lack of static type checking support or auto-suggestions, if a model was defined dynamically.


Tested with pydantic>=1.10.4,<2.* and sqlmodel==0.0.8.

Answered By: Daniil Fajnberg
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.