воскресенье, 23 марта 2014 г.

[prog.thoughts] Вспомнил про старую идею о вынесении текстов SQL-запросов в конфиги

Есть такая штука в разработке ПО, как technical debt. Т.е. вещи, которые должны были быть сделаны, но по каким-то причинам не были. Иногда вы утешаете себя тем, что когда-нибудь в будущем, когда руки дойдут, вы таки расплатитесь по своим старым долгам, вернетесь к коряво написанному коду и, не смотря ни на что, все-таки перепишете его как надо. Излишне говорить, что это напрасные надежды, в большинстве случаев. Нет ничего более долговременного, чем временное. Чудеса, т.е. исключения, конечно, случаются. Но, на моей дырявой памяти это именно чудесные исключения, а не устоявшаяся практика...

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

Суть идеи вот в чем. Мы для работы с РСУБД из C++ приложений (написанных с использованием SObjectizer, понятное дело) применяем отличную header-only библиотеку OTL. Ее особенностью является наличие т.н. потоков (реализуемых классом otl_stream). Поток связан с SQL-выражением. Если в SQL-выражение нужно подставлять параметры, то эти параметры специальным образом обозначаются в тексте SQL-запроса:

otl_stream s(
   1,
   "INSERT INTO TMP_IDS( id ) "
   "SELECT "
      "message_id "
   "FROM DRS "
   "WHERE "
      "received_at < :1<TIMESTAMP> AND "
      "rownum <= :2<UNSIGNED> ",
   db_connect );

Выделенные жирным фрагменты ":1<TIMESTAMP>" и ":2<UNSIGNED>" -- это и есть параметры для SQL-запроса. Для них перед выполнением запроса нужно задать значения. Значения задаются в стиле C++ных потоков вывода: через operator<<():

s << make_otl_datetime( time_border ) << max_count;

Вот такой примитивный, но вполне себе успешно работающий подход мы уже очень давно применяем в своих C++ проектах. Про различные ORM-инструменты, скрывающие от разработчика детали работы с БД, мы в курсе. Но, во-первых, C++ не Java, не Ruby и не C#. Как-то в C++ ORM-ов не густо. И, во-вторых, у многих ORM-ов, насколько я знаю, не все так гладко, когда нужно выжимать максимальную производительность из БД. Тут быстро выясняется, что SQL -- это не столько стандарт, сколько соглашение. А у каждой серьезной РСУБД есть свой вариант SQL-я и, если тебе за секунду нужно обработать тысячи строк в разных таблицах, то без задействования специфических для СУБД SQL-выражений далеко не уедешь. Вот и оказывается, что чем примитивнее инструмент, тем проще с его помощью выжимать нужную производительность.

При разработке и отладке приложений мы, естественно, профилируем свои SQL-запросы. Добавляем в них различные хинты для СУБД. Иногда существенно перерабатываем SQL-запросы, переходя от простых запросов над целевой таблицей, к более сложным комбинациям с временными таблицами, накапливающими промежуточные значения...

Однако, практика показывает, что как бы мы не старались на этапе разработки, в эксплуатации, на production-железе под реальной нагрузкой, когда разработчики не могут просто так взять и дотянуться до СУБД, возникают различные странные ситуации. Скажем, несколько дней программный компонент может работать с одной скоростью, а затем на несколько часов его производительность вдруг упадет в разы и проблема будет где-то в БД. В некоторых случаях служба эксплуатации обнаруживала проблемы и выдавала свои рекомендации. Иногда это были дополнительные хинты для SQL-запроса, актуальные для конкретной инсталляции СУБД на конкретном железе.

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

Насколько я помню, главными препятствиями к ее реализации были две вещи:

  • во-первых, текст SQL-запроса в коде далеко не всегда задавался в виде константного строкового литерала. Иногда он собирался из нескольких кусков. И названия таблиц, столбцов, индексов, а так же состав столбцов или даже содержимое условных выражений в SQL-запросе вычислялись прямо в runtime в зависимости от ряда условий. В принципе, подобную проблему можно было бы решить применив какой-нибудь template engine (т.е. в конфиге бы хранился не готовый текст SQL-запроса, а шаблон для его генерации). Но это получалась бы уже не совсем тривиальная штука, отдавать которую в руки эксплуатации было чревато возникновением неожиданных проблем, с которыми нам бы самим потом пришлось бы разбираться :)
  • во-вторых, параметры в otl_stream должны получать свои значения в том порядке, в котором они заданы в тексте SQL-запроса. Если текст SQL-выражения хранится в конфиге, то оператор ни в коем случае не может менять порядок следования параметров в нем. А это, временами, нужно было делать. Чтобы разрешить такие вещи, нужно было бы придумать механизм поддержки именованных параметров для otl_stream-а. Т.е., чтобы в конфиге оператор мог записать "where rownum <= :MaxLines<UNSIGNED> and received_at < :TimeBordre<TIMESTAMP>", а в программе бы параметры со своими именами шли в другом порядке:

    s << named_parameter( "TimeBorder", make_otl_datetime( time_border ) )
       << named_parameter( "MaxLines", max_count );

    А вот на это уже не нашлось времени.

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

Отправить комментарий