Pull to refresh

Comments 21

Если сделаете ещё один шаг то сможете прийти к идее компонентов доступа к БД которые во времена Delphi/ Interbase активно испольщовались и позволяли использовать логику на уровне БД хранимые процедуры.

Как вариант борьбы с рассинхронизацией схемы и запросов в БД давно придумали views.

Которые позволяют адаптировать изменения под «старые» требования

Согласен с вашим ходом мысли. Как хранимые процедуры, так и View предоставляют возможность обеспечивать обратную совместимость запросов при миграции БД. На эту тему был интересный доклад от Макса Грамина на PGConf в этом году (https://pgconf.ru/talk/3033632).

Однако в обоих случаях остаётся открытым вопрос интеграции со стороны языка. Ни View, ни хранимые процедуры не дают статических проверок, интеграционного кода и тестов в языке программирования: это остаётся на плечах программиста.

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

А я примерно с этого же года использовал БД как источник истины и пришёл к тому, что он должен быть не там

Непонятно, чем model-first лучше чем code-first: определив типы и их отношения, мы как раз и описываем модель. В том же ef core есть куча точек расширений, в которых можно получить доступ к этой модели и сгенерировать на ее основе всё что угодно.

Спасибо за ссылку. Интересная статья. Ироничная подача - тоже класс!

Насколько я понял, вы предлагаете отказаться от дихотомии: DB-first/Code-first, - и ввести третье представление между БД и языками. Данное представление будет нейтральным как относительно языков, так и относительно БД. Ценность этой нейтральности ясна: гибкость и устойчивость этой модели к переходам как между языками, так и между БД. Но помимо ценности есть и цена: переход сложности задачи от 2х до 3х тел со всеми вытекающими.

На мой взгляд, предлагаемый вами подход корректен и более гибок, чем DB-First, но и сложнее. Когда гибкость в отношении выбора БД не является требованием к системе, эта сложность будет избыточной. Иными словами, какой подход выбирать, как всегда в инженерии, "зависит".

Не думаю, что сложнее. Этот подход похож на DB-First тем, что правится база данных и потом генерируется то, что вам там надо по схеме. Отличие только в одном. DB-First ломается на нескольких окружениях, когда есть локальная БД и всякие /dev/stage/prod. У каждой БД будет своя истина, а в model-first база данных подгоняется под модель механизмом синхронизации. Code-first решает эту же проблему путём выкладывания в спец. таблицу истории миграций и жёстким запретом на трогание БД руками. Что очень неудобно.

И кстати. Если вы генерите sql запросы по схеме, можете попробовать скооперировать с моим инструментом. Его задача - выдать вашему скрипту актуальное состояние схемы данных и для абсолютной гибкости подхода я и сделал скрипт на стороне пользователя тулзы.

DB-First ломается на нескольких окружениях, когда есть локальная БД и всякие /dev/stage/prod. У каждой БД будет своя истина, а в model-first база данных подгоняется под модель механизмом синхронизации. Code-first решает эту же проблему путём выкладывания в спец. таблицу истории миграций и жёстким запретом на трогание БД руками.

А как же инструменты миграции Flyway, Liquibase, Sqitch и тп? Там так же накатывание миграций автоматизируется за счёт спец-таблиц. Конкретно это вовсе не прерогатива code-first.

Если вы генерите sql запросы по схеме

Их пишет пользователь.

Когда я только начинал и увидел hibernate, думал, что лучше его не будет, но часто именно такая магия, абстракция, становится узким местом и болью, особенно, если БД это основа приложения.

На одном из проектов я познакомился с JOOQ, DSL для работы с базой данных и насколько же удобно и прозрачно все это, теперь я жесткий фатан JOOQ) Понятно, что если у вас маленький проект ORM упростит работу, но с ростом будет сложнее с ним работать

Спасибо за статью)

Спасибо! Да. jOOQ в том же лагере инструментов. Если интересно, в доках есть сравнение pGenie vs jOOQ.

А смотрели sqlc ? Goorm? Они тоже могут генерить

Про Gorm не скажу, это похоже не ниша SQL-First.

А sqlc - прямой конкурент.

Принципиальное отличие в том, что sqlc целится на разные БД, из-за чего вынужден приводить внутренние представления к пересечению множества их возможностей, отбрасывая особенности. Поэтому, например, он до сих пор не поддерживает композитные типы Postgres. pGenie сфокусирован на Postgres и уже поддерживает продвинутые типы, включая композиты, многомерные массивы, multirange.

Ещё большое отличие в подходе к анализу. pGenie опирается на сам Postgres, разворачивая временный контейнер, проигрывая в нём миграции и прогоняя каждый запрос с извлечением метаданных из информационных схем. Благодаря этому поддерживаются любые синтаксические ухищрения и запросы любой сложности. Версию Postgres пользователь может выбирать, добиваясь соответствия продовой среде. В sqlc самописный эмулятор, который породил бесконечный поток багов несоответствия поведению Postgres. В последнее время, насколько я понял, они от него из-за этого стали плавно уходить.

Подробное сравнение есть в документации.

Здорово, спасибо

У нас .net ef + hot chocolate graphql. И много sql вообще под капотом собирается. Например, фильтры для админок склеиваются с фильтрами для контроля доступа, сверху докидываются сортировки, и т.п. Понятно, что в этом случае контроль над sql вообще утерян, но нам вроде и не надо - для всяких табличек в админах особенно.

В твоем подходе какая-то композиция запросов предполагается? Или только средствами БД (вьюшки и т.п.)?

Если подразумевается динамическое построение запросов, то пока не предполагается.

В демо-проекте есть пример со статическим запросом, реализующим динамическую выборку фильтров, условий сортировки и выборки. Для примера, вот генерируемый из него код Java.

Вопрос немного из другого разреза разработки - а зачем вообще пытаться что-то рефлектить "из SQL", или "из языка"?

В чем проблема использовать БД как фронт использует бэк, делая запросы к хранимкам (если уж там есть сложная логика)? В таком случае при миграциях приложение вообще трогать не нужно будет, если старый вид данных, конечно, можно как-то получить. Достаточно изменить хранимку в БД - т.е. при bзменении БД вы остаетесь в парадигме лишь этой БД.

Что касается тестов - естественно, они внешние должны быть, тут ничего не сделать, но эндпоинты на хранимках избавляют от необходимости переделывать те тесты, которые уже были для них написаны, и нужно просто написать тесты только для нового или изменившегося функционала.

Вообще, использовать рефлекты при работы с БД - это очень заманчивая, но очень порочная практика, как и ORM, ИМХО. Это ускоряет старт, но жутко мешает потом в жизненном цикле. Обычные запросы и более понятны (не надо гадать, во что они там трансформируются промежуточным слоем) и легче могут быть оптимизированы или вообще заменены. Единственная проблема - смена БД. Но это крайне редкая проблема, можно сделать миграцию и ручками раз в 10 лет.

Ну, либо я не понял сути вопроса.

Возможно, у нас возникло недопонимание. Попытаюсь распутать.

Я не понял, что вы имели в виду под “рефлектами” в контексте статьи.

Обычные запросы и более понятны (не надо гадать, во что они там трансформируются промежуточным слоем) и легче могут быть оптимизированы или вообще заменены.

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

В чем проблема использовать БД как фронт использует бэк, делая запросы к хранимкам (если уж там есть сложная логика)? В таком случае при миграциях приложение вообще трогать не нужно будет, если старый вид данных, конечно, можно как-то получить. Достаточно изменить хранимку в БД - т.е. при bзменении БД вы остаетесь в парадигме лишь этой БД.

Здесь противоречия нет. С помощью хранимок действительно можно создать API БД, скрыв сами таблицы от клиентов как чёрный ящик. Вы действительно получите возможность обеспечить обратную совместимость и избегать изменения в клиентских приложениях при миграциях. У хранимок есть свои проблемы, но это вполне рабочая схема и она применяется много где. Очень хороший доклад в этом году на эту тему был от @mgramin на PGConf.

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

Эту часть pGenie может взять на себя и сделать надёжной. С точки зрения pGenie SELECT call_my_stored_procedure($param1, $param2) - обычный запрос с параметрами и результатом, а потому он всё так же может сгенерировать для него обвязочный код и обеспечить проверки в CI/CD на то, что ваши миграции действительно оставляют ваши процедуры обратно-совместимыми с кодом приложений.

Если интересно, уже была близкая по смыслу ветка обсуждения.

Под рефлектами я имел ввиду отражение кого-либо экземпляра класса (его данных) в/из БД.

Да, сейчас стало понятней, что имелось ввиду, благодарю. Т.е. получается этакий линтер по типам и самим параметрам + возможность отслеживать, не нраушится ли флоу вследствии изменений структуры БД? Вот это инетресно, да.

Скорее, не линтер, а тайпчекер. То есть, он не антипаттерны выявляет, а несоответствия того, как вы БД используете с тем, что у вас в схеме. В этом процессе оно неизбежно выводит типы, и как следствие имеет всё, что нужно для кодогенерации, а потому и генерирует код. Ну а дальше включаются дополнительные фичи вроде файлов сигнатур, которые фиксируют типы ваших запросов в коде и добавляют новые свистелки вроде указания того, является ли запрос идемпотентным, что открывает возможность для авторетраев. Короче говоря, потенциала там много. Гляньте learn-pgenie-in-y-minutes для быстрой экскурсии.

Линтеры SQL есть существующие, поэтому в эту поляну пока лезть не буду.

А можно ли подружить ваш пигини с sqlx? Я уже понял что придется писать свой генератор, вопрос на сколько это сложно?

Допустим у меня один сэлект возвращает всю строку целиком. По моим конвенциям названия таблиц множественное число, а в расте я хочу что бы названия типов были единственного числа, значит мне нужно переименовывать пгшные типы по правилам в общем случае и я бы хотел вручную задать имя типа в расте не по правилам когда надо. Генератор такое умеет? Думаю правило не сложно сделать в самом генераторе, а вот как передать метаинформацию в гегератор чтобы смапать пгшный тип на произвольный растовский тип? По идее это мапа должна быть в ваших файлах сигнатур, но я не заметил по списку того что там может быть возможность задать мапу. Либо сигнатуры должны описывать генератор специфичные секции либо это должен быть еще один слой поверх сигнатур. Короче не похоже что без больших изменений это возможно - а надо.

Дальше вопрос по композитам, продолжение вопроса переименований, но в общем виде. Допустим мой кверик возвращает строку целиком + еще один скалярный флаг/енум. У меня есть такие кверики и я бы хотел что бы часть результирующего композита, которая строка, получила тип в расте тот же что я задал для обычного селекта * для этой таблицы. Короче мне надо не просто задавать производьные имена типам в расте, но и еще чтобы генератор понимал, что он должен использовать эти имена консистентно и в композитах тоже, что бы раст обьекты совпадали.

Миграции - у нас сложные миграции не чистый скул, скул + код. Есть миграции на тайпскрипте и недавно начали писать на расте. Как подружить ваш пигини с такими миграциями? Думаю никак, тут у вас ошибка проектирования генератор не должен требовать каких либо миграций вообще. Это не его дело, ему нужна тупо конекция к аптудэйт базе, а как она мигрируется это не его забота. Малотого анализ работает не совсем одинаково на пустой и не пустой базе, но это слабый аргумент т.к. к прод базе конечно никто ради анализа не полезет, но все равно очень удобно иметь не пустую базу (у нас даунсайз прода в отдельном контейнере и мигрируется также как и прод) и дебажить кверики на ней. Да и генерация быстрее не надо мигрировать каждый раз чтобы сигнатуры посчитать.

Я это все спрашиваю не просто так, у нас есть похожий генератор, только проще. Ограничен пг -> раст. Мы используем sqlx, тоже есть много чего, анализ индексов телеметрия (у вас кстати нет), композиты, нулаблы, форс нот нулл в результатах, компайл тайм генерация. Короче разница с вашим проектом в простоте https://github.com/thepartly/automodel

Ваш пигини конечно более правильный архитектурно (за исключением миграций), но нам ехать, а не шашечки.

Спасибо за развернутый фидбек! Очень ценно.

Классный термин вы ввели: “Reverse ORM”. Ёмко! Не встречал.

А можно ли подружить ваш пигини с sqlx?

Не проблема! Хотел бы только узнать, почему предпочитаете его tokio-postgres?

Спрашиваю потому, что изначально колебался для какой библиотеки писать первый генератор. В итоге выбрал tokio-postgres, потому что он более легковесный и сфокусирован на PostgreSQL. Но с другой стороны для него явно не хватает решения с одной кассой. С пулом, prepared statements, транзакциями и нормальной эргономикой. deadpool-postgres не дотягивает. Я подумывал под это дело расширить генератор, чтобы опционально с батарейками код выдавал или библиотеку расширительную выпустить, но тут надо принимать решение с пользователями. Отсюда и мои вопросы :)

Я уже понял что придется писать свой генератор, вопрос на сколько это сложно?

Несложно. Процесс устаканился. Уже есть 3 генератора как пример. Берёте входные данные как из того же демо-проекта. Руками пишете код, который хотели бы, чтобы генератор на эту модель выдавал. Передаёте это всё в LLM, а дальше напильником доводите до ума. Практика показывает, что нескольких итераций хватает для чего-то, чем можно начинать пользоваться.

Соответственно, если задача - расширить существующий генератор, то это ещё проще. Форкаете, командуете LLM изменить генератор так, чтобы он выдавал то, что вы хотите.

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

Если хотите расширить сам инструмент, приглашаю в Github Discussions. Если понадобится помощь с приватным кодом, обращайтесь по линии консалтинга. Я в РФ.

Допустим у меня один сэлект возвращает всю строку целиком. По моим конвенциям названия таблиц множественное число, а в расте я хочу что бы названия типов были единственного числа, значит мне нужно переименовывать пгшные типы по правилам в общем случае и я бы хотел вручную задать имя типа в расте не по правилам когда надо. Генератор такое умеет?

Названия таблиц pGenie вообще не касаются. Из названий его заботят только три категории: название запроса, его параметров и колонок в его результатах. Это предопределяет структуру генерируемого кода.

Так, в примере генерируемого запроса из демо-проекта, на исходный запрос /queries/select_album_by_format.sql создаётся модуль statements::select_album_by_format, в котором его параметрам соответствует тип данных Input, а его результатам - Output, который является алиасом к вектору над типом OutputRow, который объявляется в том же модуле.

Такая структура кода не типична для кодогенераторов, однако она воплощает архитектурный принцип low-coupling/high-cohesion. Запросы не связаны между собой содержащим модулем (они объявляются в отдельных модулях), а содержимое каждого модуля запроса максимально сплочённо (все детали, относящиеся к одному запросу, находятся в одном месте). Помимо предсказуемости и удобства навигации это ещё ускоряет компиляцию, так как компилятор может компилировать модули параллельно, а изменения в одном запросе не влияют на другие. Когда запросов накопятся сотни, это будет иметь значение.

Тут вытекает вопрос: а что из этого вы хотели бы переименовывать?

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

Генераторы конфигурируемы. Генератор сам определяет контракт требуемой им конфигурации. Так что, как минимум, вы можете передавать общие правила или те же мапы. Вот пример конфигурации специфичной для генератора java.

Либо сигнатуры должны описывать генератор специфичные секции либо это должен быть еще один слой поверх сигнатур. Короче не похоже что без больших изменений это возможно - а надо.

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

Дальше вопрос по композитам, продолжение вопроса переименований, но в общем виде. Допустим мой кверик возвращает строку целиком + еще один скалярный флаг/енум. У меня есть такие кверики и я бы хотел что бы часть результирующего композита, которая строка, получила тип в расте тот же что я задал для обычного селекта * для этой таблицы. Короче мне надо не просто задавать производьные имена типам в расте, но и еще чтобы генератор понимал, что он должен использовать эти имена консистентно и в композитах тоже, что бы раст обьекты совпадали.

Это уж очень тонкая настройка пошла. Так ли она важна практически? Реализовывать дорого как в плане дизайна, так и усложнения системы, а практическая польза в чём?

Похоже, это ещё и вступает в противоречие с одним из инсайтов из обсуждаемой статьи:

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

Сейчас правило простое: у каждого запроса свои типы. Чего предлагаемое усложнение нам даст с практической точки зрения?

Миграции - у нас сложные миграции не чистый скул, скул + код. Есть миграции на тайпскрипте и недавно начали писать на расте. Как подружить ваш пигини с такими миграциями? Думаю никак, тут у вас ошибка проектирования генератор не должен требовать каких либо миграций вообще. Это не его дело, ему нужна тупо конекция к аптудэйт базе, а как она мигрируется это не его забота. Малотого анализ работает не совсем одинаково на пустой и не пустой базе, но это слабый аргумент т.к. к прод базе конечно никто ради анализа не полезет, но все равно очень удобно иметь не пустую базу (у нас даунсайз прода в отдельном контейнере и мигрируется также как и прод) и дебажить кверики на ней. Да и генерация быстрее не надо мигрировать каждый раз чтобы сигнатуры посчитать.

Можно написать простой скрипт, который проиграет во временной БД ваши миграции, используя ваше текущее решение. Дальше сделать дамп получившейся схемы в миграцию /migrations/1.sql. Дальше запускается pGenie.

Для разработки это может стать билд-скриптом или инструкцией Makefile. В CI/CD это настроить тоже не проблема. Заоптимизировать тоже пространство есть: обновлять /migrations/1.sql только при изменении ваших текущих миграций.

Я это все спрашиваю не просто так, у нас есть похожий генератор, только проще. Ограничен пг -> раст. Мы используем sqlx, тоже есть много чего, анализ индексов телеметрия (у вас кстати нет), композиты, нулаблы, форс нот нулл в результатах, компайл тайм генерация. Короче разница с вашим проектом в простоте https://github.com/thepartly/automodel

Познакомлюсь с вашим проектом плотнее, спасибо за ссылку.

Соглашусь, телеметрию стоит, как минимум, опционально встроить в генератор. Ну это просто вопрос доработки генератора.

Ваш пигини конечно более правильный архитектурно (за исключением миграций), но нам ехать, а не шашечки.

Понимаю и нацелен двигаться в сторону практических кейсов. Если вам интересно, может сделать с вами пилот. Вам интеграция, мне бизнес-кейс. Rust для меня - приоритетное направление.

Это уж очень тонкая настройка пошла. Так ли она важна практически?

Это очень важное практическое свойство. Там выше вы писали что каждый кверик генерирует свой набор типов в расте - это ошибка проектирования. Все кверики врзвращаюшие одинаковые данные должны использовать одинаковые типы иначе как всем этим пользоваться? Писать руками from для каждого кверика в доменный тип? А зачем тогда вообще использовать ваш генератор?

Поймите ценность гегератора не в том что бы с ноля начинать писать проект по феншую (как автор генератора считает правильным) а в том чтобы он облегчал практические задачи (особенно бойлерплэйт) вокруг уже существующего кода. Доменные типы уже есть, никто не хочет тратить время на написание в ручную и поддержку преобразований в/из типов базы. Это кстати ответ почему sqlx он позволяет смапать доменный тип на тип базы и проверить тоже в компил тайме только дороже по времени компиляции чем наш генератор. Еще sqlx умееет в динамические кверики, ну и самый главный критерий - так решила команда.

Реализовывать дорого как в плане дизайна, так и усложнения системы

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

Сейчас правило простое: у каждого запроса свои типы. Чего предлагаемое усложнение нам даст с практической точки зрения?

Ну надеюсь из вышесказанного понятно зачем это надо. У нас много таких квериков уже и это очень важно чтобы интеграция с существующим кодом была максимально проста. Ваше боязнь скрытых зависимостей необоснована, во первых они не скрытые, а задаются самими типами из базы - ну какой смысл генерировать 100500 обсалютно одинаковых (по полям) но совершенно ге вщаимозаменяемых структур? Если один кверик читающий строку целиком поломался то очевидно все остальные которые выдают точно такую же строку обязаны поломаться, а при ваших 100500 разных типов я буду должен руками фиксить это не один раз а 100500.

Я понял у нас с вами несовместимость по философии - вам важно шашечки / как "правильно", а нам важно ехать. На практике это значит, что генератор не имеет право ничего навязывать/требовать изменить в проекте, мне не надо тратить время писать какие то скрипты, что бы после миграций еще и сдампить схему чтобы потом ее импортнуть во временную базу ровно для того что уже и так доступно без всех этих телодвижений. Юникс философия, утилита должна заниматься своим делом и не лезть со своими советами/требованиями как жить всем остальным. Никто не будет танцевать вокруг того, что там вы как автор генератора считаете правильным. Если ваш генератор больно подружить с тем что есть, ну значит не судьба. А ваш генератор больно подружить. Простые/маленькие кастомизации должны требовать простых/маленьких усилий. Поменять драйвер на sqlx это большая кастомизация - писать новый генератор адекватная затрата. Смапать тип из базы на доменный это маленькая задача - писать для этого новый генератор не адекватное усилие. Подключить генератор к существующей базе должно быть оч просто, а не напиши скрипт который сдампит базу. В общем с такой философией нам не попути.

Sign up to leave a comment.

Articles