nORM — ORM, но есть одно «no»

от автора

Если вы работаете с базами данных и используете 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)

Три шага

  1. Напишите SQL (схема + запросы репозитория).

  2. Запустите norm generate.

  3. Импортируйте сгенерированный пакет и вызывайте методы репозитория из вашего приложения.

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/