Если вы работаете с базами данных и используете ORM, вы, вероятно, сталкивались с той же проблемой, что и я. ORM отлично подходят для отображения таблиц на объекты. Но они начинают мешать, когда запрос становится сложным: агрегации, тщательно продуманные JOIN’ы, формы отчетов, которые не соответствуют одной модели на таблицу. Вы боретесь с ORM, переходите на сырой SQL, а затем вручную пишете связующий код (маппинг).
Не каждый SELECT возвращает то, что подходит под одну ORM-модель. SQL - это лучший язык для доступа к данным. Лучшие ORM, которые я использовал, такие как Drizzle, побеждают, потому что они остаются близки к SQL. Я хотел пойти дальше: хранить SQL в системе контроля версий и генерировать из него типизированный Python.
Именно поэтому я создал nORM (no ORM - не ORM) и выпустил версию v0.1.0 на этой неделе (мой первый опенсорс проект).
nORM - это альтернатива использованию ORM для всего. Пока генератор работает только с Python. Позже я планирую миграции и больше языковых бэкендов; после этого он мог бы полностью заменить ORM, если вы этого захотите. На сегодняшний день это рабочий процесс в стиле sqlc плюс динамические возможности для запросов, которые в противном случае вы бы писали на Python.
Этот рабочий процесс вдохновлен sqlc. Если вы уже используете sqlc, и его вам достаточно - продолжайте.
SQL на входе, Python на выходе
Вы пишете схему и запросы на SQL. norm generate создает модели и классы репозиториев. Откройте сгенерированный метод, и SQL будет прямо там. Никакого скрытого слоя запросов.
Схема (norm_in/schema.sql):
CREATE TABLE users ( id SERIAL PRIMARY KEY, name text NOT NULL, blocked bool DEFAULT false );
Запросы (norm_in/repositories/users_repo.sql):
-- repo_name: UsersRepo -- name: get_user :one SELECT * FROM users WHERE id = :id; -- name: list_users :many SELECT * FROM users ORDER BY name;
Сгенерированный код (сокращенно из гайда по Python):
class UsersRepo: async def get_user(self, id: int) -> User | None: query = """ SELECT users.id AS id, users.name AS name, users.blocked AS blocked FROM users WHERE users.id = %(id)s """ params = {"id": id} async with self.db.cursor() as cur: await cur.execute(query, params) result = await cur.fetchone() ... return User(id=result[0], name=result[1], blocked=result[2])
Ваше приложение:
from norm_out.users_repo import UsersRepo async with get_db() as db: repo = UsersRepo(db) user = await repo.get_user(id=42)
Три шага
Напишите SQL (схема + запросы репозитория).
Запустите
norm generate.Импортируйте сгенерированный пакет и вызывайте методы репозитория из вашего приложения.
norm init создает шаблоны norm.yaml и папки. norm check полезен в CI: если запрос ссылается на отсутствующую колонку или тип параметра не совпадает, генерация завершится ошибкой.
Чем nORM превосходит простую кодогенерацию
sqlc останавливается там, где приложению все еще нужна runtime-композиция: опциональные фильтры для конечных точек списков, выбранные пользователем колонки для сортировки, частичные обновления, nullable объединения. nORM добавляет макросы, чтобы логика оставалась в SQL, а генератор ее разворачивал.
Динамическая фильтрация. Добавьте префикс _ к параметру, чтобы сделать предикат опциональным. Один запрос может покрыть множество комбинаций фильтров вместо построения строк в Python.
-- name: search_authors :many SELECT * FROM authors WHERE name = :_name AND rating > :_min_rating;
Руководство по динамической фильтрации описывает сгенерированный API и то, как nORM обрезает дерево WHERE во время выполнения.
Динамическая сортировка. Используйте n.ord() в ORDER BY, когда клиент выбирает колонку и направление сортировки.
-- name: list_authors_sorted :many SELECT * FROM authors a ORDER BY n.ord(a, :order_by, :desc), a.id ASC;
Сгенерированные методы принимают Literal[...] для разрешенных колонок и проверяют их перед выполнением запроса. Подробности: руководство по динамической сортировке.
Больше макросов (частичные обновления, встраивание JOIN и другие) доступны в обзоре и руководствах.
Как это работает
Разбор и работа с диалектами SQL выполняются через sqlglot. nORM читает DDL, SQL репозиториев и макросы, затем анализирует типы и структуру SQL перед кодогенерацией. Postgres, SQLite, MySQL, ClickHouse и DuckDB используют один и тот же путь. CLI, генераторы, макросы и конфигурация norm.yaml — это сам nORM.
Проект имеет широкое тестовое покрытие. v0.1.0 - это первый публичный релиз, но основная часть генерации закалялась в течение нескольких месяцев.
Что входит в версию 0.1.0
Включено: Генератор Python (асинхронный или синхронный, Pydantic или dataclasses через norm.yaml). CLI: norm init, norm generate, norm check, norm schema pull для интроспекции Postgres.
Пока нет: Генераторы для Rust, Go и TypeScript. Нет команды для миграций в версии 0.1.0; я планирую добавить миграции позже. До этого используйте свой обычный инструмент для изменений схемы.
Попробуйте
pipx install norm-cli norm init norm generate
Репозиторий: https://github.com/devfros/nORM
wango_pama
Прежде чем браться за миграции: а как оно будет работать с аггрегацией данных из трёх таблиц? Какая-нибудь нормализация, например, если применена к данным
AfrosRajabov Автор
Я думаю сделать schema.sql источником правды с автогенерацией миграций там, где это возможно, плюс возможность дописывать миграции там, где изменения сложнее, чем обычные добавления/удаления колонок. Так работает Alembic, например. У меня уже есть наработки и успехи с PostgreSQL, но пока далеко от идеала.
wango_pama
Ок, ещё раз: а как nORM будет работать с аггрегацией данных из трёх таблиц?
AfrosRajabov Автор
Уже сейчас отлично работает с запросами любой сложности, и не важно, сколько таблиц в них участвует. Успешно справляется с определением того, какой набор колонок в итоге возвращается, и генерирует соответствующий код. Если запрос возвращает не строго какую-то одну таблицу, то генерируется новая модель, и результат маппится на неё.
AfrosRajabov Автор
Вся задача этого инструмента - это определение того, что принимает запрос, что возвращает и генерирование типизированного кода на основе этого. Плюс дополнительные макросы, чтобы по ORM не скучать.