- BrainTools - https://www.braintools.ru -
Всем привет. Меня зовут Сергей и я руковожу разработкой автоматизации внутренних бизнес-процессов в рамках Центра управления процессами Московской Биржи.
В эпоху повсеместного распространения инструментов на базе искусственного интеллекта (ИИ) вопрос применения доменно-специфичных языков (DSL, domain specific language) вызывает неоднозначную реакцию [2]. С одной стороны, большие языковые модели прекрасно понимают широкое многообразие естественного языка и могут работать напрямую с ним. С другой стороны, присущая любому человеческому общению неоднозначность оставляет простор для неправильных трактовок и ошибок.
Как отмечают многие исследователи, кодовые агенты наиболее эффективно работают в хорошо отстроенной системе ограничений. Ограничения могут быть мягкими, рекомендательными, как промпты и скиллы, так и жёсткими, например, компилятор или автоматическое тестирование. Задача разработчика при использовании ИИ сводится не столько к написанию кода, сколько к формированию качественного контекста и набора ограничений. Ручное тестирование и выявление ошибок кардинально увеличивает цикл обратной связи для ИИ-агента и должно применяться только в крайних случаях, когда цена ошибки [3] слишком высока.
Мы в компании MOEX внимательно относимся к процессу разработки и рекомендациям службы информационной безопасности, чтобы предупредить последствия публикации некачественного кода. Это значит, что все наши проекты проходят проверки статическими и динамическими анализаторами кода, проходят композиционный анализ и тесты на проникновение. Со своей стороны мы делаем всё возможное, чтобы соответствовать положению 757-П Центрального Банка РФ «Об установлении обязательных для некредитных финансовых организаций требований к обеспечению защиты информации при осуществлении деятельности в сфере финансовых рынков в целях противодействия осуществлению незаконных финансовых операций».
Наиболее быструю обратную связь ИИ-агент получает от компилятора (интерпретатора) языка. Если мы хотим обеспечить строгую семантику критической части нашей логики, то есть проверенный способ построения доменно-специфичных языков на основе языков программирования общего назначения (GPPL, general purpose programming language). В статье рассмотрен подход по формированию типо-безопасного доступа к данным в условиях нестандартного интерфейса источника данных и его логических ограничений.
Каждый бэкенд-разработчик, работавший с реляционными базами, знает соблазн «просто собрать SQL-строку конкатенацией». Соблазн этот опасен: динамический SQL — классический вектор SQL-инъекций, и специалисты по информационной безопасности справедливо бьют по рукам за подобный код. Разбираемый в этой статье демо-проект [1] показывает элегантное решение: типо-безопасный DSL на базе библиотеки jOOQ, который превращает Java-код в SQL-запросы к резидентному хранилищу данных (In-Memory Data Grid, IMDG) вроде Apache Ignite, доступному только через кастомный REST-контракт. Ниже — разбор исторического контекста DSL, детальный анализ архитектуры проекта, аналогия с JPA (Java Persistence API) и взгляд на будущее DSL в эпоху ИИ-ассистированной разработки.
Идея предметно-ориентированных языков не нова — она восходит к Lisp 1958 года, разработанному Джоном Маккарти, чья гомоиконность и макросистема позволяли встраивать мини-языки прямо в код общего назначения [2]. С тех пор DSL прошли путь от академических экспериментов до промышленного стандарта: SQL остаётся, вероятно, самым успешным DSL в истории, специализированным исключительно на манипуляции данными [3].
Классическая методология разработки DSL, зафиксированная ещё в работах по предметно-ориентированному программированию, включает три фазы [2][4]:
анализ — определение границ предметной области, сбор знаний экспертов, построение семантических нотаций;
реализация — создание библиотеки, воплощающей эти нотации, и компилятора/интерпретатора, транслирующего DSL-программы в вызовы этой библиотеки;
применение — написание и выполнение DSL-программ конечными разработчиками.
Исторически DSL делятся на внешние (со своим синтаксисом и парсером, как BNF (форма грамматики Бэкуса-Наура) или регулярные выражения) и внутренние — встроенные в синтаксис языка-хозяина через классы, методы и fluent-интерфейсы [5]. Второй подход резко снижает стоимость разработки: не нужен собственный парсер, IDE «из коробки» даёт автодополнение и проверку типов на этапе компиляции. Именно на этом принципе построены jOOQ, LINQ в .NET и Criteria API в JPA — все они реализуют внутренний DSL для построения запросов средствами языка общего назначения, избегая при этом уязвимостей текстовой конкатенации SQL. Ключевая мотивация [4] подобных решений всегда одна и та же: изолировать разработчика от синтаксических ошибок и рисков инъекций, зафиксировав грамматику допустимых операций на уровне API [6].
К 2020-м годам интерес [5] к DSL пережил новую волну — от финансовых расчётов до биоинформатики и низкокодовых платформ, а инструментарий для их создания расширился: ANTLR, Xtext, JetBrains MPS, встроенные метапрограммные возможности Kotlin и Scala [7]. Разбираемый ниже проект — характерный пример именно внутреннего DSL, решающего узкую, но болезненную задачу: безопасный доступ к нестандартному хранилищу данных.
В качестве примера возьмём структуру данных пользователей торговой системы Spectra срочного рынка Московской Биржи. Её контракт публичный и доступен для изучения всем желающим [17].
Проект jooq-dsl-demo [1] моделирует ситуацию, типичную для крупных финансовых и биржевых инфраструктур: данные хранятся в резидентной сетке (IMDG на основе Apache Ignite), доступ к которой предоставляется не через стандартный JDBC-драйвер, а через кастомный REST-эндпоинт /api/v2/queries/adhoc, принимающий SQL-текст в теле POST-запроса. Такая архитектура типична для Ignite: даже нативный REST API движка построен вокруг команд qryfldexe с SQL-подобным синтаксисом, инкапсулированным в HTTP-запрос [8].
При этом IMDG предметной области накладывает существенные ограничения на диалект SQL [1]:
отсутствует Data Definition Language (DDL) — схему нельзя изменить средствами SQL;
отсутствуют операции записи — INSERT, UPDATE, DELETE;
отсутствует поддержка JOIN между таблицами в SELECT-запросах;
имена «таблиц» фактически являются полностью квалифицированными именами Java-классов, а не классическими SQL-идентификаторами.
Эти ограничения — прямое следствие природы Ignite как объектно-ориентированного in-memory хранилища, где «таблица» — это по сути кэш объектов одного типа, а не запись в нормализованной реляционной схеме [9][10]. Именно поэтому официальная документация Ignite явно предупреждает: SqlFieldsQuery поддерживает произвольный SELECT, но объединения между кэшами требуют явного префиксирования именами кэшей, а не стандартного JOIN-синтаксиса [10].
Схема архитектуры демо-проекта наглядно показывает три слоя: DSL-код на Java, runtime jOOQ, транспортный адаптер к REST и сам сервис IMDG. Разработчикам, знакомым с Java Persistence API, архитектура проекта покажется узнаваемой. JPA и Hibernate решают концептуально ту же задачу — дать разработчику безопасный, типизированный способ работать с хранилищем данных, не обращаясь к «сырому» SQL напрямую. Сопоставление слоёв даёт наглядную аналогию.
|
Слой ответственности |
JPA / Hibernate |
jOOQ-DSL для IMDG |
|
Объектная модель |
|
Классы |
|
Источник схемы |
Аннотации в коде или обратная генерация из БД |
Файл |
|
Транспортный слой |
|
|
|
Язык запросов |
JPQL, Criteria API |
|
|
Контроль допустимых операций |
Ограничения диалекта JPQL, каскады, |
|
Центральная идея обеих экосистем — отделить бизнес-код от деталей транспорта и синтаксиса запроса. В JPA это достигается через EntityManager, который абстрагирует JDBC; в разбираемом проекте эту роль исполняет класс RestMockDataProvider, реализующий интерфейс MockDataProvider из тестового набора jOOQ [1]. Он подключается к MockConnection, которая реализует java.sql.Connection, поэтому весь остальной код — конфигурация DSLContext, построение запроса — работает так, будто использует настоящий JDBC-драйвер, хотя фактически SQL-текст, полученный из AST после inlineSql(), отправляется HTTP-клиентом в теле JSON-запроса на REST-эндпоинт сервиса.
Ключевой инженерный трюк проекта — обратная разработка (reverse engineering) схемы данных. Поскольку сервис не предоставляет стандартного механизма получения DDL, мы конвертируем метаданные ответа сервиса в формате JSON (файл user-data.json) в XML-описание таблицы user-table.xml, соответствующее XSD-схеме jooq-meta [1]. Указанная трансформация JSON → XML выполнена с применением LLM-модели [1] — то есть уже на этапе разработки инструмента искусственный интеллект использовался как ассистент кодогенерации метаданных.
Файл user-table.xml описывает каталог default, схему public и единственную таблицу user с двадцатью одной колонкой типов TIMESTAMP, BIGINT, INTEGER, SMALLINT и VARCHAR, а также первичный ключ по полю login. Именно этот XML-файл читает jooq-codegen-maven, встроенный в сборку проекта, и генерирует на его основе Java-классы USER (метаданные таблицы) и UserRecord (типизированная строка результата) — аналог того, как Hibernate по аннотациям @Entity строит внутреннюю объектную модель. Разница в том, что в мире IMDG обратный путь короче: нет команды SHOW CREATE TABLE, поэтому схему нужно либо получать из документации сервиса, либо — как в этом случае — синтезировать её из примера ответа API, распознав типы значений и структуру полей.
Ограничения предметной области — отсутствие записи и джойнов — реализованы не как декларативные настройки, а как явные перехватчики (listener), встроенные в конфигурацию DSLContext через провайдеры jOOQ [1]:
ReadOnlyExecuteListener расширяет DefaultExecuteListener и в методе renderStart проверяет тип выполняемой операции (ExecuteContext.type()); если тип равен ExecuteType.WRITE, выбрасывается DataAccessException с сообщением о запрете INSERT/UPDATE/DELETE
NoJoinVisitListener расширяет DefaultVisitListener и в методе visitStart анализирует узлы AST‑дерева запроса; при обнаружении узла Clause.TABLE_JOIN также выбрасывается исключение, блокирующее компиляцию запроса до отправки на сервер
Оба перехватчика подключаются к DefaultConfiguration через DefaultExecuteListenerProvider и DefaultVisitListenerProvider соответственно — паттерн, типичный для jOOQ, где поведение [6] библиотеки расширяется через слушатели жизненного цикла запроса, а не через модификацию генерируемого кода. Это принципиально отличает подход от «постфактум»-валидации SQL-строки регулярными выражениями: проверка происходит на уровне синтаксического дерева запроса, до его материализации в текст, что исключает как ложноположительные, так и ложноотрицательные срабатывания, характерные для парсинга готовой SQL-строки.
Third-party контракт REST API реализован в RestMockDataProvider.execute(): метод подставляет значения параметров прямо в SQL-текст (метод inlineSql), формирует JSON-тело с полем sql, отправляет POST-запрос на /api/v2/queries/adhoc через стандартный java.net.http.HttpClient и разбирает ответ, извлекая массив объектов из пути response.result в JSON. Далее полученные строки превращаются в jOOQ Result<Record> через вспомогательный DSLContext.newResult(), что позволяет использовать стандартный механизм result.into(UserRecord.class) для типизации итогового набора записей. Для локальной отладки без реального сервиса IMDG в проекте используется WireMock, эмулирующий REST-эндпоинт и возвращающий заранее подготовленный JSON-файл user-data.json [1].
В качестве развития инструмента можно рассмотреть доработку maven плагина кодогенерации, обеспечив возможность генерации кода из метаданных в формате JSON без промежуточного XML-представления.
Использование LLM для генерации XML-схемы из JSON-примера в разбираемом проекте — не случайность [7], а симптом более широкой тенденции. Внутренние DSL традиционно были компромиссом между выразительностью внешнего языка и простотой встраивания в существующую кодовую базу; ограничение всегда заключалось в том, что написание самого DSL и правил его валидации требовало экспертизы и в предметной области, и в языковой инженерии одновременно [6].
AI-инструменты меняют это уравнение сразу в нескольких направлениях. Во-первых, LLM снижают порог входа в domain-анализ: там, где ранее аналитику требовались недели интервью с экспертами предметной области для формализации семантики (классический этап анализа в методологии DSL [4]), сейчас модель может за минуты предложить первичную схему данных на основе примеров вывода системы — именно так был получен user-table.xml из user-data.json в разбираемом проекте. Во-вторых, LLM способны генерировать сам код DSL-обёртки — классы записей, конфигурации RenderMapping, перехватчики валидации — по текстовому описанию ограничений домена, ускоряя фазу реализации.
Более глубокая перспектива касается роли DSL как «контракта» между человеком и AI-агентом, генерирующим код. Строго типизированный внутренний DSL, подобный jOOQ, естественно ограничивает пространство возможных ошибок AI-генерации: компилятор Java отклонит некорректный вызов select().from().where() на этапе сборки, тогда как ошибка в текстовой SQL-строке, сгенерированной LLM, обнаружится только в runtime или вовсе останется незамеченной до продакшена. Комбинация «AI генерирует вызовы DSL» + «DSL гарантирует безопасность на уровне типов и AST-валидации» — жизнеспособный паттерн для доверенной AI-кодогенерации в чувствительных доменах, включая финансовую инфраструктуру, где написан этот демо-проект.
Одновременно ограничения текущих LLM создают новые требования к дизайну DSL: языки и API должны быть спроектированы так, чтобы модели было легко их «понимать» и корректно применять — с предсказуемой, консистентной сигнатурой методов, богатой типизацией и минимумом неявных побочных эффектов. Это созвучно классическим принципам хорошего DSL — минимализму и ясности синтаксиса [7] — но добавляет новый критерий: предсказуемость для statistical pattern matching, лежащего в основе генерации кода современными моделями.
Хотя разбираемый проект реализован на Java, сама идея — типобезопасная обёртка над нестандартным транспортом данных с проверкой AST на этапе построения запроса — универсальна и переносима на другие экосистемы.
|
Экосистема |
Аналог внутреннего DSL для запросов |
Особенности переноса подхода |
|
.NET / C# |
LINQ (Language Integrated Query) |
Компилятор C# нативно поддерживает выражения‑деревья ( |
|
Kotlin |
Exposed, Ktorm |
Kotlin DSL-builders на основе lambda-с-receiver дают синтаксис, близкий к SQL, при полной типобезопасности |
|
Scala |
Slick, Quill |
Богатая система типов и макросы Scala позволяют генерировать SQL на этапе компиляции, а не runtime |
|
Python |
SQLAlchemy Core, Django ORM QuerySet |
Динамическая типизация ограничивает compile-time проверки, но допускает runtime-валидацию через AST-подобные объекты выражений |
|
Rust |
Diesel, SeaORM |
Строгая система типов и трейты позволяют закодировать ограничения предметной области (например, «только SELECT») прямо в типах на этапе компиляции |
|
Go |
Squirrel, GORM |
Отсутствие обобщений в старых версиях ограничивало выразительность DSL; начиная с Go 1.18 обобщённые типы приближают библиотеки к возможностям jOOQ |
Для функциональных языков, как Scala, стоит отметить, что их экосистема традиционно предлагает наиболее развитый инструментарий для встроенных DSL благодаря выразительной системе типов, неявным преобразованиям (implicit conversions) и макросам, что делает библиотеки Slick и Quill естественным аналогом jOOQ для доступа к нестандартным источникам данных, включая REST-обёртки над Ignite или другими IMDG. Общий паттерн — «сгенерировать метамодель из схемы (реальной или восстановленной), обернуть транспорт в адаптер к стандартному интерфейсу платформы, добавить перехватчики для доменных ограничений» — переносим почти без изменений: в Rust роль перехватчиков могут взять на себя типы-обёртки (newtype pattern), запрещающие построение запроса с недопустимой клаузой уже на этапе компиляции, а в динамических языках типа Python — runtime-валидаторы AST, аналогичные NoJoinVisitListener.
Ключевой вывод для мультиязычных команд: чем более выразительна система типов языка, тем раньше — в идеале на этапе компиляции — можно перехватывать нарушения контракта предметной области, будь то запрет JOIN, запрет операций записи или иные бизнес-ограничения нестандартного хранилища данных. Это делает инвестиции в разработку внутреннего DSL оправданными именно для языков со статической типизацией и богатой системой типов — Scala, Kotlin, Rust, TypeScript — где безопасность, продемонстрированная в разбираемом Java-проекте, достигается «бесплатно» на этапе сборки, а не ценой дополнительных runtime-проверок.
В статье описан метод борьбы с распространённой уязвимостью «Инъекция» в сетевых сервисах. По данным популярного рейтинга безопасности «OWASP Top 10» за 2025 год проблема инъекций, по-прежнему, занимает стабильное место в середине списка. С учётом взрывного увеличения количества кода, сгенерированного ИИ, решение проблем защиты усложняется в разы. Своей целью в MOEX мы видим постоянный поиск и применение новых технологий, но со зрячим контролем результата и нивелированием возникающих вызовов для информационной безопасности.
GitHub — Unlocker/jooq‑dsl‑demo — исходный репозиторий с демо‑проектом DSL для доступа к IMDG через jOOQ (README, исходный код Java‑классов, user‑table.xml, user‑data.json,pom.xml) — github [8]
Domain Specific Language (DSL) Tools — методология анализа, реализации и применения DSL — 2006.secrus [9]
Apache Ignite Quick Start Guide, O’Reilly — Field based query — описание SqlFieldsQuery и ограничений SELECT‑запросов в Ignite — oreilly [10]
Концепция DSL и особенности практического применения — исторический и практический обзор DSL на русском языке — rusnauka [11]
Apache Ignite.NET — Class SqlFieldsQuery (официальная документация) — ignite.apache [12]
Apache Ignite — SQL API (официальная документация) — ignite.apache [13]
Querying with Apache Ignite (LinkedIn, Soumya Jyoti Banerjee) — практический разбор SQL‑запросов в Ignite — LinkedIn [14]
GridGain Documentation — REST API — описание REST‑контракта для доступа к данным IMDG — gridgain [15]
How to run Apache Ignite SQL queries over a REST API — stackoverflow [16]
Создание DSL: как разработать собственный язык для специфической предметной области — it‑classic [17]
GridGain SDK Javadoc — SqlFieldsQuery (Collocated Flag) — gridgain [18]
Apache Ignite SQL query composed primary key.NET — stackoverflow [19]
Предметно‑ориентированный язык — Википедия — ru.wikipedia [20]
Apache Ignite 3 Documentation — SQL API — ignite.apache [21]
Example of SQL query — Apache Mail Archives — lists.apache [22]
When and how to develop domain‑specific languages — академическая работа о паттернах разработки DSL — dl.acm [23]
Plaza II Шлюз. Версия 9.6 — Публичный FTP Московской Биржи — ftp.moex.com [24]
Автор: unlocker
Источник [25]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/36419
URLs in this post:
[1] интеллект: http://www.braintools.ru/article/7605
[2] реакцию: http://www.braintools.ru/article/1549
[3] ошибки: http://www.braintools.ru/article/4192
[4] мотивация: http://www.braintools.ru/article/9537
[5] интерес: http://www.braintools.ru/article/4220
[6] поведение: http://www.braintools.ru/article/9372
[7] случайность: http://www.braintools.ru/article/6560
[8] github: https://github.com/Unlocker/jooq-dsl-demo
[9] 2006.secrus: https://2006.secrus.org/upload/files/107.pdf
[10] oreilly: https://www.oreilly.com/library/view/apache-ignite-quick/9781789347531/1a7ffc0a-b55f-47d5-b8e1-7be0a636f7d5.xhtml
[11] rusnauka: http://www.rusnauka.com/29_DWS_2009/Informatica/53610.doc.htm
[12] ignite.apache: https://ignite.apache.org/releases/latest/dotnetdoc/api/Apache.Ignite.Core.Cache.Query.SqlFieldsQuery.html
[13] ignite.apache: https://ignite.apache.org/docs/latest/SQL/sql-api
[14] LinkedIn: https://www.linkedin.com/pulse/querying-apache-ignite-soumya-jyoti-banerjee
[15] gridgain: https://www.gridgain.com/docs/gridgain8/latest/developers-guide/restapi
[16] stackoverflow: https://stackoverflow.com/questions/48961733/how-to-run-apache-ignite-sql-queries-over-a-rest-api
[17] it‑classic: https://it-classic.ru/%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5-dsl-%D0%BA%D0%B0%D0%BA-%D1%80%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D1%82%D1%8C-%D1%81%D0%BE%D0%B1%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D1%8B/
[18] gridgain: https://www.gridgain.com/sdk/latest/javadoc/org/apache/ignite/cache/query/SqlFieldsQuery.html
[19] stackoverflow: https://stackoverflow.com/questions/43210949/apache-ignite-sql-query-composed-primary-key-net/43212267
[20] ru.wikipedia: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D1%8F%D0%B7%D1%8B%D0%BA
[21] ignite.apache: https://ignite.apache.org/suggested-site/docs/ignite3/3.1.0/api-reference/native-clients/dotnet/sql-api/
[22] lists.apache: https://lists.apache.org/thread/m6fk3y7jf08fls76oy5nz0qdn2dy7lgv
[23] dl.acm: https://dl.acm.org/doi/10.1145/1118890.1118892
[24] ftp.moex.com: http://ftp.moex.com
[25] Источник: https://habr.com/ru/companies/moex/articles/1089584/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1089584
Нажмите здесь для печати.