О ресурре... Главная
The Belarus Internet the Resource about Linux
 
   
Ж. Бланшетт, М. Саммерфильд, "Глава 12. Базы данных" - 01/12/2004

Предыдущая глава | Содержание | Следующая глава

Глава 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 входит только часть этого набора. [7]

Затем указывается сетевое имя сервера баз данных, имя базы данных, имя пользователя и пароль, после чего выполняется попытка установить соединение. Если функция 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. В следующих двух разделах мы покажем как они могут использоваться в приложениях с графическим интерфейсом, которые позволяют пользователю просматривать и изменять наборы данных, хранящиеся в базе.


12.2. Представление данных в табличной форме.

Класс QDataTable -- это ориентированный на работу с базами данных виджет, наследник QTable. Взаимодействие QDataTable с базой данных осуществляется посредством QSqlCursor. В этом разделе мы рассмотрим два диалога, которые используют виджет QDataTable. Диалоги, работающие с QSqlForm будут представлены в следующем разделе.

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

  CREATE TABLE artist ( 
      id INTEGER PRIMARY KEY, 
      name VARCHAR(40) NOT NULL, 
      country VARCHAR(40)); 
      
  CREATE TABLE cd ( 
      id INTEGER PRIMARY KEY, 
      artistid INTEGER NOT NULL, 
      title VARCHAR(40) NOT NULL, 
      year INTEGER NOT NULL, 
      FOREIGN KEY (artistid) REFERENCES artist); 
    
  CREATE TABLE track ( 
      id INTEGER PRIMARY KEY, 
      cdid INTEGER NOT NULL, 
      number INTEGER NOT NULL, 
      title VARCHAR(40) NOT NULL, 
      duration INTEGER NOT NULL, 
      FOREIGN KEY (cdid) REFERENCES cd);
      
Некоторые базы данных не поддерживают внешние ключи. В этом случае вам следует удалить предложения FOREIGN KEY. Пример останется работоспособным, но база данных не сможет соблюдать ссылочную целостность данных.

Рисунок 12.1. Таблицы приложения CD Collection.


Первым будет класс диалога, который позволит пользователю редактировать список исполнителей. С его помощью пользователь сможет добавлять, изменять или удалять сведения об исполнителях, выбирая соответствующие пункты контекстного меню QDataTable. Внесенные изменения будут записываться в базу данных, по нажатию кнопки Update.

Рисунок 12.2. Диалог ArtistForm.


Определение класса диалога:
class ArtistForm : public QDialog 
{ 
  Q_OBJECT 
public: 
  ArtistForm(QWidget *parent = 0, const char *name = 0); 
  
protected slots: 
  void accept(); 
  void reject(); 
  
private slots: 
  void primeInsertArtist(QSqlRecord *buffer); 
  void beforeInsertArtist(QSqlRecord *buffer); 
  void beforeDeleteArtist(QSqlRecord *buffer); 
  
private: 
  QSqlDatabase *db; 
  QDataTable *artistTable; 
  QPushButton *updateButton;
  QPushButton *cancelButton; 
};
      
Слоты accept() и reject() унаследованы от QDialog.
ArtistForm::ArtistForm(QWidget *parent, const char *name) 
    : QDialog(parent, name) 
{ 
  setCaption(tr("Update Artists")); 
  
  db = QSqlDatabase::database("ARTIST"); 
  db->transaction(); 
  
  QSqlCursor *artistCursor = new QSqlCursor("artist", true, db); 
  artistTable = new QDataTable(artistCursor, false, this); 
  artistTable->addColumn("name", tr("Name")); 
  artistTable->addColumn("country", tr("Country")); 
  artistTable->setAutoDelete(true); 
  artistTable->setConfirmDelete(true); 
  artistTable->setSorting(true); 
  artistTable->refresh(); 
  
  updateButton = new QPushButton(tr("Update"), this); 
  updateButton->setDefault(true); 
  cancelButton = new QPushButton(tr("Cancel"), this);
      
В конструкторе ArtistForm запускается транзакция для соединения под именем "ARTIST". Затем создается QSqlCursor, для таблицы artist в базе данных, и QDataTable, которая будет отображать содержимое таблицы.

Второй аргумент конструктора QSqlCursor -- это флаг "автозаполнение". Если в этом аргументе передать true, QSqlCursor будет загружать информацию о каждом из полей в таблице.

Второй аргумент конструктора QDataTable -- так же флаг "автозаполнение". В случае true, QDataTable будет автоматически создавать колонки для каждого из полей в QSqlCursor. В нашем примере этот флаг передается со значением false и с помощью addColumn() в виджет добавляются две колонки, соответствующие полям name и country.

Владение объектом QSqlCursor передается виджету QDataTable. Вызовом setAutoDelete() устанавливается режим автоматического удаления записей, средствами QDataTable, таким образом нам не нужно будет писать дополнительный код, удаляющий записи из таблицы. Вызовом setConfirmDelete() устанавливается режим подтверждения удаления, теперь QDataTable будет выкидывать перед пользователем окно с запросом на подтверждение выполнения операции удаления. Функция setSorting(true) позволит пользователю выполнять сортировку данных в виджете, щелчком мыши по заголовкам колонок. В заключение вызывается функция refresh(), которая заполняет QDataTable данными.

Затем создаются кнопки Update и Cancel.

  connect(artistTable, SIGNAL(beforeDelete(QSqlRecord *)), 
          this, SLOT(beforeDeleteArtist(QSqlRecord *))); 
  connect(artistTable, SIGNAL(primeInsert(QSqlRecord *)), 
          this, SLOT(primeInsertArtist(QSqlRecord *))); 
  connect(artistTable, SIGNAL(beforeInsert(QSqlRecord *)), 
          this, SLOT(beforeInsertArtist(QSqlRecord *))); 
  connect(updateButton, SIGNAL(clicked()), 
          this, SLOT(accept())); 
  connect(cancelButton, SIGNAL(clicked()), 
          this, SLOT(reject()));
      
Здесь подключаются три сигнала от QDataTable к трем приватным слотам. Кнопка Update соединяется со слотом accept(), кнопка Cancel -- со слотом reject().
  QHBoxLayout *buttonLayout = new QHBoxLayout; 
  buttonLayout->addStretch(1); 
  buttonLayout->addWidget(updateButton); 
  buttonLayout->addWidget(cancelButton); 
  QVBoxLayout *mainLayout = new QVBoxLayout(this); 
  mainLayout->setMargin(11); 
  mainLayout->setSpacing(6); 
  mainLayout->addWidget(artistTable); 
  mainLayout->addLayout(buttonLayout); 
}
      
В заключение кнопки передаются менеджеру размещения по горизонтали, а QDataTable и менеджер размещения по горизонтали -- менеджеру размещения по вертикали.
void ArtistForm::accept() 
{ 
  db->commit(); 
  QDialog::accept(); 
}
      
Когда пользователь нажимает кнопку Update, выполняется подтверждение транзакции и вызывается унаследованный метод accept() предка.
void ArtistForm::reject() 
{ 
  db->rollback(); 
  QDialog::reject(); 
}
      
Когда пользователь нажимает кнопку Cancel, выполняется откат транзакции и вызывается унаследованный метод reject() предка.
void ArtistForm::beforeDeleteArtist(QSqlRecord *buffer) 
{ 
  QSqlQuery query(db); 
  query.exec("DELETE FROM track WHERE track.id IN " 
             "(SELECT track.id FROM track, cd " 
             "WHERE track.cdid = cd.id AND cd.artistid = " 
             + buffer->value("id").toString() + ")"); 
  query.exec("DELETE FROM cd WHERE artistid = " 
             + buffer->value("id").toString()); 
}
      
Слот beforeDeleteArtist() связан с сигналом beforeDelete(), виджета QDataTable, который выдается непосредственно перед удалением записи. Здесь выполняется каскадное удаление записей, запуском двух запросов: первый удаляет все записи о дорожках на CD по исполнителю, второй -- все CD по исполнителю. Эти операции не нарушают целостность базы данных, потому что выполняются в контексте одной транзакции, которая была запущена в конструкторе формы.
void ArtistForm::primeInsertArtist(QSqlRecord *buffer) 
{ 
  buffer->setValue("country", "USA"); 
}
      
Слот primeInsertArtist() связан с сигналом primeInsert(), виджета QDataTable, который выдается непосредственно перед созданием новой записи. Здесь устанавливается значение по-умолчанию для поля country.

Это один из способов установки значений по-умолчанию. Другой способ состоит в создании производного класса от QSqlCursor и перекрытии метода primeInsert(), но такая метода имеет смысл только в том случае, если один и тот же класс QSqlCursor используется в нескольких местах в приложении и обеспечивает непротиворечивость интерфейса. Третий вариант -- сделать это на уровне базы данных, с помощью предложения DEFAULT в блоке CREATE TABLE.

void ArtistForm::beforeInsertArtist(QSqlRecord *buffer) 
{ 
  buffer->setValue("id", generateId("artist", db)); 
}
      
Слот beforeInsertArtist() связан с сигналом beforeInsert(), виджета QDataTable, который выдается в тот момент, когда пользователь завершает редактирование записи и нажимает клавишу Enter, чтобы подтвердить изменения. Здесь устанавливается значение поля id. Функция generateId() генерирует уникальное значение для первичного ключа.

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

inline int generateId(const QString &table, QSqlDatabase *db) 
{ 
  QSqlQuery query(db); 
  query.exec("SELECT max(id) FROM " + table); 
  query.next(); 
  return query.value(0).toInt() + 1; 
}
      
Функция generateId() гарантирует корректную работу только в контексте той же самой транзакции, где исполняется соответствующее выражение INSERT.

Некоторые типы баз данных поддерживают автоматическую генерацию значений полей. В этом случае нужно просто настроить базу данных на автоматическую генерацию значений поля id и вызвать setGenerated("id", false) класса QSqlCursor, чтобы сообщить ему, что не нужно генерировать значения для поля id.

Теперь рассмотрим другой диалог, который использует QDataTable. Этот диалог реализует просмотр таблиц, связанных отношением "мастер-деталь". Мастер-таблица -- это список компакт дисков (CD). Деталь-таблица -- список дорожек на текущем диске. Этот диалог является главным окном приложения CD Collection.

На этот раз, вместо контекстного меню, на форму диалога положены кнопки Add, Edit и Delete, которые позволяют пользователю вносить изменения в список компакт дисков. Когда пользователь нажимает на кнопку Add или Edit, перед ним появляется диалог CDForm. (Описание формы будет приведено в следующем разделе.)

Рисунок 12.3. Диалог MainForm.


Еще одно отличие этого примера от предыдущего заключается в том, что теперь придется работать с внешними ключами, чтобы вместо числового идентификатора исполнителя вывести его имя и название страны. Чтобы добиться этого, необходимо использовать класс QSqlSelectCursor, производный от класса QSqlCursor.

Определение класса главного окна:

class MainForm : public QDialog 
{ 
  Q_OBJECT 
public: 
  MainForm(QWidget *parent = 0, const char *name = 0); 
  
private slots: 
  void addCd(); 
  void editCd(); 
  void deleteCd(); 
  void currentCdChanged(QSqlRecord *record); 
  
private: 
  QSplitter *splitter; 
  QDataTable *cdTable; 
  QDataTable *trackTable; 
  QPushButton *addButton;
  ...
  QPushButton *quitButton; 
};
      
Класс MainForm -- производный от класса QDialog.
MainForm::MainForm(QWidget *parent, const char *name) 
    : QDialog(parent, name) 
{ 
  setCaption(tr("CD Collection")); 
  
  splitter = new QSplitter(Vertical, this); 
  
  QSqlSelectCursor *cdCursor = new QSqlSelectCursor( 
                  "SELECT cd.id, title, name, country, year " 
                  "FROM cd, artist WHERE cd.artistid = artist.id"); 
  if (!cdCursor->isActive()) { 
    QMessageBox::critical(this, tr("CD Collection"), 
            tr("The database has not been created.\n" 
            "Run the cdtables example to create a sample " 
            "database, then copy cdcollection.dat into " 
            "this directory and restart this application.")); 
    qApp->quit(); 
  }

  cdTable = new QDataTable(cdCursor, false, splitter); 
  cdTable->addColumn("title", tr("CD")); 
  cdTable->addColumn("name", tr("Artist")); 
  cdTable->addColumn("country", tr("Country")); 
  cdTable->addColumn("year", tr("Year")); 
  cdTable->setAutoDelete(true); 
  cdTable->refresh();
      
В конструкторе создается QDataTable для таблицы cd и связанный с ней курсор. Курсор основан на запросе, который соединяет таблицы cd и artist. QDataTable работает в режиме "только для чтения", потому что взаимодействует с объектом класса QSqlSelectCursor. Виджет таблицы, работающий "только на чтение" не имеет контекстного меню.

Если попытка выполнения запроса терпит неудачу, перед пользователем выводится окно, с сообщением об ошибке, и на этом работа приложения завершается.

  QSqlCursor *trackCursor = new QSqlCursor("track"); 
  trackCursor->setMode(QSqlCursor::ReadOnly); 
  trackTable = new QDataTable(trackCursor, false, splitter); 
  trackTable->setSort(trackCursor->index("number")); 
  trackTable->addColumn("title", tr("Track")); 
  trackTable->addColumn("duration", tr("Duration"));
      
Здесь создается второй виджет QDataTable и его курсор. Вызовом setMode(QSqlCursor::ReadOnly) таблица переводится в режим "только для чтения", а вызовом setSort() выполняется сортировка по полю с номером дорожки.
  addButton = new QPushButton(tr("&Add"), this); 
  editButton = new QPushButton(tr("&Edit"), this); 
  deleteButton = new QPushButton(tr("&Delete"), this); 
  refreshButton = new QPushButton(tr("&Refresh"), this); 
  quitButton = new QPushButton(tr("&Quit"), this);
  
  connect(addButton, SIGNAL(clicked()), 
          this, SLOT(addCd())); 
  ... 
  connect(quitButton, SIGNAL(clicked()), 
          this, SLOT(close())); 
  connect(cdTable, SIGNAL(currentChanged(QSqlRecord *)), 
          this, SLOT(currentCdChanged(QSqlRecord *))); 
  connect(cdTable, SIGNAL(doubleClicked(int, int, int, const QPoint &)), 
          this, SLOT(editCd())); 
  ... 
} 
      
Здесь настраивается остальная часть пользовательского интерфейса и создаются необходимые соединения "сигнал-слот".
void MainForm::addCd() 
{ 
  CdForm form(this); 
  if (form.exec()) { 
    cdTable->refresh(); 
    trackTable->refresh(); 
  } 
}
      
Когда пользователь нажимает на кнопку Add, вызывается модальный диалог CdForm и, если пользователь в этом диалоге нажмет на кнопку Update, выполняется обновление таблиц QDataTable.
void MainForm::editCd() 
{ 
  QSqlRecord *record = cdTable->currentRecord(); 
  if (record) { 
    CdForm form(record->value("id").toInt(), this); 
    if (form.exec()) { 
      cdTable->refresh(); 
      trackTable->refresh(); 
    } 
  } 
}
      
Когда пользователь нажимает на кнопку Edit, вызывается модальный диалог CdForm, конструктору которого, передается идентификатор текущего компакт диска. В этом случае диалог запускается с заполненными полями, соответствующими заданному CD.

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

void MainForm::deleteCd() 
{ 
  QSqlRecord *record = cdTable->currentRecord();
  
  if (record) { 
    QSqlQuery query; 
    query.exec("DELETE FROM track WHERE cdid = " 
               + record->value("id").toString()); 
    query.exec("DELETE FROM cd WHERE id = " 
               + record->value("id").toString()); 
    cdTable->refresh(); 
    trackTable->refresh(); 
  } 
}
      
Когда пользователь нажимает на кнопку Delete, выполняется удаление всех дорожек диска из таблицы track, после чего удаляется запись из таблицы cd. В завершение обновляются обе таблицы-виджеты.
void MainForm::currentCdChanged(QSqlRecord *record) 
{ 
  trackTable->setFilter("cdid = " 
                        + record->value("id").toString()); 
  trackTable->refresh(); 
}
      
Слот currentCdChanged() связан с сигналом currentChanged() объекта cdTable, который выдается, когда пользователь вносит изменения в текущую запись о CD или перемещается к другой записи. Всякий раз, когда это происходит, вызывается функция setFilter() и обновляется таблица trackTable. Таким образом она всегда будет отображать только те дорожки, которые относятся к текущему CD.

По сути -- это весь код, реализующий функциональность MainForm. Однако тут следует упомянуть об одном небольшом улучшении, которое можно добавить. Суть его заключается в том, чтобы показывать длительность звучания дорожки не в секундах (например, "155"), как это делается сейчас, а в минутах и секундах (например, "02:35"). С этой целью необходимо создать производный класс от QSqlCursor и перекрыть в нем метод calculateField(), для преобразования значения поля duration в QString с заданным форматом представления:

QVariant TrackSqlCursor::calculateField(const QString &name) 
{ 
  if (name == "duration") { 
    int duration = value("duration").toInt(); 
    return QString("%1:%2").arg(duration / 60, 2) 
                           .arg(duration % 60, 2); 
  } 
  return QVariant(); 
}
      
Кроме того, в этом случае необходимо вызвать метод курсора setCalculated("duration", true), чтобы QDataTable использовала значение поля duration, возвращаемое функцией calculateField().


12.3. Разработка форм, ориентированных на работу с базами данных.

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

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

  1. Создаются виджеты-редакторы (QLineEdit, QComboBox, QSpinBox и т.п.) для каждого из полей записи.

  2. Создается экземпляр QSqlCursor.

  3. Создается экземпляр QSqlForm.

  4. Выполняется настройка QSqlForm, которая заключается в связывании полей записи с виджетами.

  5. Вызывается метод QSqlForm::readFields(), который заполняет виджеты данными.

  6. Диалог выводится перед пользователем.

  7. По завершении работы диалога, Вызывается метод QSqlForm::writeFields(), чтобы скопировать измененные значения обратно в базу данных.

Все эти действия мы продемонстрируем на примере диалога CdForm. Он предназначен для создания и изменения записи о компакт диске. Пользователь может задать название диска, исполнителя и год выпуска, а так же название и продолжительность каждого произведения на диске.

Рисунок 12.4. Диалог CdForm.


Начнем с определения класса:
class CdForm : public QDialog 
{
  Q_OBJECT 
public: 
  CdForm(QWidget *parent = 0, const char *name = 0); 
  CdForm(int id, QWidget *parent = 0, const char *name = 0); 
  ~CdForm(); 
  
protected slots: 
  void accept(); 
  void reject(); 
  
private slots: 
  void addNewArtist(); 
  void moveTrackUp(); 
  void moveTrackDown(); 
  void beforeInsertTrack(QSqlRecord *buffer); 
  void beforeDeleteTrack(QSqlRecord *buffer); 
  
private: 
  void init(); 
  void createNewRecord(); 
  void swapTracks(int trackA, int trackB); 
  
  QLabel *titleLabel; 
  QLabel *artistLabel;
  ...
  QDataTable *trackTable; 
  QSqlForm *sqlForm; 
  QSqlCursor *cdCursor; 
  QSqlCursor *trackCursor; 
  int cdId; 
  bool newCd; 
};
      
В классе объявлены два конструктора: один вставляет новую запись в базу данных, другой -- обновляет существующую запись. Слоты accept() и reject() унаследованы от QDialog.
CdForm::CdForm(QWidget *parent, const char *name) 
    : QDialog(parent, name) 
{ 
  setCaption(tr("Add a CD")); 
  cdId = -1; 
  init(); 
}
      
Первый конструктор записывает в заголовок диалога строку "Add a CD" ("добавить диск") и вызывает приватную функцию init(), которая выполняет остальную часть работы.
CdForm::CdForm(int id, QWidget *parent, const char *name) 
    : QDialog(parent, name) 
{ 
  setCaption(tr("Edit a CD")); 
  cdId = id; 
  init(); 
}
      
Второй конструктор записывает в заголовок диалога строку "Edit a CD" ("изменить сведения о диске") и так же вызывает функцию init().
void CdForm::init() 
{ 
  db = QSqlDatabase::database("CD"); 
  db->transaction(); 
  if (cdId == -1) 
    createNewRecord();
      
В функции init() запускается транзакция для соединения под именем "CD". Для диалогов CdForm и ArtistForm используются различные соединения, поскольку оба они могут отображаться одновременно и при этом нельзя допустить, чтобы операция Cancel в одной форме выполняла откат транзакции, запущенной в другой форме.

Если идентификатор диска не задан, вызывается функция createNewRecord(), которая вставляет пустую запись в таблицу. Это позволит использовать cdId как внешний ключ для QDataTable с дорожками. Если пользователь нажмет на кнопку Cancel, все изменения, произведенные в контексте транзакции, будут отменены, в том числе и операция вставки новой записи.

В этом диалоге используется еще одно соединение с базой данных, отличное от того, что используется в ArtistForm. Сделано это так потому, что в соединении, активной может быть только одна транзакция, а в данном приложении может сложиться ситуация, когда потребуется иметь две активных транзакции одновременно, например, в случае, когда пользователь нажимает кнопку Add New и открывает диалог ArtistForm.

  titleLabel = new QLabel(tr("&Title:"), this); 
  artistLabel = new QLabel(tr("&Artist:"), this); 
  yearLabel = new QLabel(tr("&Year:"), this); 
  titleLineEdit = new QLineEdit(this); 
  yearSpinBox = new QSpinBox(this); 
  yearSpinBox->setRange(1900, 2100); 
  yearSpinBox->setValue(QDate::currentDate().year()); 
  artistComboBox = new ArtistComboBox(db, this); 
  artistButton = new QPushButton(tr("Add &New..."), this); 
  ... 
  cancelButton = new QPushButton(tr("Cancel"), this);
      
На форме диалога размещаются текстовые метки, поле ввода, счетчик, выпадающий список и кнопки. Выпадающий список принадлежит к классу ArtistComboBox, о котором мы поговорим немного ниже.
  trackCursor = new QSqlCursor("track", true, db); 
  trackTable = new QDataTable(trackCursor, false, this); 
  trackTable->setFilter("cdid = " + QString::number(cdId)); 
  trackTable->setSort(trackCursor->index("number")); 
  trackTable->addColumn("title", tr("Track")); 
  trackTable->addColumn("duration", tr("Duration")); 
  trackTable->refresh();
      
Далее создается и настраивается QDataTable, которая позволит пользователю просматривать и изменять сведения о дорожках на текущем компакт диске. Очень напоминает то, что мы делали с классом ArtistForm, в предыдущем разделе.
  cdCursor = new QSqlCursor("cd", true, db); 
  cdCursor->select("id = " + QString::number(cdId)); 
  cdCursor->next();
      
Создается QSqlCursor и текущей, для него, делается запись, содержащая идентификатор требуемого диска.
  QSqlPropertyMap *propertyMap = new QSqlPropertyMap; 
  propertyMap->insert("ArtistComboBox", "artistId"); 
  sqlForm = new QSqlForm(this); 
  sqlForm->installPropertyMap(propertyMap); 
  sqlForm->setRecord(cdCursor->primeUpdate()); 
  sqlForm->insert(titleLineEdit, "title"); 
  sqlForm->insert(artistComboBox, "artistid"); 
  sqlForm->insert(yearSpinBox, "year"); 
  sqlForm->readFields();
      
Класс QSqlPropertyMap хранит сведения, благодаря которым QSqlForm "знает" -- значения какого типа, в каком свойстве, может хранить тот или иной виджет-редактор. Класс QSqlForm уже "знает", что QLineEdit запоминает свое значение в свойстве text, а QSpinBox -- в свойстве value. Но он ничего не знает о нестандартных виджетах, коим является ArtistComboBox. Поэтому мы должны вставить название класса и имя свойства класса ("ArtistComboBox", "artistId") в карту свойств и вызвать installPropertyMap(), чтобы указать QSqlForm, что при работе с виджетом класса ArtistComboBox следует использовать свойство artistId.

Кроме того экземпляру класса QSqlForm нужно передать буфер, с которым он будет работать, а так же сообщить о том, какой виджет, какому полю в таблице соответствует. В заключение, вызовом readFields(), данные считываются из базы и переносятся в виджеты.

  connect(artistButton, SIGNAL(clicked()), 
          this, SLOT(addNewArtist())); 
  connect(moveUpButton, SIGNAL(clicked()), 
          this, SLOT(moveTrackUp())); 
  connect(moveDownButton, SIGNAL(clicked()), 
          this, SLOT(moveTrackDown())); 
  connect(updateButton, SIGNAL(clicked()), 
          this, SLOT(accept())); 
  connect(cancelButton, SIGNAL(clicked()), 
          this, SLOT(reject())); 
  connect(trackTable, SIGNAL(beforeInsert(QSqlRecord *)), 
          this, SLOT(beforeInsertTrack(QSqlRecord *))); 
  ... 
}
      
На последней стадии выполнения функции производится соединение сигналов и слотов, которые будут описаны несколько ниже.
void CdForm::accept() 
{ 
  sqlForm->writeFields(); 
  cdCursor->update(); 
  db->commit(); 
  QDialog::accept(); 
}
      
Когда пользователь нажимает на кнопку Update, производится запись новых значений полей в буфер редактирования объекта QSqlCursor. Затем выполняется SQL-предложение UPDATE, вызовом функции update(), вызовом commit() подтверждается транзакция и в заключение вызывается метод accept(), унаследованный от QDialog.
void CdForm::reject() 
{ 
  db->rollback(); 
  QDialog::reject(); 
}
      
Когда пользователь нажимает на кнопку Cancel, производится откат произведенных изменений и форма диалога закрывается.
void CdForm::addNewArtist() 
{ 
  ArtistForm form(this); 
  if (form.exec()) { 
    artistComboBox->refresh(); 
    updateButton->setEnabled(artistComboBox->count() > 0); 
  } 
}
      
Когда пользователь нажимает на кнопку Add New, запускается модальный диалог ArtistForm. Этот диалог позволяет добавлять нового исполнителя в базу данных, удалять или изменять сведения об исполнителях. Если пользователь нажмет кнопку Update, будет вызвана функция ArtistComboBox::refresh(), которая обновит список исполнителей в виджете.

Если в списке нет ни одного исполнителя, кнопка Update будет запрещена, поскольку необходимо избежать создания записи о CD, без указания имени исполнителя.

void CdForm::beforeInsertTrack(QSqlRecord *buffer) 
{ 
  buffer->setValue("id", generateId("track", db)); 
  buffer->setValue("number", trackCursor->size() + 1); 
  buffer->setValue("cdid", cdId); 
}
      
Слот beforeInsertTrack() связан с сигналом beforeInsert(). Он заполняет поля id, number и cdid.
void CdForm::beforeDeleteTrack(QSqlRecord *buffer) 
{ 
  QSqlQuery query(db); 
  query.exec("UPDATE track SET number = number - 1 " 
             "WHERE track.number > " 
             + buffer->value("number").toString()); 
}
      
Слот beforeDeleteTrack() связан с сигналом beforeDelete(). Он выполняет перенумерацию дорожек на диске, чьи номера больше номера удаляемой дорожки, чтобы сохранить неразрывность последовательности номеров дорожек. Например, допустим, что диск содержит 6 дорожек и пользователь удаляет 4-ю, тогда 5-я дорожка получит номер 4, а 6-я -- 5.

Имеется еще 4 функции, описание которых мы не привели: moveTrackUp(), moveTrackDown(), swapTracks() и createNewRecord(). Они совершенно необходимы, чтобы сделать приложение более-менее удобным, но их реализация не содержит ничего нового для вас, поэтому мы не будем их рассматривать. Исходные тексты функций вы найдете на CD, сопровождающем книгу.

Теперь, после того как мы рассмотрели все классы диалогов в приложении, можно перейти к описанию нестандартного класса ArtistComboBox. Как обычно, начнем с определения класса:

class ArtistComboBox : public QComboBox 
{ 
  Q_OBJECT 
  Q_PROPERTY(int artistId READ artistId WRITE setArtistId) 
public: 
  ArtistComboBox(QSqlDatabase *database, QWidget *parent = 0, 
                 const char *name = 0); 
  void refresh(); 
  int artistId() const; 
  void setArtistId(int id); 
  
private: 
  void populate(); 
  
  QSqlDatabase *db; 
  QMap<int, int> idFromIndex; 
  QMap<int, int> indexFromId; 
};
      
Класс ArtistComboBox порожден от класса QComboBox. В него добавлено свойство artistId и несколько функций.

В приватной секции объявлены две переменные-члены типа QMap<int, int>. Первая отвечает за соответствие идентификатора исполнителя индексу в списке виджета. Вторая отвечает за соответствие индекса в списке -- идентификатору исполнителя.

ArtistComboBox::ArtistComboBox(QSqlDatabase *database, 
                               QWidget *parent, const char *name) 
    : QComboBox(parent, name) 
{ 
  db = database; 
  populate(); 
}
      
В конструкторе вызывается функция populate(), которая заполняет список виджета именами и идентификаторами из таблицы artist.
void ArtistComboBox::refresh() 
{ 
  int oldArtistId = artistId(); 
  clear(); 
  idFromIndex.clear(); 
  indexFromId.clear(); 
  populate(); 
  setArtistId(oldArtistId); 
}
      
Функция refresh() очищает и повторно заполняет список виджета самыми свежими данными из базы. При этом, после обновления списка выбранным остается тот же исполнитель, который был выбран до обновления.
void ArtistComboBox::populate() 
{ 
  QSqlCursor cursor("artist", true, db); 
  cursor.select(cursor.index("name")); 
  
  int index = 0; 
  while (cursor.next()) { 
    int id = cursor.value("id").toInt(); 
    insertItem(cursor.value("name").toString(), index); 
    idFromIndex[index] = id; 
    indexFromId[id] = index; 
    ++index; 
  } 
}
      
Функция populate() переносит список исполнителей из базы данных в список виджета, попутно обновляя словари idFromIndex и indexFromId.
int ArtistComboBox::artistId() const 
{ 
  return idFromIndex[currentItem()]; 
}
      
Функция artistId() возвращает идентификатор текущего исполнителя.
void ArtistComboBox::setArtistId(int id) 
{ 
  if (indexFromId.contains(id)) 
    setCurrentItem(indexFromId[id]); 
}
      
Функция setArtistId() делает текущим исполнителя с заданным идентификатором.

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

Закончим обзор приложения "CD Collection" рассмотрением реализации функций createConnections() и main().

inline bool createOneConnection(const QString &name) 
{ 
  QSqlDatabase *db; 
  if (name.isEmpty()) 
    db = QSqlDatabase::addDatabase("QSQLITEX"); 
  else 
    db = QSqlDatabase::addDatabase("QSQLITEX", name); 
  db->setDatabaseName("cdcollection.dat"); 
  if (!db->open()) { 
    db->lastError().showMessage(); 
    return false; 
  } 
  return true; 
}

inline bool createConnections() 
{ 
  return createOneConnection("") 
          && createOneConnection("ARTIST") 
          && createOneConnection("CD"); 
}
      
Функция createConnections() создает три идентичных соединения с базой данных. Первое соединение создается безымянным, оно будет использоваться по-умолчанию, когда имя соединения не задано явно. Два других соединения создаются с именами "ARTIST" и "CD" -- они используются диалогами ArtistForm и CdForm, соответственно.
int main(int argc, char *argv[]) 
{ 
  QApplication app(argc, argv); 
  if (!createConnections()) 
    return 1; 
  
  MainForm mainForm; 
  app.setMainWidget(&mainForm); 
  mainForm.resize(480, 320); 
  mainForm.show(); 
  return app.exec(); 
}
      
Функция main() практически ничем не отличается от аналогичных функций, которые мы до сих пор видели, за одним маленьким исключением -- она вызывает createConnections().

Как мы уже говорили в конце предыдущего раздела, приложение можно было бы несколько улучшить, если показывать длительность звучания дорожки не в секундах, а в минутах и секундах. Но, кроме перекрытия метода QSqlCursor::calculateField(), это повлекло бы за собой еще и создание класса -- редактора времени звучания дорожки, производного от QSqlEditorFactory. А затем необходимо было бы с помощью QSqlPropertyMap сообщить QDataTable о том, как получить измененное значение от класса-редактора. За дополнительной информацией обращайтесь к сопроводительной документации по функциям installEditorFactory() и installPropertyMap(), класса QDataTable.

Еще одно из возможных улучшений приложения -- добавить возможность хранения в базе данных изображения обложки диска и отображения его в CdForm. Реализовать это можно за счет добавления в базу данных поля типа BLOB, в котором можно хранить изображения. Получать изображения из базы данных можно в виде QByteArray и затем передавать их в конструктор QImage.


Предыдущая глава | Содержание | Следующая глава


Ж. Бланшетт, М. Саммерфильд, "Глава 12. Базы данных" - 01/12/2004

г.ВИТЕБСК
РЕСПУБЛИКА БЕЛАРУСЬ

 
  О ресурре... Главная
[ ГЛАВНАЯ ] [ СОДЕРЖАНИЕ ]

E-mail : БИРЛ "Ombrello".

Hosted by uCoz