Глава 12. Базы данных.

Модуль SQL, в библиотеке Qt, предоставляет независимый от типа платформы и базы данных интерфейс, для доступа к базам данных SQL, и набор классов, обеспечивающих взаимодействие пользовательского интерфейса с базами данных.

Глава начинается с демонстрационного примера, который показывает, как установить соединение с базой данных и как выполнить произвольный SQL-код. Во втором и третьем разделах мы подробнее остановимся на том, как предоставить пользователю возможность просматривать и изменять наборы данных, используя QDataTable -- для просмотра данных в табличном виде, и QSqlForm -- в виде формы.

12.1. Установление соединения и выполнение запроса.

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

bool createConnection() { QSqlDatabase *db = QSqlDatabase::addDatabase("QOCI8"); db->setHostName("mozart.konkordia.edu"); db->setDatabaseName("musicdb"); db->setUserName("gbatstone"); db->setPassword("T17aV44"); if (!db->open()) { db->lastError().showMessage(); return false; } return true; } Первым делом, вызовом QSqlDatabase::addDatabase(), создается экземпляр класса QSqlDatabase. Аргумент функции определяет драйвер базы данных, используемый для доступа к ней. В данном случае -- это драйвер Oracle. Коммерческая версия Qt включает в себя следующий набор драйверов: QODBC3 (ODBC), QOCI8 (Oracle), QTDS7 (Sybase Adaptive Server), QPSQL7 (PostgreSQL), QMYSQL3 (MySQL), and QDB2 (IBM DB2). В некоммерческие версии Qt входит только часть этого набора. [1]

Затем указывается сетевое имя сервера баз данных, имя базы данных, имя пользователя и пароль, после чего выполняется попытка установить соединение. Если функция open() завершилась неудачей -- выводится сообщение об ошибке, с помощью QSqlError::showMessage().

Обычно функция, подобная createConnection() вызывается из функции main():

int main(int argc, char *argv[]) { QApplication app(argc, argv); if (!createConnection()) return 1; ... return app.exec(); } После установления соединения, посредством QSqlQuery, можно выполнять SQL-запросы к базе данных. Например, следующий код выполняет SQL-предложение -- SELECT: QSqlQuery query; query.exec("SELECT title, year FROM cd WHERE year >= 1998"); После вызова функции exec(), можно просматривать полученный набор данных: while (query.next()) { QString title = query.value(0).toString(); int year = query.value(1).toInt(); cerr << title.ascii() << ": " << year << endl; } Первый вызов next() позиционирует QSqlQuery на первую запись в наборе данных. Последующие вызовы next() передвигают указатель на следующую запись и так до тех пор, пока не будет достигнут конец набора. В этой точке next() вернет false.

Функция value() возвращает значение поля в виде QVariant. Поля нумеруются, начиная с 0, в порядке их следования в предложении SELECT. Класс QVariant может хранить огромное количество типов языка C++ и Qt, в том числе int и QString. Различные типы данных, которые могут храниться в базе данных переводятся в соответствующие типы C++ и Qt, и сохраняются в виде QVariant. Например, VARCHAR представляется в виде QString, а DATETIME -- как QDateTime.

Класс QSqlQuery предоставляет целый набор функций для навигации по набору данных: first(), last(), prev(), seek() и at(). Они очень удобны в использовании, но на некоторых базах данных могут оказаться довольно медлительными и ресурсоемкими. С целью оптимизациии, при работе с большими наборами данных, можно вызвать QSqlQuery::setForwardOnly(true), перед exec(), а затем выполнять просмотр набора данных с помощью next(), правда в этом случае мы получаем, так называемые, однонаправленные наборы данных, т.е. такие наборы, навигация по которым может осуществляться только вперед, с помощью next().

Чуть выше говорилось о том, что SQL-запрос передается как аргумент функции exec(), но текст запроса может передаваться напрямую, конструктору QSqlQuery:

QSqlQuery query("SELECT title, year FROM cd WHERE year >= 1998"); Проверка на наличие ошибок и выдача сообщения могут быть выполнены таким образом: if (!query.isActive()) query.lastError().showMessage(); Выполнение предложения INSERT ничуть не сложнее, чем SELECT: QSqlQuery query("INSERT INTO cd (id, artistid, title, year) " "VALUES (203, 102, 'Living in America', 2002)"); После выполнения такого запроса, QSqlQuery::numRowsAffected() возвращает количество записей, подвергшихся изменению (или -1, если база данных не предусматривает поставку такой информации).

В случае необходимости вставить в запрос значения переменных или когда нежелательно, или невозможно перевести аргументы предложения INSERT в строковый вид, можно построить параметризованный запрос, с помощью функции prepare(). Текст параметризованного запроса, вместо реальных значений содержит параметры, которые заполняются фактическими значениями после создания запроса. Qt поддерживает Oracle-подобный и ODBC-подобный стили именования параметров для всех типов баз данных. В примере ниже показано использование Oracle-подобного стиля именования:

QSqlQuery query(db); query.prepare("INSERT INTO cd (id, artistid, title, year) " "VALUES (:id, :artistid, :title, :year)"); query.bindValue(":id", 203); query.bindValue(":artistid", 102); query.bindValue(":title", QString("Living in America")); query.bindValue(":year", 2002); query.exec(); Теперь тот же самый пример, но в стиле ODBC: QSqlQuery query(db); query.prepare("INSERT INTO cd (id, artistid, title, year) " "VALUES (?, ?, ?, ?)"); query.addBindValue(203); query.addBindValue(102); query.addBindValue(QString("Living in America")); query.addBindValue(2002); query.exec(); После создания запроса, вызовом prepare(), параметры запроса заполняются фактическими значениями, с помощью функции bindValue() или addBindValue(), после чего запрос исполняется вызовом exec(). Параметризованные запросы можно выполнять в цикле. Перед началом цикла создается запрос, а в теле цикла производится заполнение параметров новыми значениями и исполнение запроса.

Параметризованные запросы очень часто используются в тех случаях, когда в базу данных нужно записать двоичные данные или строки, которые содержат символы из наборов, не принадлежащих диапазону ASCII или Latin-1. Для баз данных, которые поддерживают Unicode, Qt использует эту кодировку символов, в других случаях выполняется преобразование строк в соответствующую кодировку.

Qt поддерживает механизм транзакций для баз данных, в которых он присутствует. Для запуска транзакции вызывается метод объекта QSqlDatabase -- transaction(). Для завершения транзакции вызывается либо функция commit(), либо rollback(). Например, выполним поиск по внешнему ключу и вставим запись в таблицу в рамках транзакции:

QSqlDatabase::database()->transaction(); QSqlQuery query; query.exec("SELECT id FROM artist WHERE name = 'Gluecifer'"); if (query.next()) { int artistId = query.value(0).toInt(); query.exec("INSERT INTO cd (id, artistid, title, year) " "VALUES (201, " + QString::number(artistId) + ", 'Riding the Tiger', 1997)"); } QSqlDatabase::database()->commit(); Функция QSqlDatabase::database() возвращает указатель на объект QSqlDatabase, который был создан в createConnection(). Если транзакция не может быть запущена, QSqlDatabase::transaction() возвращает false.

Некоторые базы данных не поддерживают механизм транзакций. В этом случае, функции transaction(), commit() и rollback() не выполняют никаких действий. Наличие поддержки механизма транзакций, той или иной базой данных, можно проверить с помощью метода hasFeature(), объекта QSqlDriver, ассоциированного с базой данных:

QSqlDriver *driver = QSqlDatabase::database()->driver(); if (driver->hasFeature(QSqlDriver::Transactions)) ... В примерах выше рассматривались случаи с единственным подключением к базе данных. Однако ничто не мешает нам создать и второе, и третье и т.д. соединения. В этом случае необходимо просто передать имя соединения, вторым аргументом в функцию addDatabase(): QSqlDatabase *db = QSqlDatabase::addDatabase("QPSQL7", "OTHER"); db->setHostName("saturn.mcmanamy.edu"); db->setDatabaseName("starsdb"); db->setUserName("gilbert"); db->setPassword("ixtapa6"); Чтобы потом получить указатель на объект QSqlDatabase, достаточно просто передать имя соединения в функцию QSqlDatabase::database(): QSqlDatabase *db = QSqlDatabase::database("OTHER"); Для исполнения запросов через эти соединения, необходимо передать объект QSqlDatabase конструктору QSqlQuery: QSqlQuery query(db); query.exec("SELECT id FROM artist WHERE name = 'Mando Diao'"); Каждое соединение с базой данных может поддерживать только одну активную транзакцию, поэтому множественные подключения могут оказаться полезными в том случае, когда необходимо одновременно запустить несколько транзакций. При использовании нескольких соединений, в приложении по прежнему имеется одно неименованное соединение, которое используется по-умолчанию объектами QSqlQuery, если им явно не указать с каким соединением они должны работать.

В дополнение к QSqlQuery, Qt предоставляет класс QSqlCursor, производный от QSqlQuery. Этот класс расширяет функциональность предка большим числом дополнительных методов, которые позволяют отказаться от написания SQL-запросов для наиболее употребимых SQL-операций, таких как: SELECT, INSERT, UPDATE и DELETE. Кроме того QSqlCursor выступает в роли посредника между QDataTable и базой данных. Далее, в этом разделе мы будем говорить о QSqlCursor, а в следующем разделе покажем, как можно использовать QDataTable, для представления наборов данных в табличной форме.

Следующий пример демонстрирует выполнение SQL-запроса -- SELECT:

QSqlCursor cursor("cd"); cursor.select("year >= 1998"); Эквивалентный вариант с использованием QSqlQuery: QSqlQuery query("SELECT id, artistid, title, year FROM cd " "WHERE year >= 1998"); Навигация по набору данных выполняется точно так же, как и в QSqlQuery, за одним маленьким исключением -- теперь, вместо порядкового номера поля, функции value() можно передать его имя: while (cursor.next()) { QString title = cursor.value("title").toString(); int year = cursor.value("year").toInt(); cerr << title.ascii() << ": " << year << endl; } Для вставки записи в таблицу, предварительно нужно создать новую запись QSqlRecord, вызовом primeInsert(), а затем, для каждого из полей, вызвать setValue(). После всего этого можно выполнить вставку функцией insert(): QSqlCursor cursor("cd"); QSqlRecord *buffer = cursor.primeInsert(); buffer->setValue("id", 113); buffer->setValue("artistid", 224); buffer->setValue("title", "Shanghai My Heart"); buffer->setValue("year", 2003); cursor.insert(); Чтобы изменить запись -- нужно позиционировать QSqlCursor на запись, которая должна подвергнуться изменениям (например, с помощью select() и next()). Получить указатель на QSqlRecord, вызовом primeUpdate(). После этого записать новые значения функцией setValue() и вызвать update(), чтобы отправить сделанные изменения в базу данных: QSqlCursor cursor("cd"); cursor.select("id = 125"); if (cursor.next()) { QSqlRecord *buffer = cursor.primeUpdate(); buffer->setValue("title", "Melody A.M."); buffer->setValue("year", buffer->value("year").toInt() + 1); cursor.update(); } Процедура удаления записи похожа на процедуру изменения: QSqlCursor cursor("cd"); cursor.select("id = 128"); if (cursor.next()) { cursor.primeDelete(); cursor.del(); } Классы QSqlQuery и QSqlCursor реализуют интерфейс между Qt и базами данных SQL. В следующих двух разделах мы покажем как они могут использоваться в приложениях с графическим интерфейсом, которые позволяют пользователю просматривать и изменять наборы данных, хранящиеся в базе.

Примечания

[1]

От переводчика: кроме вышеперечисленных, Qt 3.2 включает в себя еще один драйвер, который уважаемые авторы, видимо по забывчивости, не указали -- это QIBASE (Interbase/Firebird).