Pull to refresh

QST: QsT SQL Tools, инструментарий для Qt

Reading time11 min
Views1.2K
UPD
Данный блог появился благодаря advix. Этот человек дал инвайт за пост в песочнице, за что я ему искренне благодарен.

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

Преамбула

Начиная с лета 2009 года, я активно изучаю Qt. И знаете, по-настоящему счастлив программировать в этой среде. Она решила все мои главные проблемы. Например, я не умел строить интерфейс программы, особенно меня раздражало, что каждый контрол в том же Builder нужно выравнивать, перетаскивать, подгонять размеры и положение. С Qt об этом можно забыть, – и заниматься исключительно эстетикой без всякой рутины. Кроме того, мне нравится STL за ее хитрый подход (хотя еще ее не знаю толком), и в Qt она есть, равно как и собственные контейнеры, – используй с удовольствием. И еще «Кьют» – очень продуманная библиотека, вся такая ООП-шная и технологичная. А я очень чту и уважаю ООП, равно как и паттерны проектирования…
В конце лета получаю заказ на базу данных. Вопрос, на чем писать, не стоит. Конечно, Qt! Практика поможет изучить то, что еще скрыто. Начинаю работать над приложением. Вроде бы формочки красивые получаются, код удобно писать, все есть… И должен бы радоваться, да вот получается лапша из SQL-С++ кода. Ошибкоопасная, в сопровождении трудная, на вид – убожество… Как потом людям в глаза смотреть и называть себя программистом?
Так родился проектик. Да, маленький проект, призванный ото всего этого безобразия избавить. Благо, тропинка проторена, и для всех языков программирования, для всех платформ уже есть подобные вещи. Ну и ладно, что есть. Хочу свою, тем более, под Qt ничего толкового не нашел.

QST: QsT SQL Tools

Это инструментарий, а лучше сказать – библиотека, а еще лучше сказать – набор классов, который избавляет программиста от «SQL-лапши» в коде. Естественно, с помощью генерации запросов, но не простой, а через специальные DFD-описатели, что дает много-много полезных плюшек. Как-то: обращаемся к полям таблицы БД по именам, извлекаем любые данные, имеем разные DFD-описатели под разные запросы, работаем с моделями-представлениями, инкапсулируем все это в классы-хэндлеры, а те еще много чего умеют, поелику отнаследованы от AbstractModelHanlder…
Но – по порядку.

DFD

Я разработал концепцию, которая называется DFD: «Declarative Field Descriptor». (Хочу прибавить к ней L[anguage], но тянет ли на язык?..) Концепция генерации простых SQL-запросов из описателей. Легче всего ее объяснить на примере. Хотим, значит, мы создать такой запрос:

SELECT [ID], LastName, FirstName, ParentName, SerialNumber, Number, DocType_ID
FROM tPersonalDocuments
WHERE
[ID] > 30
AND
DocType = 1


И будет он выглядеть так:

SqlBatch batch;
batch.addSource("tPersonalDocuments");
// Секция 1
batch << SqlField("[ID]", fv_invisible, fr_id)
<< SqlField("LastName", fv_visible, fr_none, "Фамилия", 120)
<< SqlField("FirstName", fv_visible, fr_none, "Имя", 120)
<< SqlField("ParentName", fv_visible, fr_none, "Отчество", 120)
<< SqlField("SerialNumber", fv_visible, fr_none, "Серия", 45)
<< SqlField("Number", fv_visible, fr_none, "Номер", 70)
<< SqlField("DocType_ID", fv_invisible)
// Секция 2
<< SqlField("[ID]", SqlValue(30, fo_greater), fp_where)
<< SqlField("DocType_ID", SqlValue(1), fp_where);

// Результат – описатель. На 0 в конце пока не обращаем внимания.
SqlQueryDescriptor queryDescriptor(batch, sql_select, 0);


Как видно, список для FROM загружается с помощью SqlBatch:: addSource(), и далее идут поля, поля, поля… Все просто. В SqlBatch загружается класс SqlField, который (секция 1) отвечает за конкретное поле, роль поля, его присутствие/отсутствие в Qt’шных view, шапку для колонки во view и ширину этой колонки. Например, поле ID – ключевое, а это важный момент, и мы даем ему роль ключа: fr_id. И еще оно не должно отображаться во view, т.е., мы хотим скрыть от пользователя, какой там ключ у записи. Нет, если не хотим скрывать, так и пожалуйста, никто препятствовать не будет… В секции 2 мы описываем фильтры для WHERE, и чтобы отличить их от других, есть параметр fp_where, принадлежащий enum SqlFieldPurpose. Для фильтра нужно указать поле и значение – специальный класс SqlValue. В примере видим целочисленные значения; здесь:

batch << SqlField("[ID]", SqlValue(30, fo_greater), fp_where);


указан функтор сравнения «больше», а здесь:

batch << SqlField("DocType_ID", SqlValue(1), fp_where);


функтор не указан, поэтому по умолчанию для целочисленных берется fo_equal.
Да-да, все это хорошо, а как же с другими типами, особенно со строкой и датами? Всё можно! Эти обрывки SQL-запроса:

FirstName LIKE 'Алекс%'
AND
Birthday BETWEEN convert(datetime '20.01.2009', 104) AND
convert(datetime '15.05.2009', 104)


легким движением руки превращаются в элегантные строчки:

batch << SqlField("FirstName", SqlValue("Алекс", fo_like, fb_right), fp_where)
<< SqlField("Birthday", SqlValue(QDate(2009, 1, 20)), SqlValue(QDate(2009, 5, 15)));


(Конвертация даты происходит автоматически, в соответствии с шаблоном, заданным в константе.)
Вспомним, что кроме секции WHERE в простом SELECT’е могут быть еще ORDER BY и GROUP BY. Класс SqlField имеет конструкторы и для этих случаев:

SqlField("Birthday", fp_order_by)
SqlField("Birthday", fp_group_by)


(Правда, для GROUP BY мы должны бы указать агрегирующие функции и только те поля, которые в них участвуют, – но оставим на совести программиста, получится у него валидный SQL-запрос или нет.)
Сам запрос генерируется классами SqlGen и SqlQueryComposer. Фактически, генерирует только SqlQueryComposer, а SqlGen определяет порядок полей и предоставляет высокоуровневый интерфейс для генерации. (Кстати, при желании можно класс SqlQueryComposer переписать под другой диалект SQL.) Вот как можно получить запрос SELECT, передав ему наш заполненный полями класс SqlBatch:

SqlGen gen;
QString selectQuery = gen.query(batch, sql_select);


А если мы напишем вместо sql_select что-нибудь другое, например, sql_update, то генератор попробует сгенерировать UPDATE-запрос, взяв только те поля, которые для него разрешены. Какие? Ну, те, у которых параметр SqlFieldPurpose стоит вместо | вместе с другими. Вот пример такого поля:

SqlField("Age", SqlValue(10), fp_where | fp_update | fp_insert)


Соответственно, при sql_update будет картина вроде этой: «SET Age = 10», при sql_insert что-то подобное:

INSERT INTO … (Age)
VALUES (10)


а для всех остальных случаев – просто фильтр для WHERE:

WHERE
Age = 10


Концепция DFD-описателей начинает проясняться, правда? И то верно, что она сильно похожа на другие подходы. Я не горел желанием пооригинальничать…

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

AbstractModelHandler

В моей библиотечке есть некий абстрактный класс AbstractModelHandler. Он содержит много всего хорошего, и лучше него никто и ничто не может работать с DFD-описателями. Допустим, программист хочет инкапсулировать работу с таблицей родов войск tArmyTypes. Для этого он создает класс-наследник h_ArmyTypesHandler:

const int ARMY_TYPES_QUERY = 7575;

class h_ArmyTypesHandler : public AbstractModelHandler
{
public:
h_ArmyTypesHandler();

private:

virtual SqlQueryDescriptor _selector(const SqlQueryModelTypes &modelType = mt_plain, const int &queryNumber = 0) const;
virtual SqlQueryDescriptor _inserter(const int &queryNumber = 0) const;
virtual SqlQueryDescriptor _updater(const int &queryNumber = 0) const;
virtual SqlQueryDescriptor _deleter(const int &queryNumber = 0) const;
virtual SqlQueryDescriptor _executor(const int &queryNumber = 0) const;
};


Виртуальные функции _selector(), _inserter(), updater(), _deleter() и _executor() унаследованы от базового класса. Именно они содержат в себе DFD-описатели и выдают их по требованию других функций AbstractModelHandler’а. Например, вот типичное переопределение функции _selector():

SqlQueryDescriptor h_ArmyTypesHandler::_selector(const SqlQueryModelTypes &modelType, const int &queryNumber) const
{
SqlBatch batch;

batch.addSource("tArmyTypes");

if (queryNumber == ARMY_TYPES_QUERY)
{
batch << SqlField("ID", fv_invisible, fr_id)
<< SqlField("ShortName", fv_visible, fr_none, "Сокращение", 90)
<< SqlField("Name", fv_visible, fr_none, "Род войск", 100)

<< SqlField("ID", value(ID_VALUE), fp_where)
<< SqlField("Name", value("Name"), fp_where)
<< SqlField("ShortName", value("ShortName"), fp_where);
}
else
if (queryNumber == LAST_ID)
{
batch << SqlField("max(ID)", fv_visible, fr_none);
}
else
{
Q_ASSERT(false);
}

return SqlQueryDescriptor(batch, sql_select, queryNumber);
}


Мы видим здесь нечто новое: некую функцию value(). Возможно, кто-то уже догадался, что она возвращает по имени ранее записанное значение SqlValue(), например, переданное в h_ArmyTypesHandler где-нибудь в классе формы:

h_ArmyTypesHandler th;
th.setValue("Name", SqlValue("Имя_Имя_Имя", fo_not_equal, fb_none));


То есть, мы сохраняем значение функцией setValue() под нужным именем, а затем в описателе извлекаем его с помощью функции value() (обе – от класса AbstractModelHandler). Теперь, при генерации запроса, у нас будут параметризованные DFD-описатели.

Предпоследняя строчка примера проливает свет на третий параметр конструктора класса SqlQueryDescriptor: это номер запроса. Вы можете одной и той же функцией _selector() генерировать разные запросы, хоть два, хоть десять: для каждой ситуации свой запрос. И это хорошо. Однажды вам нужны такие поля, в другой раз – такие; потом вы хотели бы вызвать запрос с агрегирующими функциями, а когда и просто запрос, вообще не из той таблицы. Вам достаточно для каждого DFD-описателя определить номер запроса, и – вуаля!
Встает закономерный вопрос, как вообще генерировать и выполнять SQL-запросы. Ну, например, так:


const int ARMY_TYPE_INSERT_QUERY = 5;
const int ARMY_TYPE_UPDATE_QUERY = 68;


h_ArmyTypesHandler th;
th.setValue("Name", SqlValue("Имя_Имя_Имя", fo_not_equal, fb_none));
th.Insert(ARMY_TYPE_INSERT_QUERY);

th.setValue(ID_VALUE, SqlValue(10));
th.Update(ARMY_TYPE_UPDATE_QUERY);
th.Delete(4);


При этом вызываются соответствующие функции: _inserter(), _updater() и _deleter(), где лежат описатели. Затем абстрактный родитель хэндлера генерирует SQL и исполняет его средствами Qt. Все просто.

Однако, чаще приходится работать с SELECT-запросами, которые в пустоту не выполнишь, ведь нужно же видеть результат выборки! Желаем мы, например, чтобы TableView отображала таблицу родов войск. Нет проблем!

…Но сначала отступление. В Qt реализована одна из разновидностей паттерна MVC, по которой для QTableView необходима модель данных. Таковые есть в самой Qt, а можно и свою сделать. Признаюсь честно, в программировании своих моделей я не силен, но для библиотечки написал простейшую древовидную модель, которой мне так не хватало – SqlTreeModel. Да, в Qt нет древовидной модели, удовлетворяющей моим требованиям, или я просто мало знаю. Как бы то ни было, в QST имеются две модели: SqlTreeModel и SqlQueryModel, последняя лишь незначительно отличается от QSqlQueryModel, наследуясь от нее. Модели активно используются в классе AbstractModelHandler.

Теперь нужно вникнуть в еще одно понятие библиотеки QST: источник данных. Это, фактически, связка DFD – Model – View. Каждый источник данных поименован, имя задается программистом. Чтобы отобразить в нашей TableView таблицу родов войск, мы в классе формы создаем источник данных:


h_ArmyTypesHandler _handler;
SqlQueryModel _model;


void f_ArmyTypesForm::loadArmyTypes()
{
_handler.reloadSource(ARMY_TYPES_SOURCE, ARMY_TYPES_QUERY, &_model);
_handler.setTableView(ARMY_TYPES_SOURCE, ui->TableView);
}


И все. В таблице TableView появятся колонки с заданными ширинами и заголовками. А заданы эти параметры, стало быть, в функции h_ArmyTypesHandler::_selector(). Перечислю появившиеся колонки: «ShortName» с названием «Сокращение» и шириной 90; «Name» с названием «Род войск» и шириной 100. А вот поле «ID» отображено не будет, поскольку у него стоит fv_invisible… Другими словами, функции reloadSource() и setTableView() сделают за программиста всю рутину по форматированию таблицы.
ARMY_TYPES_SOURCE – это текстовое имя нового источника данных. Правда, если повторно вызвать loadArmyTypes(), то источник «перезальется»: снова будет вызвана функция _selector(), снова будет сгенерирован SELECT-запрос, и модель данных старые строчки удалит, а новые возьмет из БД. При вызове следующих функций мы получим два разных набора строчек:

void f_ArmyTypesForm::someFunc1()
{
_handler.setValue(ID_VALUE, SqlValue(10));
loadArmyTypes();
}

void f_ArmyTypesForm::someFunc2()
{
_handler.setValue(ID_VALUE, SqlValue(111, fo_less));
loadArmyTypes();
}


Ага, ага, мне этого мало. AbstractModelHandler позволяет создавать несколько источников данных (а иначе зачем бы мы давали им имена?!.). Допустим, помимо TableView у нас на форме есть еще TreeView и ComboBox, куда мы хотим вывести то же самое. Без проблем! Пишем что-то вроде следующего:


h_ArmyTypesHandler _handler;
SqlQueryModel _model;
SqlQueryModel _modelPlainForTreeView;
SqlQueryModel _modelForComboBox;


void f_ArmyTypesForm::loadArmyTypes()
{
_handler.reloadSource(ARMY_TYPES_SOURCE, ARMY_TYPES_QUERY, &_model);
_handler.setTableView(ARMY_TYPES_SOURCE, ui->TableView);

_handler.reloadSource(TREE_VIEW_SOURCE, ARMY_TYPES_QUERY, &_modelPlainForTreeView);
_handler.setTreeView(TREE_VIEW_SOURCE, ui->TreeView);

_handler.reloadSource(COMBO_BOX_SOURCE, ARMY_TYPES_QUERY, &_modelForComboBox);
_handler.setComboBox(COMBO_BOX_SOURCE, ui->ComboBox);
}


И что с того, спросите вы. Как что? Модели разные передаются? Разные. Стало быть, и источников данных тоже три. Только хэндлер у них общий, queryNumber один и тот же – ARMY_TYPES_QUERY, следовательно, и запросы генерируются одни и те же. Зато мы можем делать волшебные вещи! Например, такие:

// Ключевое поле текущей строки ComboBox:
QVariant id = _handler.keyValueOfCurrent(ARMY_TYPES_SOURCE, ui->ComboBox);

// Ключевое поле текущей строки TreeView:
QVariant id2 = _handler.keyValueOfView(TREE_VIEW_SOURCE);

// Значение поля Name для строки с ID == id2:
h_ArmyTypesHandler th;
th.setValue(ID_VALUE, SqlValue(id2));
QVariant name = th.SelectToValue("Name", ARMY_TYPES_QUERY);

// Удаляем текущую в TableView строку из БД (если ее вообще возможно удалить):
_handler.DeleteCurrent(ARMY_TYPES_SOURCE, ui->TableView);


И даже выбирать данные списком:

h_ArmyTypesHandler th;
th.setValue(ID_VALUE, SqlValue(id2));

QVariantMap valMap = th.SelectToMap(QStringList() << "Name" << "ShortName" << "ID", ARMY_TYPES_QUERY);

ui->LineEdit_Name->setText(valMap["Name"].toString());


И ни одного SQL-запроса в коде! Кра-со-та!!!

И в заключение…

Многих деталей я, конечно, не раскрываю. Даже в такой маленькой библиотечке их более чем достаточно. К сожалению, многого QST не умеет. Сильно не хватает нормальной древовидной модели; та, что есть, может быть с ошибками, да и странно себя ведет при взаимодействии с сигналами QTreeView: при определенных условиях крашится, залезая в чужую память. Найти ошибку не могу. У меня даже есть пример, с которым я обратился к людям на двух форумах по Qt, чтобы они мне помогли, но пока никто ничего не сказал.
Пока нет места в DFD и более сложным запросам. Сейчас я размышляю, как можно добавить условие OR в секцию WHERE. Возможно, стоит подумать и над JOIN’ами, которые бывают очень нужны. Не то чтобы имеющимися средствами нельзя обойти эти ограничения, – можно, язык SQL достаточно многогранен, – однако, развиваться тоже надо.
Дополнительно в библиотечку включен класс по работе с подключениями к БД (DBConnection), а так же класс, устанавливающий кодек для функции tr() (Cyrillic). Ну и самопальный TreeItem с заимствованными способами работы с нодами. Мне класс, если честно, не нравится. И SqlTreeModel не нравится. Не люблю я, когда по коду раскиданы new и delete. Если уж и работать с памятью, то через принцип «Получение ресурса есть инициализация» . Глядишь, и перепишу все по уму. Когда-нибудь.
Мне бы очень хотелось, чтобы кому-то эта разработка помогла. И я был бы не против помощи от сообщества в выявлении ошибок, в предложениях по улучшению. Исходники QST лежат в архиве на SourceForge, здесь. Лицензия свободная, GPL v3 и LGPL v3. Дополнительную информацию можно найти в этой теме.
Буду рад ответить на вопросы заинтересовавшихся.

С уважением,
Гранин Александр
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+5
Comments11

Articles