Как стать автором
Обновить

Меньше кода, больше результата: применяем sqlc для работы с БД

Уровень сложностиСредний
Время на прочтение17 мин
Количество просмотров6.6K
Всего голосов 14: ↑14 и ↓0+17
Комментарии20

Комментарии 20

Пример: есть SQL‑запросы в файле query.sql и схема базы данных в schema.sql. После выполнения команды sqlc generate генерируются три файла

Как я понимаю, оба файла содержат SQL-запросы - первый те, которые надо выполнить, второй те, которые создают саму БД "с нуля". Соответственно возникает вопрос различия диалектов и интерпретации СУБД-специфичных конструкций в схеме, сложности запросов (иной раз такое напишешь...), использования user-friendly фич в запросах, использование запросов, не имеющих описания структуры результата в схеме (CALL stored_procedure) и пр. Как решается эта проблема? Или поддерживается только весьма ограниченный список допустимого?

  • Подход с использованием SELECT * делает приложение хрупким: добавление новых колонок в таблицу может вызвать ошибки маппинга, такие как «expected 6 destination arguments in Scan, not 3».

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

Использование нормализующих представлений легко снимает обе проблемы.

  • Ручная сборка SQL‑запросов с учётом различных условий требует значительных усилий и повышает риск ошибок.

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

SQLC поддерживает PostgreSQL, MySQL и SQLite, используя их нативные парсеры, но не пытается быть универсальным инструментом. Он хорошо работает с простыми и средними запросами, но сложные конструкции могут потребовать ручной доработки. Например, вызовы хранимых процедур не поддерживают автоматическое определение схемы результата, а динамические SQL-запросы вообще невозможны, кроме того, что я описал в статье. Для специфичных функций (unnest), JSON_TABLE) SQLC может неправильно определить возвращаемый тип, поэтому в таких случаях нужно либо использовать sqlc.embed, либо явно кастовать данные. В целом, инструмент ориентирован на статический SQL и ограничен его возможностями.

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

Настоящая мощь SQL проявляется не в запросах, а в использовании хранимых процедур. Но это для сложных задач, где обработку данных и транзакции можно перенести в хранимые процедуры. А всякие костыли вместо чистого SQL это для тех кто плохо знает этот мощный и производительный язык. А насчет sqlite , где нет хранимых процедур, можно использовать дополнительную таблицу, где хранить запросы с абстрактными параметрами, загружать запрос из таблицы, подставлять параметры и выполнять. Таблицу с запросами можно сразу загрузить при старте программы. Такая методика уже отрабатывалась мною, и она работает в задаче , где нужно было поддерживать SQL server и sqlite в качестве сетевого и локального варианта приложения. Вызов ханимых процедур там сделан через запрос с exec. При разработке я старался чтобы код хранимок не отличался от кода запросов sqlite. Они получились практически идентичными. Была только некоторая несовместимость в использовании СТЕ в sqlite, но ее тоже удалось пофиксить.

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

Если инструмент старый, но всё ещё мощный, и всё ещё не потерявший своей актуальности - так это, получается, проверенный временем.

А на деле.. масштабировать - вот не так уж и сложно, да и инструментарий вот не так уж и неудобен. Отговорки всё это, чтобы... кому - язык не учить, кому - квалифицированного программиста нанимать. Уж серверы БД поди пишет народ ну никак не глупее тех, кому надо (вот реально надо, а не для выпендрёжу - а таких один на миллион), и ведь у них-то, у двух умных, как раз масштабируется. Инструментов больше да удобнее пишут для туда, где больше пользователей, а SQL-программистов куда как меньше прочих - в том числе и по причине, упомянутой выше.

Много лет плотно работал с SQL Server - и да, было достаточно удобно. А уж как дебаггер выручал - не передать. Но и признаю, что критика такого подхода появилась не на пустом месте. Версионность, консистентность набора хранимок - регулярно с этим возникали проблемы. Особенно когда работа идет с множеством баз данных.

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

И да, многие раработчики не знают SQL - и совершенно зря. ORM и query builder зачастую могут очень неожиданным образом отработать, особенно на хоть сколько-нибудь сложных запросах. Не говоря уже о навыках проектирования баз данных.

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

Вы использовали хранимку как обычный код. Тогда как к ним, ну или по крайней мере к значительной части их, нужно бы относиться как, например, к встроенным функциям языка. Написана, отлажена, оттестирована, отдокументирована, и теперь используется. ВСЁ! Прибито гвоздями к служебной схеме, и фактически уже не подлежит изменению. А повод к корректировке - либо изменение бизнес-требований, либо законодательства. Ну либо найденный баг. То есть применительно к отдельно взятой процедуре событие достаточно редкое.

Написана, отлажена, оттестирована, отдокументирована, и теперь используется. ВСЁ! Прибито гвоздями к служебной схеме, и фактически уже не подлежит изменению. А повод к корректировке - либо изменение бизнес-требований, либо законодательства. То есть применительно к отдельно взятой процедуре событие достаточно редкое.

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

Усуглублялось тем, что у разных клиентов (в разных регионах) могли быть разные требования к отдельным хранимкам - и эти отличия от стандартных надо было вовремя учитывать, когда с БД работаешь или отправляешь обновление.

У меня одно приложение на 36 Таблиц, 120 встроенных процедур, 6 функций работает полтора года, всего 75 пользователей. За это время ниаких багов не было выявлено. Добавлял кое что по функционалу, но немного в sql. Для информирования и общения с пользователями даже свой мессенджер реализовал в бд, ну и возможность удаленно убить приложение на клиенте , если нужно. В общем очень удобно оповещать всех об изменениях или фичах, которые добавились. Приложение запускается через ярлык на рабочем столе. Права доступа к функциям, перемещение и работа с документом все реализовано в БД. Для выходных форм использую шаблоны excel и ole automation. Даже такие мелочи как автоматическое переключение на русский регистр при вводе данных реализовано.

Ну не всем же нужно масштабировать задачи. Все зависит от самих задач. У меня задачи сложные , но не масштабные. Поэтому в коде клиента можно долго долго ковыряться. Насчет версионности. Никто не запрещает использовать тот же qit. Для клиентского кода я его использую, для SQL. кода мне достаточно своего триггера бд, который весь dml код автоматом пишет в отдельную таблицу в отдельной бд. Просто указываешь в комментах к процедуре что изменил. Всегда можно откатить без проблем. Команды у меня нет, максимум еще один и все. Я не говорю о тестировании еще. Поэтому протестированный код процедур практически гарантирует отсутствие проблем с клиентом, так как интерфейсные зависимости в любом случае проще править чем бизнес логику. Ну и приемы разные тоже использую. Допустим надо добавить поле или несколько полей в таблицу. Если таблица завязана на множество других процедур, то просто добавляю связанную с ней таблицу и пишу новую процедуру. Параметры всегда определяю как null. План запроса оптимизирую с учетом индексов или операторов. И ничего не ломается в приложении, все работают по прежнему. Добавляю поля если надо на клиенте, корректирую форму, тестирую и обновляю клиента. Все. Никакого web доступа не нужно. Одно подразделение уехало в другой конец города, через VPN все работает. Потому что все оптимизировано в sql. Ну а в компании есть и приложения на PHP, все тормозит и на старых компах не грузится вообще. Да и не очень удобно с интерфейсом. Старый подход работает лучше.

При всей своей нелюбви к PHP должен сказать, что хранимками и PHP решаются разные задачи. И я сильно сомневаюсь, что если весь функционал засунуть в хранимки - они будут лучше работать. Как минимум потому, что PHP и SQL сильно разные проблемы решают.

Какие-то проблемы решает sql вы даже не представляете похоже. Я бы сказал что 90 %. бизнес логики. Остальное просто интерфейс. Это применительно к десктопным приложениям. И PHP здесь слабое, требующее ресурсов и тормознутое ненужное звено, которое еще и требует инфраструктуры для своей работы, как и любой web интерфейс. В этом то и беда, что все "забыли" как просто, эффективно и быстро решать сложные задачи, которые не требуют web интерфейса, что актуально в большинстве мелких и средних компаний и требуется для производства и т п. Нанимают программиста, а он ничего кроме рнр не знает и владеет sql на уровне select и insert. Пишет что-то, потом это все начинает жутко тормозить и т д и т п. Или что ещё хуже получает легаси от уволившихся и все проблемы этого php впридачу. Или еще пример как человек вместо базы данных для одного приложения, работающего с приборами использовал хмл файл , все бы ничего, но в процессе разработки приборов программисты встроенного ПО должны были этот файл редактировать, а файл мог включать до 200 cтраниц. И таких файлов до 50. Это просто был ад редактировать такие файлы. Причина этого была в том что приложение локальное должно было быть. Применить sqlite или что-то подобное было невдомек. Вместо PHP. Использовался c#. Пришлось мне все переделывать, так ка чел отправился в другую компанию, применить свой богатый опыт.

Я очень хорошо представляю, что такое SQL и для чего он нужен. Как и касательно PHP. Впрочем, в "предназначении" ни там, ни там не значится "бизнес-логика".

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

Но мы сейчас сравниваем "непонятно что" на PHP и другое "непонятно что" на SQL. Судя по описанию, на SQL получилось лучше, чем на PHP. Но вина ли в этом PHP? Не думаю. Как и не думаю, что заслуга SQL есть в том, что получилось лучше. А вот человеческий фактор - уверен что есть.

Могу вспомнить про базы данных, где "нормальная форма" было ругательством, наверное. Или где разрабы не знали ничего, кроме 1й нормальной формы, ведь "не зря же она первая, изучать другие не нужно".

Так что все тут очень относительно.

P.S. а криминал с бизнес-логикой в хранимках скорее вижу в том, что таблицы, ограничения, триггеры, хранимки - очень плотно между собой переплетаются. И работа такого софта становится крайне непрозрачной (помимо БД нужен еще и клиент - не SQL-запросами же оперируют пользователи). И что еще хуже - в отличие от классического кода в IDE, навигация по объектам сильно менее удобна, что затрудняет работу разработчика.

P.P.S. ну а повсеместный веб - это реалии современности. За это можно ругать, но, в первую очередь, это удобно. Кросс-платформенный интерфейс "из коробки" и простота обновления. Нет необходимости обновлять клиентов вручную, нет 100500 ошибок автоматического обновления и прочее-прочее-прочее. Ушел очень большой пласт проблем в общем. Конечно, добавился новый пласт проблем с зоопарком браузеров и настроек системы. Но в целом стало сильно проще.

Я триггеры практически не использую сознательно. Очень редко в единичных случаях. Насчет обновления приложения у меня так- ехе лежит в сетевой папке, у пользователя ярлык на рабочем столе . если надо обновить, то файл переименовываю и записываю новый exe . в одном приложении даже сделал меssenger в бд через который всем посылаю сообщение что программа обновлена и ее надо перезапустить. Вот и все обновление. Если программа запущена, то сообщение открывается в модальном окне и его невозможно пропустить не прочитав. Все приложения мои логируются и все что делал пользователь я вижу в этой же программе, в том числе прочитал ли он сообщение. Это возможно потому что каждое приложение имеет такую архитектуру, где предусмотрена таблица коннектов, таблица логов, таблица справочника операций. Это кстати упрощает разработку бизнес логики в БД. Общая система мониторинга всех приложений позволяет видеть все запущенные приложения и пользователей и их логи операций. Все ходы записаны, легко разобраться кто когда и что делал. В одном приложении у меня 75 пользователей из разных отделов компании, через которые проходит документ и если нужны разборки то логи все показывают.

Печальный опыт показывает, что такие инструменты хорошо работают на примитивных примерах и затыкают пробелы в инженерной культуре. С ростом приложения, невозможность непосредственной отладки хоть бы и в рантайме осложняет поддержку, лишний шаг в сборке на кодогенерацию отдаляет изменения от выполнения, ну и к паре языков общего назначения изучать специфичный DSL перемешанный с SQL, без гарантий стандартизированной поддержки - ну такая себе перспектива. Выстраивание культуры 3-этапных развертываний, определение зон изменений и автоматизация развертывания включая откаты, стоит затрачиваемых усилий, если на продукт долгоиграющие планы.

А GORM уже адаптировался к дженерикам и позволяет мапить шаблонные и шаблонизированные типы в базу данных? Пару лет назад его разрывало в клочья при компиляции при таком подходе.

Я не понял нахрена все это. Особенно когда сегодня почти любая модель жпт тебе и запрос напишет, и дтошки создаст, и тестами покроет и т.д.

Зачем все это?

Да, жпт может нагенерить код, но sqlc решает другую задачу — он гарантирует, что мои SQL-запросы точно соответствуют схеме БД, даёт мне строгую типизацию и убирает рутинный набор кода.

GPT напишет запрос, но не проверит его на ошибки, не встроится в CI/CD и не обеспечит безопасность типов. sqlc делает это автоматически и без магии.

так тесты же…какой у вас % покрытия в рабочих проектах?

Так, а какой тезис мы обсуждаем? Если я правильно понял, идея в том, что при высоком тестовом покрытии можно вообще не использовать специализированные инструменты для кодогенерации и просто писать код через GPT?

Если да, то можно ли тогда отказаться, например, от protoc и просто генерировать всё через GPT? Или там почему-то подход с тестами уже не считается достаточным?

Просто интересно понять, где проходит грань между «давайте заменим специализированный инструмент на AI» и «ну тут всё же нужен формальный генератор».

Затем что chatgpt выдает как бог на душу положит. А sqlc даёт абсолютно повторяемый детерминированный результат.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий