惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
aimingoo的专栏
aimingoo的专栏
Microsoft Security Blog
Microsoft Security Blog
NISL@THU
NISL@THU
T
Threatpost
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs
S
Securelist
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
人人都是产品经理
人人都是产品经理
B
Blog RSS Feed
S
Secure Thoughts
MyScale Blog
MyScale Blog
O
OpenAI News
P
Palo Alto Networks Blog
美团技术团队
C
Cyber Attacks, Cyber Crime and Cyber Security
TaoSecurity Blog
TaoSecurity Blog
量子位
L
Lohrmann on Cybersecurity
G
GRAHAM CLULEY
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
Know Your Adversary
Know Your Adversary
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Simon Willison's Weblog
Simon Willison's Weblog
宝玉的分享
宝玉的分享
PCI Perspectives
PCI Perspectives
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tenable Blog
I
InfoQ
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Microsoft Azure Blog
Microsoft Azure Blog
Recent Announcements
Recent Announcements
S
Security @ Cisco Blogs
S
Schneier on Security
B
Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Cloudflare Blog
AWS News Blog
AWS News Blog
IT之家
IT之家
V
Vulnerabilities – Threatpost
The Hacker News
The Hacker News
H
Heimdal Security Blog
I
Intezer
A
Arctic Wolf
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Help Net Security
W
WeLiveSecurity

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Django-style фильтры поверх SQLAlchemy: зачем я написал python пакет sqlalchemy-query-manager
ViAchKoN · 2026-06-27 · via Все публикации подряд на Хабре

Простой

7 мин

0

SQLAlchemy — очень удобный инструмент. В нём явно видно, какой select() строится, где используются join, какие условия попадают в where, как загружаются связи и какой SQL в итоге уходит в базу.

Но в обычном backend-коде далеко не каждый запрос является сложным.

Во многих Flask/FastAPI-сервисах есть большое количество однотипных запросов: отфильтровать записи, пройти по связи, добавить OR, отсортировать результат, ограничить количество строк, заранее загрузить связанные объекты и вернуть список результатов.

Такой код часто выглядит примерно так:

stmt = (
    select(Item)
    .join(Item.group)
    .where(
        or_(
            Item.is_valid == True,
            Item.number > 100,
        )
    )
    .where(Group.is_active == True)
    .options(joinedload(Item.group))
    .order_by(Item.number.desc())
    .limit(20)
)

items = session.execute(stmt).scalars().all()

Проблема: бизнес-смысл запроса заметно короче, чем код вокруг него.

Фактически мы хотим сказать следующее:

Дай мне валидные Item или Item с number > 100,
только из активных групп,
сразу загрузи group,
отсортируй по number по убыванию
и верни 20 строк.

После нескольких десятков похожих запросов в приложении начинает хотеться более компактного слоя, который оставляет SQLAlchemy в основе, но убирает часть повторяющейся механики.

Обычно такой слой всё равно появляется в проекте, даже если его никто специально не проектировал. Сначала это несколько helper-функций для фильтров, потом общий код для сортировки и пагинации и так далее. В какой-то момент становится понятно, что уже написан собственный мини-query layer.

В результате похожей истории я решил вынести этот слой в отдельный пакет. Так и появился sqlalchemy-query-manager.

Идея очень простая: добавить поверх SQLAlchemy небольшой query layer с Django-style фильтрами, Q-объектами, relationship lookups, eager loading и preview итогового SQL.

Это вспомогательный слой для типовых backend-запросов, где прямой SQLAlchemy-код начинает выглядеть слишком громоздким.

Какой API хотелось получить

Тот же запрос можно записать так:

items = (
    Item.query_manager
    .where(
        Q(is_valid=True) | Q(number__gt=100),
        group__is_active=True,
    )
    .select_related("group")
    .order_by("-number")
    .limit(20)
    .all()
)

Здесь остаётся тот же смысл:

  • Q(is_valid=True) | Q(number__gt=100) описывает условие OR;

  • group__is_active=True фильтрует по связанной модели;

  • select_related("group") явно говорит, что связь нужно загрузить заранее;

  • order_by("-number") сортирует по убыванию;

  • limit(20) ограничивает результат;

  • .all() выполняет запрос.

Главная цель такого API — приблизить код к намерению, не теряя SQLAlchemy как основу. Такой слой может уменьшить количество повторяющегося кода.

Основа query-manager

query-manager привязывается к SQLAlchemy-модели и работает как chainable query builder.

Пример:

items = (
    Item.query_manager
    .where(is_valid=True)
    .where(number__gte=100)
    .order_by("-number")
    .limit(20)
    .all()
)

Каждый вызов добавляет состояние будущего запроса. Сам запрос не выполняется сразу после .where() или .order_by().

Выполнение происходит только в конце при:

.all()
.first()
.get(...)
.count()
.exists(...)

То есть manager не пытается неявно ходить в базу на каждом шаге. Он собирает запрос, а затем выполняет его в конце цепочки.

Lookup-строки вместо повторяющихся выражений

Самая полезная часть для меня — упрощение написания условий в запросе.

Вместо такого условия:

Item.number >= 100

можно написать:

number__gte=100

Вместо ручного join и фильтра по связанной модели:

select(Item).join(Item.group).where(Group.is_active == True)

можно написать:

group__is_active=True

Ключ group__is_active несёт в себе путь:

group__is_active

Это можно прочитать так:

relationship: group
field:        is_active
operator:     equality

Если путь глубже, он разбирается так же:

group__owner__email__isnull=False

Здесь смысл уже такой:

relationship path: group -> owner
field:             email
operator:          isnull

В обычном SQLAlchemy это тоже можно сделать без проблем. Но каждый раз нужно явно писать переходы по relationshipjoin и условиям. В CRUD-heavy коде такая механика быстро начинает повторяться.

Lookup-строки как раз забирают на себя эту рутинную часть.

Примеры операторов

Простейший случай — equality:

Item.query_manager.where(is_valid=True).all()

Для сравнений можно использовать суффиксы:

Item.query_manager.where(number__gt=100).all()
Item.query_manager.where(number__gte=100).all()
Item.query_manager.where(number__lt=500).all()
Item.query_manager.where(number__lte=500).all()

Для проверки на NULL:

Item.query_manager.where(name__isnull=False).all()

Для фильтрации по списку:

Item.query_manager.where(number__in=[1, 2, 3]).all()

Для строковых условий можно использовать like и ilike:

Item.query_manager.where(name__like="test%").all()
Item.query_manager.where(name__ilike="test%").all()

Идея в том, чтобы часто используемые фильтры можно было записывать компактно и одинаково во всём приложении.

Q-объекты для составных условий

Keyword-фильтры естественно складываются через AND:

items = (
    Item.query_manager
    .where(
        is_valid=True,
        number__gte=100,
    )
    .all()
)

Это читается как:

is_valid = true AND number >= 100

Но в реальном коде часто нужен OR.

Для этого используются Q-объекты:

items = (
    Item.query_manager
    .where(
        Q(is_valid=True) | Q(number__gt=100)
    )
    .all()
)

Можно группировать условия:

items = (
    Item.query_manager
    .where(
        (Q(is_valid=True) | Q(number__gt=100))
        & Q(group__is_active=True)
    )
    .all()
)

Такой код читается почти как само условие:

(is_valid = true OR number > 100) AND group.is_active = true

SQLAlchemy уже умеет делать это через or_() и and_():

where(
    and_(
        or_(
            Item.is_valid == True,
            Item.number > 100,
        ),
        Group.is_active == True,
    )
)

Q-объекты полезны там, где условия собираются динамически: из query parameters, фильтров в API, формы поиска или внутренних правил бизнес-логики.

Фильтры по relationship

Отдельный случай — фильтрация по связанным моделям.

Например, есть Item, у которого есть relationship group, а у Group есть поле is_active.

В чистом SQLAlchemy мы обычно пишем что-то вроде:

stmt = (
    select(Item)
    .join(Item.group)
    .where(Group.is_active == True)
)

Через manager это можно записать так:

items = (
    Item.query_manager
    .where(group__is_active=True)
    .all()
)

Если связь вложенная, путь продолжается:

items = (
    Item.query_manager
    .where(group__owner__email__isnull=False)
    .all()
)

Внутри manager разбирает lookup key по __, определяет путь по связанным моделям, находит конечное поле и строит соответствующее условие.

Упрощённо процесс выглядит так:

group__owner__email__isnull=False

превращается в:

пройти от Item к group;
пройти от Group к owner;
взять поле Owner.email;
применить оператор isnull;
добавить нужные JOIN-ы и условие в WHERE.

Это один из тех случаев, где абстракция действительно упрощает работу. Особенно если в проекте много фильтров по связанным сущностям.

Eager loading должен быть виден в коде запроса

Ещё одна вещь, которую я хотел оставить явной, — это eager loading.

В web-приложениях session scope часто короткий. Кроме того, при работе через sessionmaker или context manager пакет после операции может отдавать уже detached-объекты. В такой ситуации обращение к lazy relationship после выполнения запроса легко приводит к DetachedInstanceError.

Поэтому загрузка связей должна быть видна прямо в query chain:

items = (
    Item.query_manager
    .select_related("group")
    .all()
)

Или так:

items = (
    Item.query_manager
    .prefetch_related("group")
    .all()
)

Названия вдохновлены Django, но цель здесь практическая: сделать загрузку relationship явной на уровне кода. Внутри select_related() использует JOIN-based loading, а prefetch_related() — отдельную загрузку через selectinload.

Когда в запросе написано:

.select_related("group")

сразу понятно, что group должен быть загружен вместе с Item.

SQL preview

Любая обёртка над базой данных становится опасной, если разработчик перестаёт понимать, какой SQL она генерирует.

Поэтому в manager есть возможность посмотреть итоговый SQL:

sql = (
    Item.query_manager
    .where(
        Q(is_valid=True) | Q(number__gt=100),
        group__is_active=True,
    )
    .get_sql_query()
)

print(sql)

Для меня это было важно так как очень часто приходится смотреть на реальный SQL запросы при правке багов или написания нового функционала.

CRUD helpers

В пакете также есть базовые CRUD helpers:

Item.query_manager.create(...)
Item.query_manager.bulk_create(...)
Item.query_manager.where(id=1).update(...)
Item.query_manager.where(is_valid=False).delete()

Например, update привязан к фильтру:

Item.query_manager.where(id=1).update(name="updated")

То же самое с delete:

Item.query_manager.where(is_valid=False).delete()

Мне кажется, это нормальная форма. Сначала явно задаётся набор строк, к которому применяется операция, и только потом вызывается update() или delete().

Aggregates и count

Для обычных backend-задач часто нужны простые запросы на подсчёт строк, проверку наличия записи или получение агрегированных значений.

Примеры:

count = Item.query_manager.where(is_valid=True).count()
exists = Item.query_manager.where(number__gt=100).exists()

Для агрегатов есть отдельный метод aggregate():

from sqlalchemy_query_manager.core.helpers import Sum, Count, Avg, Min, Max

stats = Item.query_manager.where(is_valid=True).aggregate(
    total=Sum("number"),
    count=Count("id"),
    avg=Avg("number"),
    min=Min("number"),
    max=Max("number"),
)

Такие вещи можно писать и через SQLAlchemy напрямую. Но если они часто встречаются рядом с остальными фильтрами, удобно иметь их в том же стиле.

Основной компромисс

У такого подхода есть понятный trade-off.

Мы выигрываем:

- более короткую запись типовых запросов;
- Django-style lookup notation;
- composable Q-фильтры;
- фильтрацию по relationship path;
- явный eager loading в query chain;
- меньше повторяющегося session/query boilerplate;
- возможность быстро посмотреть итоговый SQL.

Но теряем:

- часть явности;
- часть прямого контроля в каждой строке кода;
- не все функции SQLAlchemy могу поддерживаться

Мне кажется, на данный момент такой слой имеет смысл в проектах, где много обычных Flask/FastAPI endpoint-ов с фильтрацией, сортировкой, limit/offsetrelationship filters и простыми CRUD-операциями.

Итог

sqlalchemy-query-manager — это небольшой слой поверх SQLAlchemy для случаев, где обычные backend-запросы становятся слишком большими.

Код пакета можно посмотреть на GitHub:

https://github.com/ViAchKoN/sqlalchemy-query-manager

Установить можно используя pip:

pip install sqlalchemy-query-manager

Очень буду благодарен за обратную связь, так как планирую дорабатывать пакет далее.