Comments 56
А что делать в ситуации когда нам нужна фильтрация по полям и в зависимости от запроса мы должны сформировать разный SQL?
Не надо разный SQL. Нужно один запрос с масимальным количеством полей. Если поле на задано (пустая строка) - по нему не фильтруем.
Пример:
CREATE OR REPLACE FUNCTION get_account_list(
IN a_login TEXT,
IN a_password TEXT,
IN a_limit INT,
IN a_offset INT
)
RETURNS TABLE (
list client_accounts,
full_count BIGINT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT a, count(*) OVER() AS full_count FROM client_accounts AS a
WHERE ((a.login = a_login) OR (a_login = '')) and
((a.password = a_password) OR (a_password = ''))
ORDER BY a.id ASC
LIMIT a_limit
OFFSET a_offset;
END;
$$;
ох насмотрелся я на такие "запросы" с максимальным количеством атрибутов, универсализированные "конструкторы", натчнутые на непродуманную семантическу модель без нормальной реализации средствами субд. куча тянущихся говноданных, нагрузка на субд, на сеть, на балансир и тд и тп., а если у вас структура поменялась? сколько sp отвалится? а сколько линков и внешних клиентов? нет однозначных решений, типа, вот это таблетка и она работает везде. каждый случай, особенно на хайлоад уникален. где-то можно и процедурами или функциями, а где-то параметризированными запросами, а где-то вьюхами, а в каком то случае и без динамики не обойтись.
Вы наверняка рассматривали вероятность использования кодогена, чтоб не зависеть от магических ключей мапы и не иметь оверхеда на запуск, когда надо добро все прочитать да распарсить. Почему вместо генерации обертки над sql в виде как раз go кода отказались? Ведь такой подход выглядел бы логичнее, строже, и гибче?
Да, вы угадали. И долго не мог выбрать - как лучше. Но хотелось иметь максимально простой вариант - добавил в sql-файл запрос и всё. Я периодически забываю запускать кодогенерацию ). Решающим аргументо было: Тесты на все запросы в любом случае должны быть. И ошибки в именах запросов они в любом случае должны выявлять. Если так - кодогенерация становится еще одной дополнительной и не обязательной подстраховкой. Но в целом, я считаю что плюсы и минусы кодогенерации равны 50/50. И это уже личный выбор, вкусовщина.
Чтоб не забывать запускать кодоген, часто достаточно не хранить сгенерированные файлы под гитом, и писать тесты ;)
В общем и целом, выбрали бы вы дорожку с генерацией кода - ваш покорный (и, уверен, много кто еще) прониклись и глубже бы стали смотреть ваш проект. И сравнивать его с sqlc, разумеется :D
Но магия ключей мап, и оверхед на запуск как минимум для меня выглядят как редфлаги, заставляющие пройти мимо :(
Удачи с проектом, не бойтесь экспериментировать!
Оверхед на запуск?! Лишние сколько-то миллисекунд при запуске - редфлаг?... Ну даже не знаю, что сказать )
Предлагал сообществу чутка ранее вариант с кодогенерацией. Сравнение с sqlc действительно неизбежно :)
sqlc уже парсит *.sql и генерит по нему код
плюсую, очень классная вещь
Ну, не всем нравится дополнительная кодогенерация )
а что, она есть просит? после описания sql-запроса в файле вы либо снова ручками пишете код, использующий этот запрос, либо выполняете 1-секундную команду, получая единообразный и типизированный код на все запросы.
Ладно бы ваш подход чем-то принципиальным отличался от sqlc. Так вы просто делаете то же самое, только медленнее и хуже.
Ну насчет медленнее и хуже - приведите аргументы, прежде чем клеить ярлыки ). Почему я не хочу использовать кодогенерацию, я уже писал. Я ж не навязываю своё мнение.
Ну насчет медленнее и хуже - приведите аргументы
ручками и неединообразно
Почему я не хочу использовать кодогенерацию
Я периодически забываю запускать кодогенерацию
ну вы же кодогенерацию запускаете, чтобы сгенеренный код использовать? как у вас тут выйдет забыть сгенерить?
Мне кажется это бесконечный спор ).
Самое главное - sqlc же и модели генерирует? Я предпочитаю использовать по возможности одну модель для одной сущности для всех слоёв. По этой причине генерация моделей мне никак не подойдет.
Второстепенное - Когда я пишу метод, вызывающий запрос к БД ручками, всё под моим контролем. Я могу использовать все 100% возможностей, которые дает мне драйвер СУБД так, как мне нравится.
Кодогенерация отнимает у меня эту свободу.
JFI: Я не утверждаю, что использование sqlc - плохо. Отнюдь. Но подходы могут быть разные. И у каждого свои плюсы и минусы. Каждый выбирает то, что ему больше по душе. Идеальных решений нет.
Вопрос, в Go до сих пор нет ORM? Хотя бы аналог Dapper для C#, ну или что то более серьёзное типа EF?
Есть ещё sqlbuilder. Умеет в разные диалекты. Таким образом, вместо того чтобы собирать SQL из разных фрагментов, собирается все с помощью Go-кода. Скорее всего, довольно сложные запросы он собрать не поможет, и понадобится решение наподобие вашего.
Есть еще huma - тоже билдер. Мне подобный сахар не нравится по нескольким причинам. Напишу в отдельном комментарии. Запросы должны быть статичными если только это возможно. А это возможно и целесообразно в 99,999% случаев.
Вот: https://habr.com/ru/articles/977046/#comment_29259296
Недостатки динамических запросов
Смешивается в одну кучу приложения GO и код SQL. Портировать будет труднее.
Уязвимость к SQL-инъекциям — главная и самая опасная. Если в запрос напрямую вставляются пользовательские данные без правильной обработки, злоумышленник может внедрить вредоносный код. Например, классика вроде ' OR '1'='1 может обойти аутентификацию или вытащить все данные из таблицы. Это приводит к утечкам данных, удалению БД или даже выполнению команд на сервере.
Проблемы с производительностью — Каждый динамический запрос часто компилируется заново, что не позволяет эффективно кэшировать планы выполнения. В результате — повышенная нагрузка на CPU, медленные запросы и возможные проблемы с параметр-сниффингом (неоптимальные планы для разных значений параметров).
Сложность отладки и поддержки — Код становится менее читаемым, труднее тестировать и логировать. Ошибки в формировании строки могут привести к синтаксическим ошибкам или неожиданному поведению.
Какая связь динамически составляемого запроса и SQL-инъекции? Что мешает в динамически формируемом запросе использовать подготовленные запросы и SQL параметры?
«Каждый динамический запрос часто компилируется заново» — но тем не менее, их обычно довольно ограниченное количество, а популярных в рамках приложения комбинаций еще меньше. И происходит эта «компиляция» не каждый раз, а только один при первом выполнении. Так что это всего несколько штук на запущенный инстанс базы, не так это и страшно)
Так sqlbuilder и делает статические запросы, если вы только сами не напишете цепочку сбора этого самого запроса динамической.
Билдер накидывает абстракцию над SQL и выражает ее в виде Go методов. И все рядовые запросы выглядят читаемо. Там все те же select, where и equals. И подстановки параметров там безопасные(через ?). Захотели переехать на другую бд- меняете диалект в настройках билдера.
К тому же, он умеет привязываться к гошным структурам через db тэги. Это выглядит ORM, но ограничивается генерацией запроса.
а зачем тогда билдер? Почему не писать запросы руками? См https://habr.com/ru/articles/977046/comments/#comment_29260880
Потому что для рядовых запросов он освобождает от необходимости изобретать велосипед. В запросы надо передавать параметры, да ещё и в правильном порядке. Билдер следит за этим сам, от меня ему нужен только тип структуры. Плюс в коде это выглядит понятно и читаемо.
Если нужно написать сложный запрос, то я выберу чистый SQL. У билдера есть свой предел, где он становится неудобным. Но такие запросы не в каждом сервисе существуют.
Я за то чтобы использовать инструмент там, где он хорошо подходит.
Согласен с вами. Но я за то, чтобы использовать один инструмент везде и единообразно. Зачем тащить билдер, если простые запросы написать просто? ИМХО - он лишний. Или чистый SQL или - билдер. Не вижу никакой пользы от смешивания - только лишняя зависимость, лишний код. Это - только моё личное мнение )
Чистый SQL дает полную свободу и весь диапазон функциональных возможностей.
Признаюсь, что мне подсознательно кажется, что за динамику топят те, кто не любит конструировать сложные SQL запросы, процедуры. Ну или не умеет и не хочет заморачиваться. Кажется так, потому что несколько лет назад был таким же. Но теперь я считаю, что лучше не строить над SQL сладкие надстройки. В любом случае, когда ты пишешь найтивный SQL-код - у тебя больше гибкости и контроля. Юзать билдер - это всё равно, что на Python писать go-код с помощью фреймворка. Всё равно что разговаривать с иностранцем через Google Translate. Ну или гланду удалять через одно место. Изврат )
В самом Postgres, кстати можно создавать динамические запросы в процедурах. Да и вообще, в Postgres сейчас столько всего напихали - трудно найти то, что он не умеет делать. Невозможно. И делает это он по-любому оптимальнее обычно. Как-то так...
Ну а про ORM-ы лучше вообще промолчу ))))
ORM позволяет стандартизировать интерфейс доступа к данным, пожертвовав гибкостью конкретной СУБД, в пользу унификации. Такой подход влияет на архитектуру кода.
Билдеры и ОРМ-ы кажется удобными на старте, пока не появились специфические задачи/требования. А потом - это будет боль, мне кажется. Ну и бойлерплейт конечно.
Боль начинается из за отказа от унификации и острого желания найти уже готовую фичу в конкретной специфичной БД и пользоватся ею независимо от последствий в будущем - привязал проект к конкретной СУБД и точка, зато все быстро сделал - менеджеры таких любят.
Кстати, ни в коем случае не берите на работу тех, кто сможет объяснить менеджерам почему теперь мы не можем отказатся от этой технологии и использовать аналогичное open source решение :)
Ну если менять СУБД каждый год... То конечно ). Переписать имплементацию слоя репозитория раз в 10 лет - не криминально поверьте. Особенно если запросы хранятся отдельно от кода. Но мучится все эти 10 лет с минусами, вызванными абстрагированием от СУБД - тоже такое себе. Впрочем - зависит всё от контекста. И к слову, однотипные СУБД принципиально не отличаются по функционалу. 99% всего что можно делать в MS SQL сделаете и в Postgres легко. Я про SQL код.
Конечно нет, коллация в MSSQL по умолчанию регистронезависимая в отличии от Postgres и далее начиная с пагинации, регистронезависимые ILIKE и до оконных функций - много отличий, даже в типах данных дат, я молчу про json функции.
Я не про нюансы выполнения функций, я про наличие/отсутствие аналогичных функций. И никто не заставляет использовать какие-то уникальные фичи конкретной СУБД. Можно писать запросы, не используя уникальную специфику.
ОРМ априори не подпускает вас к низкому уровню. И если идеологически это хорошо, то практически, если у вас хайлоад и вы ходите максимально оптимизировать работу с БД - он вам не позволит этого сделать. А как показывает практика, рано или поздно такая потребность возникает. И потом люди локти кусают.
Highload и реляционная СУБД плохо совместимы, для highload другие БД обычно используются, минимальное требование - горизонтальная масштибируемость на запись, с этим у реляционных БД все плохо, на чтение ещё можно репликами разрулить, а вот если высокая нагрузка на запись, то приходиться танцевать с бубнами и менять структуру БД чтобы можно было сегментировать хранилище по разным дискам, пока не упрёшся в лимит по CPU, а уж по разным узлам разносить логически одно хранилище, это отдельная боль.
Позволю себе еще одно объяснение по поводу моего отношения к ORM и кодогенерации. Чтоб не плодить холивары в нескольких местах:
Если проект достаточно сложный и долгоживущий, развивающийся, рано или поздно но возникает необходимость в ручном написании и вызове каких-то запросов. С большой вероятностью возникает. Возникает потому, что нужно реализовать какую-то фичу или минимизировать время и количество обращений к БД - мало ли. И вот тут приходится обходить ORM и кодогенерацию.
Сочетать же несколько инструментов одновременно для работы с БД конечно можно, но мне не хочется. Мне импонирует строгость и единообразие - меньше когнитивная нагрузка при работе с кодом проекта.
Я не утверждаю что мой подход единственно верный. Но думаю, он имеет право на жизнь наравне с другими ).
Возможно некоторые выше уже писали, не было возможности прочесть все комментарии.
Есть отличная Чистая архитектура Роберта Мартина (Clean Architecture by Unce Bob)
Я согласен что в вашем случае уже был готовый проект и писать с нуля не имело смысла, но в указанной выше архитектуре вам бы просто пришлось написать +1 реализацию интерфейса, что вероятно, не составило бы труда
Не понимаю какие проблемы могут быть с запросами в коде, если они не выходят за пределы репозитория.
SQLSet — отделяем GO код от SQL-запросов