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


Проблема состоит в противоречии:


  • С точки зрения эффективности работы с данными желательно максимум бизнес-логики реализовывать в хранимых процедурах.
  • С точки зрения эффективности разработки ПО желательно, чтобы части одной программы находились в одном месте. Хранение кода работы с хранилищем прямо в хранилище создаёт много трудностей.

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


Как правило, развитие проекта происходит неравномерно и итеративно: каждое новое изменение затрагивает лишь несколько составляющих проекта.


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


Когда сроки поджимают (т. е. практически всегда), разработчик стоит на распутье:


  • Или отказаться от рефакторинга процедуры, поскольку он вызывает проблемы в другом подпроекте.
  • Или заняться рефакторингом и смежных проектов тоже.

Способы выбора пути варьируются от проекта к проекту.


Версионность в названии хранимых процедур


Многие компании создают внутреннюю полиси (зачастую подкреплённую скриптами CI), которая требует называть хранимые процедуры по определённому правилу.
Например, указанную функцию user.profile мы переименовываем в user.profile_0001, где 0001 — номер её версии. Программист, которому потребовался рефакторинг этой функции, пишет новую — user.profile_0002, а user.profile_0001 продолжает существовать в системе до тех пор, пока весь зависимый от неё код не перепишут.


Достоинства и недостатки такого подхода очевидны.


Полный отказ от использования хранимых процедур


Я видел многие SQL- и не SQL-проекты, сознательно отказывающиеся от хранимых процедур («кроме тех случаев, когда совсем уж без них нельзя»).
Разрешение противоречия в пользу разработчиков.


Перенос максимума бизнес-логики в хранимые процедуры


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


Сессионное хранилище


Многие БД имеют сессионное хранилище, но немногие предоставляют возможность хранить в нём функции/процедуры.


БД Tarantool имеет сессионное хранилище, позволяющее хранить в нём в том числе и функции.


Что такое сессионное хранилище? Это хранилище, которое будет полностью очищено сразу после того, как клиент отсоединится от базы данных.


Как можно использовать сессионное хранилище для решения поставленных проблем? Алгоритм приблизительно следующий:


  1. Клиент коннектится к БД.
  2. Клиент заполняет сессионное хранилище ссылками на хранимые процедуры, которые будет использовать.
  3. Работает, делая вызовы к созданным процедурам.
  4. После дисконнекта БД сама очищает хранилище.

Плюсы


  1. Код хранимых процедур можно держать в том же Git-дереве, что и код, их использующий.
  2. При необходимости обращения к одним и тем же процедурам в разных проектах можно применять Git-сабмодули.
  3. Исправление/рефакторинг хранимки и связанного с ней кода упрощается: правки производятся в одном месте.

Минусы


  1. Компиляция хранимок происходит при каждом коннекте клиента. Если число присоединённых клиентов огромно (тысячи) — может получаться значительный оверхед по использованию памяти.
  2. Данный подход, увы, не решает вопрос с хранимками, продолжающими работать и после дисконнекта клиента. Работа с ними — тема для отдельной статьи.

Пример


Некоторые драйверы к Tarantool позволяют сразу после коннекта автоматически выполнять набор Lua-файлов из заданной директории.
Например, используя Perl-коннектор DR::Tnt, вы можете указать опцию lua_dir.


Размещаем в директории проекта каталог lua, в который можем положить несколько файлов. Например user.lua из этого каталога будет выглядеть так:


function box.session.storage.user.profile(id)
    local user = box.space.users:get{id}
    local profile = box.space.profiles:get{id}
    -- do something
    return { user, profile }
end

function box.session.storage.user.add(o)
    return box.space.users:insert{uuid(), o.name, o.surname }
end

function box.session.storage.user.get(id)
    return box.space.users:get{id}
end

В приложении коннектимся и работаем, используя процедуры box.session.storage:


my $tnt = tarantool
    host        => $host,
    port        => $port,
    user        => $user,
    password    => $password,
    lua_dir     => "lua"
;

# Создание пользователя с именем «Вася»
my $new_user = $tnt->call_lua('box.session.storage.user.add', { name => 'Вася' });

# Получение пользователя
my $user = $tnt->call_lua('box.session.storage.user.get', 123);

Относительно длинный неймспейс box.session.storage можно сократить до минимального размера, поместив в глобальном справочнике его алиас:


_G.ss = box.session.storage

Размышления


В «больших» БД, в PostgreSQL, например, существует сессионное хранилище (аналог) для данных CREATE TEMPORARY TABLE. Было бы замечательно, если бы там появилась возможность создавать и временные функции CREATE TEMPORARY FUNCTION, которые были бы видны только приконнектившемуся клиенту и удалялись бы после отсоединения.


Ссылки


  1. Эта статья на GitHub
  2. Проект Tarantool
  3. box.session.storage в документации

Комментарии (8)


  1. Q2W
    04.04.2018 10:02

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

    Так же БД должна поддерживать выполнение операций на других инстансах (типа добавили строку в таблицу на одном сервере БД — надо увеличить счётчик в таблице не другом).

    В остальном приближенность логики к данным — нужная штука, когда бизнеслогика хочет перемалывать много данных и мы теряем существенное время именно на пересылке данных между БД и приложением.

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


    1. linuxover Автор
      04.04.2018 12:27

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

      в тарантуле это можно делать.


      :)


      хранимая процедура может даже по http сходить скажем в гугл, распарсить его выдачу и положить результаты в БД :)


      1. Q2W
        04.04.2018 12:52

        в тарантуле это можно делать.

        Тарантул крут. На нём ещё сервера приложений не делают?


        1. linuxover Автор
          04.04.2018 12:53

          дык тарантул это и есть «сервер приложений с БД на борту» :)


  1. AlexLeonov
    04.04.2018 11:50

    Вы не слышали о такой технологии, как миграции?
    — храним код, изменяющий структуру и объекты БД в системе контроля версий,
    — код проходит код-ревью ровно как и любой другой,
    — при сборке версии/релиза/стенда — накатываем изменения

    В общем-то стандартная практика для всех реляционных БД, отчего бы не делать так на Тарантуле?


    1. linuxover Автор
      04.04.2018 12:23

      Вы не слышали о такой технологии, как миграции?

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


      — при сборке версии/релиза/стенда — накатываем изменения

      даже в рамках одного проекта задача миграции сложна, пример:


      1. имеется демон в проекте, периодически выполняющий запросы к БД
      2. посылка сигнала завершения этому демону означает что он остановится через некоторое время: как доделает текущую работу
      3. текущую работу он должен делывать по старым запросам (иначе возможен креш при несовместимых рефакторингах)

      и вот приходит миграция


      она делает апгрейд БД, апгрейд кодовой базы сервера (серверов), рестарт демона и тут получается противоречие:


      1. или миграция (скрипт миграции) должна стопнуть демоны перед миграцией, что приводит к простою демона на время миграции (что часто нежелательно)
      2. или миграция должна удерживать некоторое время старые и новые функции в БД (то есть версии — о чем написано в статье)

      в случае если каждое приложение работает с функциями из сессионного хранилища этой проблемы нет вообще: скрипт миграции просто апгрейдит БД и посылает сигналы перезапуска демонам.


      ну а задача апгрейда многих проектов на одной базе — миграциями вообще не разрешается.
      вернее разрешается только введением версий в имена функций (или неймспейсов) о чем и написано в статье :)


      1. AlexLeonov
        04.04.2018 12:50

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

        В таком случае сам сервис БД становится отдельным проектом.

        >> или миграция (скрипт миграции) должна стопнуть демоны перед миграцией, что приводит к простою демона на время миграции (что часто нежелательно)

        Зачем stop? Реализуйте reload в демоне.


        1. linuxover Автор
          04.04.2018 12:59

          В таком случае сам сервис БД становится отдельным проектом.

          в общем статья о проблемах реализации миграций в рамках множества проектов на одной БД.


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


          Зачем stop? Реализуйте reload в демоне.

          релоад не помогает проблеме.


          еще раз


          1. есть апгрейд БД
          2. есть интервал времени между релоадом демона и апгрейдом.

          если этот интервал отрицательный (релоад после апгрейда), то некоторое время имеем работающий демон, выполняющий старые запросы — требуются и старые и новые версии на это время.
          если интервал положительный (релоад до апгрейда), то релоад вообще не подходит: задача релоадом не решается.


          ну а если представить что демон — это вообще внешний проект и его апгрейд может быть затянут на сутки-месяцы, то станет нетрудно понять о каких проблемах речь идет в статье :)


          как-то так