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

推荐订阅源

Security Archives - TechRepublic
Security Archives - TechRepublic
博客园 - Franky
U
Unit 42
有赞技术团队
有赞技术团队
P
Proofpoint News Feed
H
Hackread – Cybersecurity News, Data Breaches, AI and More
量子位
博客园 - 【当耐特】
罗磊的独立博客
B
Blog
Stack Overflow Blog
Stack Overflow Blog
F
Fortinet All Blogs
爱范儿
爱范儿
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
Hugging Face - Blog
Hugging Face - Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
腾讯CDC
月光博客
月光博客
酷 壳 – CoolShell
酷 壳 – CoolShell
A
About on SuperTechFans
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
V2EX
Microsoft Azure Blog
Microsoft Azure Blog
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
N
Netflix TechBlog - Medium
Google DeepMind News
Google DeepMind News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
MyScale Blog
MyScale Blog
阮一峰的网络日志
阮一峰的网络日志
H
Help Net Security
S
SegmentFault 最新的问题
aimingoo的专栏
aimingoo的专栏
B
Blog RSS Feed
L
LangChain Blog
T
Tailwind CSS Blog
WordPress大学
WordPress大学
GbyAI
GbyAI
Blog — PlanetScale
Blog — PlanetScale
V
Visual Studio Blog
Vercel News
Vercel News
Recent Announcements
Recent Announcements
博客园 - 司徒正美
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cybersecurity and Infrastructure Security Agency CISA
L
LINUX DO - 热门话题
Security Latest
Security Latest
T
Threat Research - Cisco Blogs

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 миллионов точек без потерь
Пишем свой SQL query builder на Python: DSL, кеширование в Redis и защита от инъекций
Antony_Glyzi · 2026-04-30 · via Все публикации подряд на Хабре

Уровень сложностиСложный

Время на прочтение7 мин

Охват и читатели527

Туториал

Я — Python-разработчик, и большую часть времени работаю с Django и пишу сырые SQL запросы. Мне нравится Django ORM и я не имею ничего против, но разбираясь с legacy кодом, я невольно стал задумываться, что файлы содержащие модели огромны. Вроде ничего такого, так и должно быть, проект большой и много кода. Это в порядке вещей, так я решил. Выбросил лишние мысли и стал жить дальше. Но через несколько месяцев, мне показалось, что моделям не хватает кеширование данных. Порой кусок данных в несколько тысяц строк приходится получать не один раз. В какой-то момент я задался вопрос, что было бы, если бы в Django не было бы моделей. И в этот момент все завертелось. Это был мой вызов, который заставил меня двигаться. Мне хотелось видеть что-то похожее с SQLAlchemy или похожим на простой SQL, но при этом не контактировать с моделями и иметь возможность кешировать данные из коробки, а также иметь защиту от инъекций. Это был взрыв мозга, который зарядил меня делать эту библиотеку по праздникам, выходным и моим отпускам. Я был этим так увлечен, как ребенок в свой рождественский день, что не заметил, как быстро выросла библиотека со своей ссылкой на pypi.

Вот так появилась идея и библиотека, которую я назвал CORMless — «запросы в объектном стиле без моделей». Звучит, наверно, смешно, но давайте разберёмся, что это значит на практике.

Проблема: когда ORM избыточна

ORM — мощный инструмент. Но есть сценарии, где он становится обузой:

  • Динамическая структура БД. Если у вас legacy-база или часто меняется структура БД, вы не можете заранее описать модели. Поддержка моделей превращается в бесконечную гонку. Что при этом придется делать: написать миграцию (к примеру, сервис миграций), обновить модель (возможно, в нескольких сервисах), согласовывать с командами, у которых есть зависимости по этому полю.

  • Аналитика и сложные отчёты. Когда нужны тройные JOIN с HAVING и GROUP BY, а не CRUD для одной таблицы. Также ORM поддерживает не все фичи СУБД.

  • Микросервисы-адаптеры. Часто нужно просто «сходить в БД, забрать данные и отдать их в другом формате». Заводить ради этого модели — как стрелять из пушки по воробьям.

Но писать сырой SQL руками — тоже не выход. Теряется объектность, появляется риск инъекций, код обрастает из f"SELECT * FROM {table}", а хотелось бы более менее объектный SDL.

В моей практике в Django-проектах в основном используются сырые SQL-запросы. ORM запросы тоже есть, но это если нужно выбрать запись из одной модели. Поэтому было желание уйти от сырых SQL запросов, но и не использовать модели.

Мне хотелось получить золотую середину: удобство цепочечного API, как в ORM, но без необходимости описывать классы моделей. И чтобы под капотом это был чистый, параметризованный SQL.

Архитектура: строим DSL на Python

Концепция проста: вы «просите» библиотеку прочитать структуру вашей БД, и она сама узнаёт, какие таблицы и поля существуют. Дальше вы строите запрос, как в Django ORM или SQLAlchemy Core:

from query_tables import Tables
from query_tables.db import SQLiteQuery

sqlite = SQLiteQuery('path/to/database.db')
table = Tables(sqlite, non_expired=True)  # включаем "вечный" кеш

# Простой запрос с фильтрацией
res = table['person'].filter(id=2).get()
# Вернёт список словарей: [{'person.id': 2, 'person.name': 'Anton 2', ...}]
# Наименование ключа - это название таблицы / alias таблицы, а через точку название поля.

Ключевое отличие от сырого SQL здесь — это объектное построение запроса. Методы .filter(), .join(), .select(), .group_by() не выполняются сразу. Они лишь собирают внутреннее представление запроса. А когда вы вызываете .get() — библиотека компилирует это в SQL, выполняет и возвращает результат.

Сложные JOIN без боли

Самой хардкорной частью разработки стала реализация вложенных JOIN. Я хотел, чтобы сложные запросы выглядели в коде так же структурированно, как и в голове у разработчика. Вот, например, как выглядит запрос с JOIN и фильтрацией по связанным таблицам:

from query_tables.query import Join, LeftJoin, AND, OR, Field, Ordering

query = table['person'].select(
    Field('person', 'id'), 
    Field('person', 'name'), 
    Field('person', 'age')
).join(
    Join(table['address'], Field('address', 'id'), Field('person', 'ref_address')).filter(
        OR( 
            AND(Field('address', 'street').like('%%ушкина'), Field('address', 'building').equ(10)),
            Field('address', 'building').in_([5, 10])
        )
    )
).join(
    LeftJoin(table['employees'], Field('employees', 'ref_person'), Field('person', 'id')).select(
        Field('employees', 'id'), 
        Field('employees', 'ref_person'), 
        Field('employees', 'ref_company'), 
        Field('employees', 'hired')
    )
).filter(
    Field('person', 'id').equ(1), Field('person', 'name').like('Ant%%')
).order_by(
    Field('person', 'age').desc()
)

res = query.get()

Класс Field здесь играет ключевую роль — он явно указывает, к какой таблице относится поле, и библиотека всегда может проверить, существует ли оно. Это убирает опечатки на этапе сборки запроса, а не на этапе выполнения в БД.

Библиотека сгенерирует такой SQL:

SELECT person.id, person.name, person.age, address.id, address.street, 
       address.building, employees.id, employees.ref_person, employees.ref_company, 
       employees.hired
FROM person
JOIN (
    SELECT address.id, address.street, address.building 
    FROM address
    WHERE ((address.street LIKE %(address_street_1)s AND address.building = %(address_building_2)s) 
           OR address.building IN (%(address_building_3)s, %(address_building_4)s))
) AS address ON address.id = person.ref_address
LEFT JOIN (
    SELECT employees.id, employees.ref_person, employees.ref_company, employees.hired 
    FROM employees
) AS employees ON employees.ref_person = person.id
WHERE person.id = %(person_id_1)s AND person.name LIKE %(person_name_2)s
ORDER BY person.age DESC

В сыром SQL запросе нет значений параметров, что предотвращает инъекций. Также из запроса можно увидеть, что каждая таблица подключается через подзапросы внутри JOIN. Это специально сделано, чтобы было визуальное соответствие с объектными запросами в стиле query_tables.

Кеширование: не просто «сохранить результат»

Отдельная большая тема — это кеширование. Просто взять и закешировать SQL-запрос недостаточно. Нужно знать, когда инвалидировать кеш.

Предположим, есть три запроса к БД:

  • query1: получает данные из таблиц person + address

  • query2: получает данные из таблиц person + address + employees + company

  • query3: получает данные из таблиц person + employees

Данные закешированы. Теперь вы делаете INSERT в таблицу address. Что должно произойти? Кеш query1 и query2 должен быть сброшен, а query3 — нет, потому что он не зависит от address. Моя библиотека делает именно это. Она оставляет кеш запроса, если по этим таблицам не было изменений.

# Вставка новой записи в address
table['address'].insert(street='Новая', building=999)

# После этого кеш query1 и query2 будет автоматически очищен.
# query3 останется нетронутым.

Как это работает под капотом: для каждого кеша хранится отображение “хеш запроса → данные”. Плюс обратный индекс: “таблица → список хешей запросов”. Именно он позволяет быстро найти, какой кеш нужно сбросить при изменении конкретной таблицы. При любом изменении данных через INSERT, UPDATE или DELETE библиотека находит все хеши, связанные с изменяемой таблицей, и удаляет их из кеша. Это инвалидация по тегам (таблицам), реализованная вручную.

Поддерживается два типа кеша:

  • In-memory: на основе aiocache или cachetools (TTLCache или LRUCache). Подходит для приложений, работающих в одном процессе.

  • Redis: распределённый кеш, который могут использовать несколько экземпляров приложения одновременно.

В обоих случаях реализована защита от гонок.

С одной стороны можно было бы обойтись бы и одним видом кеша, скажем через Redis. Это было бы хорошим решением для Django. Но это стало бы ограничением для тех сервисов, которые не иcпользуют внешнее кеширование, а сохраняют значения в памяти процесса. Поэтому два вида кеша.

Синхронность и асинхронность: две параллельные вселенные

Python-экосистема сейчас раздвоена: есть синхронный мир (Django, Flask с WSGI) и асинхронный (FastAPI, aiohttp). Я хотел, чтобы библиотека работала в обоих мирах с одинаковым API.

Итоговая архитектура:

  • Синхронная версия: SQLiteQuery, PostgresQuery, Tables, CacheQuery, RedisCache.

  • Асинхронная версия: AsyncSQLiteQuery, AsyncPostgresQuery, TablesAsync, AsyncCacheQuery, AsyncRedisCache.

С точки зрения пользователя, код отличается только наличием await перед методами:

# Синхронный код
res = table['person'].filter(id=2).get()

# Асинхронный код
res = await async_table['person'].filter(id=2).get()

Под капотом пришлось построить параллельные иерархии классов. Самое сложное было — не допустить дублирования логики построения запросов. Поэтому она вынесена в общие модули (query/query.py, query/condition.py). Класс Query, отвечающий за компиляцию объектного DSL в SQL-строку, используется и синхронной, и асинхронной версией без каких-либо изменений. А вот специфика работы с БД (соединения, пулы) и кешем инкапсулирована в отдельных классах — QueryTable и AsyncQueryTable.

Безопасность: защита от инъекций на уровне библиотеки

Один из главных принципов при разработке — никакой конкатенации пользовательских данных в SQL. Вообще.

Библиотека использует параметризованные запросы. Все значения, которые вы передаёте в .filter() или .insert(), не вставляются напрямую в строку запроса. Вместо этого генерируется плейсхолдер %(field_name_N)s, а само значение уходит в словарь параметров:

# Ваш код:
table['person'].filter(name__like='%%Anton%%')

# Генерируется SQL:
# SELECT ... WHERE person.name LIKE %(person_name_1)s

# И параметры:
# {'person_name_1': '%%Anton%%'}

База данных сама экранирует параметры. SQL-инъекция становится невозможной на фундаментальном уровне. Вам не нужно помнить про экранирование — библиотека делает это за вас.

Функции, GROUP BY и всё остальное

Для полноты картины — библиотека поддерживает практически все операторы SQL, которые могут понадобиться в повседневной работе:

  • Фильтрация: __like, __ilike, __in, __between, __gt/gte/lt/lte, __isnull, __regex, __iregex и их отрицания.

  • SQL-функции: Max, Min, Avg, Count, Sum, Concat, Upper, Lower, Substring, Replace, Case, Coalesce, Extract, Interval и другие.

  • Группировка и фильтрация после группировки: GROUP BY + HAVING.

  • Сырые запросы: если возможностей DSL не хватает, всегда можно выполнить произвольный SQL через table.query('SELECT ...'), и его тоже можно кешировать.

Я не знал какие функции могут понадобиться другим, поэтому постарался добавить как можно больше возможностей со стороны CORMless.

Заключение

CORMless — это не замена SQLAlchemy или Django ORM. Это нишевый инструмент для ситуаций, когда модели избыточны, но писать сырой SQL не хочется.

Для меня этот проект стал отличной школой:

  • Проектирования DSL на Python.

  • Понимания, как работают ORM изнутри.

  • Реализации сложной логики кеширования с инвалидацией.

  • Построения двойных (синхронных и асинхронных) API без дублирования кода.

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


Ссылки