Команда Python for Devs подготовила перевод статьи о том, почему автору SQLAlchemy нравится… но не настолько, чтобы не попробовать создать собственный ORM. SQLORM ― минималистичный, прямолинейный и честный: никакой магии, никаких скрытых Unit of Work, максимум контроля над SQL и минимум связности с сессией.
На моей основной работе я использую SQLAlchemy как ORM. Это потрясающий проект — мощный и гибкий. Но при этом мне всегда казалось, что некоторые архитектурные решения не совсем подходят под то, как я люблю работать с ORM. В частности:
Мне не особенно нравится паттерн Unit of Work и то, что вы не управляете моментом выполнения DML-запросов. Я предпочитаю, чтобы запросы выполнялись сразу, как только вызываются в коде.
Я не хочу, чтобы мои объекты были «привязаны» к сессии или конкретной базе данных. Я хочу иметь возможность выбрать данные из одной базы и вставить в другую, используя тот же объект.
В большинстве случаев мне нужны простые объекты, которые отображают строку таблицы.
Я хочу писать SQL вручную для сложных запросов. Мне не нужен query builder или DSL — я предпочитаю писать обычный SQL.
Меня не интересует абстракция поверх разных СУБД. Обычно я выбираю сервер базы данных в начале проекта и оптимизирую работу под него.
Оставаться как можно ближе к уровню DB-API.
С этими идеями в голове и родился SQLORM. (Да, название так себе — я ужасно придумываю имена для подобных проектов.) Он вдохновлён множеством существующих ORM, но при этом привносит несколько собственных особенностей.
(К слову, я знаю, что в экосистеме Python есть много других ORM, но для меня SQLAlchemy остаётся лучшей. Мне не нравится API или кодовая база остальных.)
Главная особенность SQLORM в том, что SQL находится в центре всего. Вы можете создавать SQL-запросы как обычные функции Python, используя докстринг, чтобы писать в нём шаблон SQL-запроса:
from sqlorm import sqlfunc
@sqlfunc
def tasks_completion_report(start_date, end_date):
"""SELECT done_at, COUNT(*) count
FROM tasks
WHERE done_at >= %(start_date)s AND done_at <= %(end_date)s
GROUP BY done_at"""В этом примере start_date и end_date — это параметры, и они будут корректно экранированы. Выполнение функции запускает SQL-запрос в рамках активной транзакции.
Подключения и транзакции используются через контекстные менеджеры. Класс Engine управляет подключениями DB-API.
from sqlorm import Engine
import datetime
engine = Engine.from_uri("sqlite://app.db")
with engine:
report = tasks_completion_report(datetime.date(2025, 1, 1), datetime.date.today())SQLORM предоставляет множество утилит, которые помогают строить SQL-выражения, а также выбирать связанные строки одним запросом.
По умолчанию строки возвращаются как словари, но при желании можно «наполнять» объекты:
class Task:
pass
@sqlfunc(model=Task)
def find_tasks():
"SELECT * FROM tasks"
with engine:
tasks = find_tasks()Теперь нам не хочется вручную писать бесконечные простые выражения, чтобы заново собрать базовые возможности CRUD, поэтому в SQLORM есть класс Model. Он следует паттерну Active Record.
from sqlorm import Model
class Task(Model):
pass
with engine:
tasks = Task.find_all()
task = Task.create(title="my task")
task = Task.find_one(id=1)
task.done = True
task.save()Разумеется, у классов моделей тоже могут быть SQL-методы!
class Task(Model):
@classmethod
def find_todos(cls):
"SELECT * FROM tasks WHERE not done"
def toggle(self):
"UPDATE tasks SET done = not done WHERE id = %(self.id)s"
with engine:
tasks = Task.find_todos()
task = next(tasks)
task.toggle()Как вы уже заметили, классам моделей не нужно заранее знать список колонок. Однако полезно всё же определить их — для автодополнения, проверки типов и генерации DDL-выражений. SQLORM позволяет сделать это с помощью аннотаций Python:
from sqlorm import PrimaryKey
class Task(Model):
id: PrimaryKey[int]
title: str
done: boolКлассы моделей предоставляют ещё множество утилит для работы со связями, ленивой загрузкой, типами колонок и многим другим.
Как уже упоминалось, классы моделей не привязаны к конкретному engine — они работают на том engine, который предоставляет текущий контекст. Это упрощает реализацию таких подходов, как чтение из реплики и запись в основной сервер.
Реализовать чтение из реплики и запись в primary можно в несколько строк:
main = Engine.from_uri("postgresql://main")
replica = Engine.from_uri("postgresql://replica")
with replica:
task = Task.get(1)
if not task.done:
with main:
task.toggle()У SQLORM есть ещё много мощных возможностей. Он хорошо документирован и предоставляет интеграцию с Flask. Попробуйте!
Русскоязычное сообщество про Python

Друзья! Эту статью подготовила команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!