Если вы работаете с базами данных и используете 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 :oneSELECT * FROM users WHERE id = :id;-- name: list_users :manySELECT * 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 UsersRepoasync 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 :manySELECT * FROM authorsWHERE name = :_name AND rating > :_min_rating;
Руководство по динамической фильтрации описывает сгенерированный API и то, как nORM обрезает дерево WHERE во время выполнения.
Динамическая сортировка. Используйте n.ord() в ORDER BY, когда клиент выбирает колонку и направление сортировки.
-- name: list_authors_sorted :manySELECT * FROM authors aORDER 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-clinorm initnorm generate
Репозиторий: https://github.com/devfros/nORM
ссылка на оригинал статьи https://habr.com/ru/articles/1044020/