Русский
Русский
English
Статистика
Реклама

Про uuid-ы, первичные ключи и базы данных

Статья посвящена альтернативным версиям Qt-драйверов для работы с базами данных. По большому счету отличий от нативных Qt-драйверов не так много, всего пара: 1) Поддержка типа UUID; 2) Работа с сущностью "Транзакция" как с самостоятельным объектом. Но эти отличия привели к существенному пересмотру кодовой реализации исходных Qt-решений и изменили подход к написанию рабочего кода.

Первичный ключ: UUID или Integer?

Впервые с идеей использовать UUID в качестве первичного ключа я познакомился в 2003 году, работая в команде дельфистов. Мы разрабатывали программу для автоматизации технологических процессов на производстве. СУБД в проекте отводилась существенная роль. На тот момент это была FireBird версии 1.5. По мере усложнения проекта появились трудности с использованием целочисленных идентификаторов в качестве первичных ключей. Опишу пару сложностей:

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

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

Использование UUID-ов в качестве первичных ключей сводило к минимуму архитектурную проблему, и полностью решало программную. UUID-ключ генерировался перед началом вставки записи на стороне программы, а не в недрах сервера БД, таким образом дополнительный SELECT-запрос стал не нужен, и требование единой транзакции утратило актуальность. FireBird версии 1.5 не имел нативной поддержки UUID-ов, поэтому использовались строковые поля длинной в 32 символа (дефисы из UUID-ов удалялись). Факт использования строковых полей в качестве первичных ключей нисколько не смущал, нам не терпелось опробовать новый подход при работе с данными.

У UUID-ов есть свои минусы: 1) Существенный объем; 2) Более низкая скорость работы по сравнению с целочисленными идентификаторами. В рамках проекта достоинства оказались более значимы, чем указанные недостатки. В целом, опыт оказался положительным, поэтому в последующих решениях при создании реляционных связей предпочтение отдавалось именно UUID-ам.

Примечание: Более подробный анализ UUID vs Integer для СУБД MS SQL можно посмотреть в статье "Первичный ключ GUID или автоинкремент?"

Первый драйвер для FireBird

В 2012 году мне снова довелось поработать с FireBird. Нужно было создать небольшую программу по анализу данных. Разработка велась с использованием QtFramework. Примерно в это же время у FireBird вышла версия 2.5 с нативной поддержкой UUID-ов. Я подумал: "Почему бы не добавить в Qt-драйвер для FireBird поддержку типа QUuid?" Так появилась первая версия Qt-драйвера с поддержкой UUID-ов. Этот вариант не сильно отличался от оригинальной версии драйвера и, в основном, был ориентирован на использование в однопоточных приложениях.

Появление сущности "Транзакция"

Следующая модификация Qt-драйвера для FireBird произошла в конце 2018 года. Наша фирма взялась за разработку проекта по анализу данных большого объема. Для фирмы выросшей из стартап-а эта работа была очень важна, как с финансовой, так и с репутационной точек зрения. Сроки исполнения были весьма жесткие. В качестве СУБД была выбрана FireBird, несмотря на определенные сомнения в ее пригодности. Хорошим вариантом могла бы стать PostgreSQL, но у нашей команды на тот момент отсутствовал опыт эксплуатации данной СУБД.

Архитектура программы предполагала подключение к базе данных из разных потоков. Нативный Qt-FireBird драйвер не очень подходил для такого режима работы. К тому же, у концепции Qt-драйверов, на мой взгляд, есть один существенный недостаток: управление транзакциями (точнее одной транзакцией) происходит на уровне объекта подключения к базе данных (сущность "Транзакция" инкапсулирована внутри объекта Driver). То есть при работе с нативным Qt-драйвером в один момент времени можно оперировать только одной транзакцией. Почему так сделано, в принципе, понятно: достаточно много популярных СУБД работают по этой схеме (одно подключение - одна транзакция). В качестве примера можно назвать Oracle, PostgreSQL, MS SQL при работе через ODBC. Но FireBird не такой, его API позволяет оперировать сразу несколькими транзакциями в контексте одного подключения. Обладая этими знаниями, я начал адаптацию Qt-FireBird драйвера для работы в многопоточном приложении.

Первоначальная концепция предполагала небольшое (2-3) подключений к базе данных, при этом каждый поток приложения должен был иметь возможность работать со своей персональной транзакцией. Таким образом у одного подключения могло быть несколько активных транзакций выполняющихся в разных потоках. Для достижения этой цели код драйвера пришлось основательно переработать. К сожалению, в конце меня ждал неприятный сюрприз: при нагрузочном тестировании выяснилось, что если от одного подключения создать несколько транзакций, и выполнять их в разных потоках, то фактически, в один момент времени будет выполняться только одно sql-утверждение в рамках одной транзакции, а остальные транзакции будут находиться в состоянии ожидания. В общем, концепция параллельных транзакций с треском провалилась. Пришлось в срочном порядке переходить к решению "один поток - одно подключение". В плане параллельного выполнения sql-запросов этот вариант работал значительно бодрее.

Новая концепция не нуждалась в сущности "транзакция", как в самостоятельной единице, тем не менее, я не стал ее упразднять. Дальнейшая эксплуатация показала, что наличие объекта "транзакция" делает работу с базой данных более гибкой, дает больше инвариантов при написании кода. Например, разработчик может передать объект "Транзакция" в функцию в качестве параметра, явно говоря таким образом, что внутри нужно работать в контексте указанной транзакции. В функции можно проверить активна транзакция или нет, можно выполнить COMMIT или ROLLBACK. Для вспомогательных операций можно создавать альтернативную транзакцию, не затрагивающую основную. Таких возможностей нет у нативных Qt-драйверов.

Ниже приведен пример с тремя функциями, которые последовательно вызываю друг друга. Каждая функция запрашивает объект подключения к базе данных (Driver) у пула коннектов. Так как функции вызываются в одном потоке, они получают объект коннекта, ссылающийся на одно и тоже подключение к БД. Далее в каждой функции создается собственный независимый объект транзакции и все последующие запросы будут выполняются в его контексте.

Приведенный пример не будет работать с нативным Qt-драйвером, причина описана выше: ограничение на одно подключение и одну транзакцию

Кто-то скажет: "Зачем все так усложнять?! Вот есть же понятная концепция 'Один коннект - одна транзакция', с ней легко и просто работать!" Действительно, в большинстве случаев этого будет достаточно, но я хорошо помню, еще по команде дельфистов, как эта "простота" выходила нам боком при работе с ODBC драйверами. Поэтому считаю, что наличие инвариантов - лучше, чем их отсутствие.

void function3(int value3){    db::firebird::Driver::Ptr dbcon = fbpool().connect();    db::firebird::Transaction::Ptr transact3 = dbcon->createTransact();    QSqlQuery q3 {db::firebird::createResult(transact3)};    if (!transact3->begin())        return;            if (!q3.prepare("INSERT INTO TABLE3 (VALUE3) VALUES (:VALUE3)"))        return;            sql::bindValue(q3, ":VALUE3" , value3);        if (!q3.exec())         return;    transact3->commit();}void function2(int value2){    db::firebird::Driver::Ptr dbcon = fbpool().connect();    db::firebird::Transaction::Ptr transact2 = dbcon->createTransact();    QSqlQuery q2 {db::firebird::createResult(transact2)};    if (!transact2->begin())        return;    if (!q2.prepare("SELECT * FROM TABLE2 WHERE VALUE2 = :VALUE2"))        return;             sql::bindValue(q2, ":VALUE2 " , value2);          if (!q2.exec())         return;             while (q2.next())    {        qint32 value3;        sql::assignValue(value3, q2.record(), "VALUE3");        function3(value3);    }}void function1(){    db::firebird::Driver::Ptr dbcon = db::firebird::pool().connect();    db::firebird::Transaction::Ptr transact1 = dbcon->createTransact();    QSqlQuery q1 {db::firebird::createResult(transact1)};        if (!transact1->begin())        return;            if (!sql::exec(q1, "SELECT * FROM TABLE1"))        return;            while (q1.next())    {        QSqlRecord r = q1.record();        QUuidEx  id;        qint32   value1;        qint32   value2;        sql::assignValue(id     , r, "ID     ");        sql::assignValue(value1 , r, "VALUE1 ");        sql::assignValue(value2 , r, "VALUE2 ");        ...        function2(value2);    }}

В примере экземпляры транзакций (1-3) созданы для наглядности. В рабочем коде их можно опустить. В этом случае транзакции будут создаваться неявно внутри объекта QSqlQuery. Неявные транзакции всегда завершаются ROLLBACK-ом для SELECT-запросов и попыткой COMMIT-а для всех остальных.

Ниже показано как можно использовать одну транзакцию для трех sql-запросов. Подтвердить или откатить транзакцию можно в любой из трех функций.

void function3(db::firebird::Transaction::Ptr transact, int value3){    QSqlQuery q3 {db::firebird::createResult(transact)};    // Тут что-то делаем}void function2(db::firebird::Transaction::Ptr transact, int value2){    QSqlQuery q2 {db::firebird::createResult(transact)};    // Тут что-то делаем    function3(transact, value3);}void function1(){    db::firebird::Driver::Ptr dbcon = db::firebird::pool().connect();    db::firebird::Transaction::Ptr transact = dbcon->createTransact();    QSqlQuery q1 {db::firebird::createResult(transact)};        if (!transact->begin())        return;            while (q1.next())    {        // Тут что-то делаем        function2(transact, value2);    }    transact->commit();}

Драйвер для PostgreSQL

В начале 2020 года мы приступили к новому объемному проекту. Требования к СУБД заказчик сформулировал однозначно: PostgreSQL. В отличие от ситуации с проектом 18-го года, сейчас время на изучение матчасти было. Для PostgreSQL хотелось создать драйвер похожий по поведению и функционалу на FireBird. Первой мыслью было подглядеть решение в Qt, но взять оттуда получилось только знание о том, что этот вариант нам не подходит. Работа Qt-драйвера построена на двух командах: PREPARE и EXECUTE. Эти команды могут работать только со строковым представлением, а это означает, что любые бинарные данные придется преобразовывать к строкам. Подумав, что "это не наш путь", я принялся отсматривать существующие решения и документацию по PostgreSQL API. Из библиотеки libpqxx получилось взять концепцию уровней изоляции для транзакций, все остальное было написано с нуля. Не обошлось без "ложки дегтя". Выяснилось, что для одного подключения к БД в один момент времени можно создать только одну транзакцию. Ограничение концептуальное, существует на уровне движка СУБД. Пример с тремя функциями, описанный выше, в PostgreSQL работать не будет. Были попытки посмотреть в сторону субтранзакций, но элегантного решения найти не удалось. Нивелировать проблему получилось переработкой пула коннектов. В него было добавлено свойство singleConnect(), определяющее режим создания нового подключения к базе данных. По умолчанию пул коннектов создает в одном потоке исполнения только одно подключение к БД. Свойство singleConnect() установленное в FALSE позволяет создавать новое подключение при каждом обращении к пулу. Таким образом, ограничение в одну транзакцию на одно подключение удалось обойти. Обратной стороной этого решения является большое количество подключений к базе. Но так как пул коннектов может использовать повторно уже существующие подключения, их количество не будет расти неконтролируемо и со временем придет к равновесному состоянию. Теперь пример с тремя функциями работает.

Драйвер для MS SQL

Пару раз на переговорах с заказчиками возникал вопрос по поводу интеграции с их системами через MS SQL. Чтобы опять не оказаться в ситуации цейтнота при разработке нового драйвера, было принято решение сделать его загодя. Драйвер для MS SQL реализован с использованием ODBC. В плане работы с транзакциями он похож на PostgreSQL: одно подключение - одна транзакция. Возможно, в драйвере OLE DB для MS SQL этого ограничения нет, но ODBC инвариантов не допускает. Сейчас драйвер имеет статус демонстрационного решения, в "бою" не применялся. На текущий момент, не реализованным остался биндинг и запись NULL-значений. Полагаю, к очередному проекту эту недоработку закроем.

Чего нет в классе Driver

Описываемые здесь драйверы не повторяют один в один функционал Qt-решений. В классе оставлены следующие методы:

  • beginTransaction();

  • commitTransaction();

  • rollbackTransaction().

С введением сущности "Транзакция" они утратили актуальность и нужны исключительно для отладки и диагностирования их вызовов из Qt-компонентов.

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

  • tables();

  • record();

  • primaryIndex();

  • formatValue();

  • escapeIdentifier().

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

Еще один момент, на который хотелось бы обратить внимание: по умолчанию все драйверы работают в режиме "Forward Only". Точнее сказать это единственный режим, в котором работаю драйверы, при получении данных с сервера СУБД. Тем не менее, механизм кэширования имеется, он реализован при помощи класса SqlCachedResult. Основное назначение механизма - отображение данных в визуальных Qt-компонентах.

Новые функции

В классе Driver добавлена функция abortOperation(), она дает возможность асинхронно прерывать тяжелые sql-запросы, тем самым предотвращая "замерзание" приложения. В классе Result появилась сервисная функция size2(), она возвращает количество записей для подготовленного sql-запроса. Функция size2() расположена с защищенной секции, доступ к ней осуществляется через глобальную функцию resultSize(const QSqlQuery&). Знание о количестве записей бывает полезно при реализации механизма пагинации.

Лицензионные ограничения

Все драйверы могу использоваться только под лицензиями GPL/LGPL 2.1. Драйверы зависят от класса SqlCachedResult, который принадлежит компании Qt и распространяется под вышеозначенными лицензиями. Это налагает запрет на прямое включение кода драйверов в закрытые коммерческие проекты. Даже драйвер PostgreSQL, который фактически написан с нуля, подпадает под указанное ограничение (заражен копилефтом). Тем не менее, ситуация вполне разрешима: можно собрать драйвер как динамическую библиотеку и использовать ее в коммерческом продукте под лицензией LGPL. В этом случае, все формальности будут соблюдены.

Зависимости

В реализации драйверов используется система логирования ALog, которая является составной частью библиотеки общего назначения SharedTools.

Демо-примеры

Специально для этой статьи был создан демонстрационный проект. Он содержит примеры работы с тремя СУБД: FireBird, PostgreSQL, MS SQL. Репозиторий с драйверами расположен здесь, он подключен в проект как субмодуль. Библиотека SharedTools так же подключена как субмодуль.

Проект создан с использованием QtCreator, сборочная система QBS. Есть четыре сборочных сценария:

  1. db_demo_project.qbs - демо примеры для всех СУБД (содержит пункты 2-4);

  2. db_demo_firebird.qbs - демо пример для FireBird (требуется FireBird-клиент);

  3. db_demo_postgres.qbs - демо пример для PostgreSQL (требуется пакет libpq-dev);

  4. db_demo_mssql.qbs - демо пример для MS SQL.

Драйвера в первую очередь разрабатывались для работы в Linux, поэтому эксплуатационное тестирование выполнялось именно для этой ОС. В Windows будет работать FireBird-драйвер (проверено), для остальных драйверов тестирование не проводилось.

Демо-примеры записывают следующие логи:

  • /tmp/db-demo-firebird.log

  • /tmp/db-demo-mssql.log

  • /tmp/db-demo-postgres.log

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

Заключение

Черновой вариант статьи не предполагал наличие этого раздела, за что старый товарищ и, по совместительству, корректор подверг меня критике: "Мол, непонятна мотивация, целеполагание неясно. Зачем ты вообще писал эту статью?!" Что ж, исправляюсь!

В создание драйверов вложено много моего труда и труда коллег, потрачено время жизни. Зная нелюбовь программистов к внешним зависимостям, я не питаю иллюзий по поводу того, что представленные решения будут использоваться "как есть". Допускаю, что кто-то решит "выжечь каленым железом" ALog и заменит его на нечто свое - я не буду против (сам так поступаю с другими логгерами ;) В любом случае, если наши решения сэкономят кому-то время, или послужат отправной точной для новых идей - будет хорошо!

Источник: habr.com
К списку статей
Опубликовано: 16.06.2021 00:20:01
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование

C++

Qt

Базы данных

Драйверы

Категории

Последние комментарии

  • Имя: Murshin
    13.06.2024 | 14:01
    Нейросеть-это мозг вселенной.Если к ней подключиться,то можно получить все знания,накопленные Вселенной,но этому препятствуют аннуннаки.Аннуннаки нас от неё отгородили,установив в головах барьер. Подр Подробнее..
  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru