主页 | 所有的类 | 主要的类 | 注释的类 | 分组的类 | 函数 |
SQL模块
这个模块是Qt企业版的一部分。
介绍
Qt的SQL类帮助你给你的Qt应用程序提供无缝的数据库集成。
这个概述假设你已经至少有一些SQL的基础知识。你需要能够理解简单的SELECT、INSERT、UPDATE和DELETE命令。尽管QSqlCursor类提供了数据库浏览和编辑的界面不需要SQL的知识,但是对于SQL的基本理解还是被高度推荐的。一本SQL数据库的标准文本是C. J. Date所作的《数据库系统的介绍》(第七版),ISBN 0201385902。
本文所描述的实例是从完全人工编写代码的角度出发的,你还可以参考Qt设计器的用户手册中“创建数据库应用程序”这一节,它提供了更高层次的实现方法来演示窗口部件、执行挖掘和处理外键查找等相关的操作。
这份文档被分为六个部分:
SQL模块架构。描述了这些类是如何结合在一起的。
连接数据库。这部分解释了如何使用QSqlDatabase类建立数据库连接。
执行SQL命令。描述如何执行标准的数据操作命令,比如SELECT、INSERT、UPDATE和DELETE。(当然任何有效的SQL指令均可被送往数据库)。焦点在于用QSqlQuery进行交互处理。
使用游标。描述如何使用QSqlCursor类来实现更为强大的应用编程接口函数而不象在QSqlQuery中执行SQL语句。
基于数据的窗口部件。描述如何把数据库与用户接口链接起来。在这一节中我们介绍QDataTable、QSqlForm、QSqlPropertyMap和QSqlEditorFactory类,演示如何使用自定义的基于数据的窗口部件。Qt设计器提供了一个方便的可视化途径来实现同样的事情。请参考Qt设计器的手册,更多的详细情况请见QDataBrowser和QDataView。
继承QSqlCursor。本节给出了继承QSqlCursor的实例。继承类可为字段提供默认值与计算字段等功能(比如以一个自动数值为主键的索引列),以及显示计算字段等等的内容。比如显示一个名称而不是一个外键的标识符。
这个文档中所使用的表的定义的所有实例都在实例表部分。
SQL模块架构
SQL类分成以下三层:
用户接口层。这些类提供了基于数据的窗口部件,这些窗口部件不仅连接数据库还可为用户所浏览。(以QSqlCursor作为数据源)。终端用户通过这些组件来浏览与编辑数据。Qt设计器集成了这些类并可用来创建基于数据的窗体。这些窗口部件也可在程序中与你的C++代码直接交互。支持这一层的类包括QSqlEditorFactory、QSqlForm、QSqlPropertyMap、QDataTable、QDataBrowser和QDataView。
SQL应用编程接口层。这些类存取数据库。QSqlDatabase类用来连接数据库。数据交互的实现要么通过QSqlQuery类以SQL语句来实现,要么用QSqlCursor类,它封装了SQL命令集。除了QSqlDatabase、QSqlCursor和QSqlQuery这些类外,QSqlError、QSqlField、QSqlIndex和QSqlRecord也支持该层。
驱动程序层。本层由三个类组成:QSqlResult、QSqlDriver和QSqlDriverFactoryInterface。这个层在数据库和SQL类之间提供了底层的桥梁。这个层的文档是另外的,因为它只和驱动程序编写者有关,并且在标准数据库应用程序编程中很少用到。对于实现Qt的SQL驱动程序插件的详细情况请参考这里。
SQL驱动程序插件
Qt的SQL模块可以在运行期动态地装载新的驱动程序,这是通过插件实现的。
SQL驱动程序文档描述了如何为一个特定的数据库建立相应的插件。
一旦该插件被建立,Qt可以自动地装载它,之后就可为QSqlDatabase所用(详细信息请参考QSqlDatabase::drivers())。
连接数据库
如果要使用QSqlQuery或QSqlCursor类,则少要连接并打开一个数据库连接。
如果应用程序只需要一个数据库连接,QSqlDatabase类可以创建一个为所有SQL操作所利用的默认连接,如果需要多个数据库连接,每个连接也可以容易地建立。
QSqlDatabase需要qsqldatabase.h这个头文件。
连接单一数据库
创建一个数据库连接需要三个操作:激活驱动程序、设置连接信息、打开连接。
#include <qapplication.h> #include <qsqldatabase.h> #include "../login.h" int main( int argc, char *argv[] ) { QApplication app( argc, argv ); QSqlDatabase *defaultDB = QSqlDatabase::addDatabase( DB_SALES_DRIVER ); if ( defaultDB ) { defaultDB->setDatabaseName( DB_SALES_DBNAME ); defaultDB->setUserName( DB_SALES_USER ); defaultDB->setPassword( DB_SALES_PASSWD ); defaultDB->setHostName( DB_SALES_HOST ); if ( defaultDB->open() ) { // 数据库被成功打开,我们现在可以运行SQL命令。 } } return 0; }
在以上代码中我们先通过QSqlDatabase::addDatabase()激活了一个数据库驱动程序,传入了驱动程序名。现在可利用的驱动程序为:QODBC3(开放数据库连接)、QOCI8(Oracle 8和9)、QTDS7(Sybase Adaptive Server和Microsoft SQL Server)、QPSQL7(PostgreSQL 6和7)和QMYSQL3(MySQL)。请注意部分驱动程序并未包括在Qt的自由版中,详细情款请查看README文件。
连接创建后就成为应用程序的默认数据库连接并可为Qt的SQL类所使用。
其次我们调用了setDatabaseName()、setUserName()、setPassword()和setHostName()来初使化连接信息。对于QOCI8(Oracle 8和9),TNS服务名称必须由setDatbaseName()传递。当连接ODBC数据源时,数据源名称(DSN)必须在setDatabaseName()调用中使用。
最后我们调用了open()来打开数据库以存取数据,如果该步失败将返回假,用户可通过QSqlDatabase::lastError()来查明错误信息。
连接多个数据库
连接至多个数据库需要在使用QSqlDatabase::addDatabase()传入两个参数,其中第二个参数用来指明这个连接的唯一标识符。
#include <qapplication.h> #include <qsqldatabase.h> #include "../login.h" bool createConnections(); int main( int argc, char *argv[] ) { QApplication app( argc, argv ); if ( createConnections() ) { // 数据库被成功打开,得到它们的指针: QSqlDatabase *oracledb = QSqlDatabase::database( "ORACLE" ); // 现在我们可以在oracle连接或默认连接上执行SQL命令 } return 0; } bool createConnections() { // 创建默认数据库连接 QSqlDatabase *defaultDB = QSqlDatabase::addDatabase( DB_SALES_DRIVER ); if ( ! defaultDB ) { qWarning( "Failed to connect to driver" ); return FALSE; } defaultDB->setDatabaseName( DB_SALES_DBNAME ); defaultDB->setUserName( DB_SALES_USER ); defaultDB->setPassword( DB_SALES_PASSWD ); defaultDB->setHostName( DB_SALES_HOST ); if ( ! defaultDB->open() ) { qWarning( "Failed to open sales database: " + defaultDB->lastError().driverText() ); qWarning( defaultDB->lastError().databaseText() ); return FALSE; } // 创建一个名为oracle的连接 QSqlDatabase *oracle = QSqlDatabase::addDatabase( DB_ORDERS_DRIVER, "ORACLE" ); if ( ! oracle ) { qWarning( "Failed to connect to oracle driver" ); return FALSE; } oracle->setDatabaseName( DB_ORDERS_DBNAME ); oracle->setUserName( DB_ORDERS_USER ); oracle->setPassword( DB_ORDERS_PASSWD ); oracle->setHostName( DB_ORDERS_HOST ); if ( ! oracle->open() ) { qWarning( "Failed to open orders database: " + oracle->lastError().driverText() ); qWarning( oracle->lastError().databaseText() ); return FALSE; } return TRUE; }
在这个实例中我们打开了两个连接并加入了错误处理。静态函数QSqlDatabase::database()可以在任何地方调用以获得一个数据库连接的指针。如果我们在调用此函数时未使用参数则返回默认连接,如果有参数则返回指定的连接,如上例中的“ORACLE”。
如果你使用Qt设计器来创建一个main.cpp,它将不会包含我们的实例createConnections()函数。这也就是说在Qt设计器中预览正确的应用程序将不会运行,除非你实现了你自己的数据库连接函数。
请注意以上的代码上ODBC连接没有被命名,因此被作为默认连接。QSqlDatabase维护着通过addDatabase()这个静态函数返回的的连接指针。如果有移去一个连接,先调用QSqlDatabase::close()来关闭连接,然后通过静态函数QSqlDatabase::removeDatabase()来移除连接。
使用QSqlQuery执行SQL命令
QSqlQuery类提供了一个接口来执行SQL命令。它也提供相应的函数来定位SELECT查询的数据集和检索的不同的记录和字段值。
QSqlCursor类在下一节中讲述。它为我们提供了更高层的接口来实现SQL命令。QSqlCursor特别适用于在屏幕上显示的可视窗口部件。不熟悉SQL的程序员可略过这一节,直接去阅读“使用 QSqlCursor”来了解QSqlCursor类。
事务处理
如果下面的数据库引擎支持事务处理,则函数QSqlDriver::hasFeature(QSqlDriver::Transactions)将返回 真。你可以通过调用QSqlDatabase::transaction()来初始化一个事务处理。之后执行你想在该事务处理的工作。完了再执行QSqlDatabase::commit()来提交事务处理或QSqlDatabase::rollback()取消事务处理。
基本浏览
#include <qapplication.h> #include <qsqldatabase.h> #include <qsqlquery.h> #include "../login.h" bool createConnections(); int main( int argc, char *argv[] ) { QApplication app( argc, argv ); if ( createConnections() ) { QSqlDatabase *oracledb = QSqlDatabase::database( "ORACLE" ); // 从oracle数据库向ODBC(默认)数据库复制数据 QSqlQuery target; QSqlQuery query( "SELECT id, name FROM people;", oracledb ); if ( query.isActive() ) { while ( query.next() ) { target.exec( "INSERT INTO people ( id, name ) VALUES ( " + query.value(0).toString() + ", '" + query.value(1).toString() + "' );" ); } } } return 0; }
在以上实例中我们加入了一个头文件:qsqlquery.h. 我们使用缺省数据库创建了一个查询:target,其初始值是空的。我们使用“ORACLE”数据库创建了第二个查询以存取数据。数据连接的设置在createConnections()中完成。
在创建了一个SELECT语句后,函数isActive()被用来检测该命令是否被成功执行。之后的next()被用来遍历整个数据集。value()函数返回字段中的值,其数据类型为QVariants。插入操作通过先前就已创建的target QSqlQuery来完成。
注意这个实例和这份文档中所有的实例使用的表的定义都在实例表中。
int count = 0; if ( query.isActive() ) { while ( query.next() ) { target.exec( "INSERT INTO people ( id, name ) VALUES ( " + query.value(0).toString() + ", '" + query.value(1).toString() + "' );" ); if ( target.isActive() ) count += target.numRowsAffected(); } }
以上的代码来统计有多少记录被成功地插入数据库。如果操作,比如插入,失败则isActive()将返回假,如果记录行的数量不能被决定,比如查询失败,numRowsAffected()将返回-1。
基本数据处理
int main( int argc, char *argv[] ) { QApplication app( argc, argv ); int rows = 0; if ( createConnections() ) { QSqlQuery query( "INSERT INTO staff ( id, forename, surname, salary ) " "VALUES ( 1155, 'Ginger', 'Davis', 50000 );" ); if ( query.isActive() ) rows += query.numRowsAffected() ; query.exec( "UPDATE staff SET salary=60000 WHERE id=1155;" ); if ( query.isActive() ) rows += query.numRowsAffected() ; query.exec( "DELETE FROM staff WHERE id=1155;" ); if ( query.isActive() ) rows += query.numRowsAffected() ; } return ( rows == 3 ) ? 0 : 1; }
这个实例演示了直接的SQL DML(数据操作语言)命令。既然我们没有在QSqlQuery的构造函数中指明数据库则默认数据库将被使用。QSqlQuery也可以用来执行SQL DDL(数据定义语言)命令,诸如CREATE TABLE与CREATE INDEX。
定位数据集
一旦SELECT查询语句被成功执行,我们就可以存取与之对应的结果集。我们先前已使用了一个浏览函数:next()。它可以顺序扫描整个记录集。QSqlQuery同时也提供了first()、last()、next()和prev()。执行其中的任意一个函数后我们都可以通过isValid()来检验是否成功执行。
我们也可以通过seek()来定位至任意一条记录。第一条记录号为零。最后一条记录号为记录的总数减一。请注意并不时所有的数据都为SELECT查询提供记录总数这样的信息,此时size()将返回-1。
if ( createConnections() ) { QSqlQuery query( "SELECT id, name FROM people ORDER BY name;" ); if ( ! query.isActive() ) return 1; // 查询失败 int i; i = query.size(); // 在这个实例中我们有9个记录,i==9。 query.first(); // 移动到第一条记录。 i = query.at(); // i==0 query.last(); // 移动到最后一条记录。 i = query.at(); // i==8 query.seek( query.size() / 2 ); // 移动到中间那条记录。 i = query.at(); // i==4 }
以上实例显示了一些定位函的用法。
注意不是所有的驱动程序都支持size(),我们可以通过以下的示例来获知驱动程序是否支持这一特征:
QSqlDatabase* defaultDB = QSqlDatabase::database(); if ( defaultDB->driver()->hasFeature( QSqlDriver::QuerySize ) ) { // QSqlQuery::size()支持 } else { // QSqlQuery::size()不能被依赖 }
当我们定位了一定记录录后我们往往希望取出这条记录的内容。
if ( createConnections() ) { QSqlQuery query( "SELECT id, surname FROM staff;" ); if ( query.isActive() ) { while ( query.next() ) { qDebug( query.value(0).toString() + ": " + query.value(1).toString() ); } } }
注意如果你只是希望扫描整个记录集你只需不断地进行next()操作。
提示:函数lastQuery()返回最后一次的的Sql语句,有时你可能想检查一下最后那次操作是否是你所想象中的操作。
使用QSqlCursor
QSqlCursor类提供了一个更高层次的接口来浏览与编辑记录,它不需要你写SQL语句。
QSqlCursor几乎可以做QSqlQuery所能做的所有事情。有两点例外:由于其代表的是数据库中的一张表或视图。默认情况下 当你浏览记录时QSqlCursor对象总是取出记录中的所有字段。如果你只关心其中的一部分字段你需要进行一些配置,或者使用QSqlRecord::setGenerated()来屏蔽掉一些字段。当然你也可以使用QSqlQuery来完成这个功能。 如果表或者视图有其唯一索引你可以通过QSqlCursor来编辑记录。否则就只能通过QSqlQuery来编辑了。(请注意不是所有的数据库支持可编辑的视图。)
QSqlCursor一次只操作一条记录。无论是执行insert、update还是 delete,QSqlCursor都只是影响一条记录。当浏览记录集时无论何时都只有一条记录处于应用状态。也就是说QSqlCursor只维护了一条记录的“可编辑缓冲区”。“定位缓冲区”单独放置,因此当你浏览记录时该缓冲并不会受其影响。
当我们使用QSqlCursor对象前我们必须先创建与打开一个数据库连接。数据库连接在上面的连接数据库一节中已经被讲述。在实例中我们假设已经通过createConnections()创建了连接如同在QSqlDatabase实例描述的那样。
在随后的基于数据的窗口部件一节中我们将展示如何连接窗口部件至数据库,当我们了解了游标和基于数据的窗口部件后我们就可以讨论继承QSqlCursor。
QSqlCursor类需要qsqlcursor.h这个头文件。
检索记录
#include <qapplication.h> #include <qsqldatabase.h> #include <qsqlcursor.h> #include "../login.h" bool createConnections(); int main( int argc, char *argv[] ) { QApplication app( argc, argv ); if ( createConnections() ) { QSqlCursor cur( "staff" ); // 指定表/视图名称 cur.select(); // 我们将检索每一条记录 while ( cur.next() ) { qDebug( cur.value( "id" ).toString() + ": " + cur.value( "surname" ).toString() + " " + cur.value( "salary" ).toString() ); } } return 0; }
我们指定表或视图的名字来创建QSqlCursor对象,如果我们使用的不是缺省的数据库我们需要在QSqlCursor的构造函数中指明。
cur.select()将自动执行以下的Sql语句:
SELECT staff.id, staff.forename, staff.surname, staff.salary, staff.statusid FROM staff
然后我们通过cur.next()来遍历整个记录集。获取字段值的方法与QSqlQuery类似,不过它指明的是字段名而不是value()和 setValue()的数字索引。
排序和过滤记录
如果我们需要取记录子集,我们可以在select()函数中指定过滤条件,随后取到的结果集就都是满足条件的记录(过滤条件在SQL语言中由WHERE负责))。
cur.select( "id > 100" );
这个select()调用将执行SQL命令:
SELECT staff.id, staff.forename, staff.surname, staff.salary, staff.statusid FROM staff WHERE staff.id > 100
这将检索出id大于100的职员。
与过滤类似我们也可以对指定记录集进行排序。这个功能需要通过QSqlIndex对象来实现,它可以包括排序的字段的名称并且可以传递这个对象给select()调用。
QSqlCursor cur( "staff" ); QSqlIndex nameIndex = cur.index( "surname" ); cur.select( nameIndex );
以上语句使用“surname”字段创建了一个QSqlIndex对象,当我们把该对象与select()函数集合起来以后,得到的记录集就是按staff.surname进行排序的,在索引对象中的每个字段通过ORDER BY组织起来,被执行的SQL语句如下所示:
SELECT staff.id, staff.forename, staff.surname, staff.salary, staff.statusid FROM staff ORDER BY staff.surname ASC
把过滤与排序结合起来的方法也是简单易懂的。
cur.select( "surname LIKE 'A%'", nameIndex );
过滤字符串引起WHERE操作,QSqlIndex对象引起ORDER BY的操作,最后的命令如下:
SELECT staff.id, staff.forename, staff.surname, staff.salary, staff.statusid FROM staff WHERE staff.surname LIKE 'A%' ORDER BY staff.surname ASC
如果需要对不止一个字段进行排序,则索引对象创建时可包含多个字段。顺序还是倒序也可以通过QSqlIndex::setDescending()来指定,默认是升序排序。
QSqlCursor cur( "staff" ); QStringList fields = QStringList() << "surname" << "forename"; QSqlIndex order = cur.index( fields ); cur.select( order ); while ( cur.next() ) {
以上代码中我们通过一个字符串队列来存放不止一个字段,排序中它们都将被用到。然后我们基于这些字段创建了QSqlIndex对象,最后的select()操作将执行以下语句:
SELECT staff.id, staff.forename, staff.surname, staff.salary, staff.statusid FROM staff ORDER BY staff.surname ASC, staff.forename ASC
你可以在多重排序中加入过滤条件。
QSqlCursor cur( "staff" ); QStringList fields = QStringList() << "id" << "forename"; QSqlIndex order = cur.index( fields ); QSqlIndex filter = cur.index( "surname" ); cur.setValue( "surname", "Bloggs" ); cur.select( filter, order ); while ( cur.next() ) {
这将执行以下的SQL语句:
SELECT staff.id, staff.forename, staff.surname, staff.salary, staff.statusid FROM staff WHERE staff.surname='Bloggs' ORDER BY staff.id ASC, staff.forename ASC
QSqlIndex对象“order”包含了两个字段:“id”和“forename”,它们被用来对记录集排序。QSqlIndex对象“filter”包含了一个字段:“surname”。当索引对象被作为过滤条件传给select()函数的时候,该对象中的字段名将被用来进行条件限制的列名,即fieldname=value。其中的value即是对应字段的限定值。我们可以用setValue()来确定每一个过滤字段的限定值。
提取数据
QSqlCursor cur( "creditors" ); QStringList orderFields = QStringList() << "surname" << "forename"; QSqlIndex order = cur.index( orderFields ); QStringList filterFields = QStringList() << "surname" << "city"; QSqlIndex filter = cur.index( filterFields ); cur.setValue( "surname", "Chirac" ); cur.setValue( "city", "Paris" ); cur.select( filter, order ); while ( cur.next() ) { int id = cur.value( "id" ).toInt(); QString name = cur.value( "forename" ).toString() + " " + cur.value( "surname" ).toString(); qDebug( QString::number( id ) + ": " + name ); }
在以上的实例中,我们为creditors表创建了一个游标,我们创建了两个QSqlIndex对象,第一个“order”创建于“orderFields”字串列,第二个“filter”创建于“filterFields”字串列,我们为过滤字段“surname”与“city”分别设定了条件值。现在我们调用select(),产生的SQL语句如下:
SELECT creditors.city, creditors.surname, creditors.forename, creditors.id FROM creditors WHERE creditors.surname = 'Chirac' AND creditors.city = 'Paris' ORDER BY creditors.surname ASC, creditors.forename ASC过滤字段用于WHERE语句,排序字段用于ORDER BY语句。
现在我们遍历整个符合条件的记录集(如果有的话),我们得到字段id、forename与surname的值并把它们传入我们需要处理的函数中去,在这个实例是一个简单的qDebug()调用。
处理记录
当一张表或视图有唯一索引的情况下,使用QSqlCursor可对表或视图的记录进行插入、更新与删除操作。否则只能用QSqlQuery来进行操作。(注意不是所有数据库都支持可编辑的视图。)
每一个游标有一个内部的编辑缓冲区用于编辑操作(insert、update和delete)。 编辑过程对于每种操作都是类似是:获取指向相关缓冲区的指针,调用setValue()来更新缓冲,调用insert()或update()或del()来执行所需要的操作。举例来说,当通过游标来插入记录时,你通过primeInsert()来获得一个指向编辑缓冲区的指针,调用setValue()来刷新缓冲区中每个字段的值,然后调用QSQlCursor::insert()把缓冲区的数据插入数据库。类似的,当刷新(或删除)一条记录的时候,缓冲区中的数据将刷新(或删除)至数据库。你调用任何一个游标定位的函数都不会影响缓冲区中的内容。请注意如果你调用setValue()传入的是一个单引号括起来的字符串,单引号将被忽略,因为单引号在SQL中有着特殊的意义。
primeInsert()、primeUpdate()和primeDelete()这样的方法都返回一个指向内部可编辑缓冲区的指针,每一种方法在返回前都会在可编辑缓冲区上执行一些不同的隐藏操作,默认情况下,QSqlCursor::primeInsert()清空缓冲区上所有字段的值。(请参考QSqlRecord::clearValues())。QSqlCursor::primeUpdate()和QSqlCursor::primeDelete()在缓冲区中初始化当前游标处的各个字段值,这三个函数都是虚函数,因些你可以重新定义它们的行为。(比如重载primeInsert()以便对自动数值型的字段作出处理)。用户界面的数据感知组件会发出信号,比如primeInsert(),这样就可以进行连接,这些操作都会传递一个指针至合适的缓冲,因些子数据操作可能并不需要。相应信息请见继承QSqlCursor。参考Qt设计器的用户手册可了解与primeInsert()有关的信号。
当你调用了insert()、update()或del()之后,游标将无法定位至一条有效的记录并且实际上是不可用的。如果你还想继续浏览记录的话你必须调用select()以便从数据库读取更改后的记录。
插入记录
QSqlCursor cur( "prices" ); QStringList names = QStringList() << "Screwdriver" << "Hammer" << "Wrench" << "Saw"; int id = 20; for ( QStringList::Iterator name = names.begin(); name != names.end(); ++name ) { QSqlRecord *buffer = cur.primeInsert(); buffer->setValue( "id", id ); buffer->setValue( "name", *name ); buffer->setValue( "price", 100.0 + (double)id ); count += cur.insert(); id++; }
在以上的实例中我们为“prices”表创建了一个游标,然后我们创建了一个产品名列表并进入了一个循环。在循环中我们调用了游标的primeInsert()方法。该方法返回了一个指向QSqlRecord缓冲区的指针,该缓冲区的所有字段均置为空值。(请注意QSqlCursor::primeInsert()是虚函数,可以在继承类中定制,请参考QSqlCursor)。然后我们调用setValue()来设置每一个字段,最后调用insert()来实际插入记录。insert()返回值告诉我们有多少记录被实际插入。
我们通过primeInsert()调用来获得指向QSqlRecord对象的指针。QSqlRecord对象存储一条记录的数据以及相关的中间数据。在实际的应用程序中大多数与QSqlRecord有关的操作只是简单进行value()与setValue()的调用。就象上面以及下面的实例一样。
更新记录
QSqlCursor cur( "prices" ); cur.select( "id=202" ); if ( cur.next() ) { QSqlRecord *buffer = cur.primeUpdate(); double price = buffer->value( "price" ).toDouble(); double newprice = price * 1.05; buffer->setValue( "price", newprice ); cur.update(); }
以上代码我们先在价格表上创建了一个游标。调用select()把需更新的记录选出来,通过next()进行定位。再调用primeUpdate()来获得一个指向QSqlRecord的指针,该缓冲区中的内容将被当前记录的数据所填充。我们获得当前字段的价格值再计算出一个新的价格。最后调用update()来更新记录,update()返回实际被更新的记录数。
如果有许多同样的更新需要执行,比如说更新一批价格,通过QSqlQuery来执行一条SQL语句的效率可能更高一些。
QSqlQuery query( "UPDATE prices SET price = price * 1.05" );
删除记录
QSqlCursor cur( "prices" ); cur.select( "id=999" ); if ( cur.next() ) { cur.primeDelete(); cur.del();
需要删除记录时,选择相应的记录然后定位,调用primeDelete()使缓冲区保存当前记录的值,然后调用QSqlCursor::del()来进行删除操作。
与更新操作一样,如果有多条类似的记录需要删除,使用一条SQL语句的效率可能更高一些。
QSqlQuery query( "DELETE FROM prices WHERE id >= 2450 AND id <= 2500" );
基于数据的窗口部件
基于数据的窗口部件通过简便而强有力的方法把数据与用户界面结合起来。使用Qt设计器来创建与维护基于数据的窗口部件是一件方便的事情。本节为那些致力于完全用编程语言实现下述实例的开发人员们做出一个导引。请注意在Qt设计器的手册中“创建数据库应用程序”这一节所讲述的应用以及附带的实例。
基于数据的表
#include <qapplication.h> #include <qsqldatabase.h> #include <qsqlcursor.h> #include <qdatatable.h> #include "../login.h" bool createConnections(); int main( int argc, char *argv[] ) { QApplication app( argc, argv ); if ( createConnections() ) { QSqlCursor staffCursor( "staff" ); QDataTable *staffTable = new QDataTable( &staffCursor, TRUE ); app.setMainWidget( staffTable ); staffTable->refresh(); staffTable->show(); return app.exec(); } return 0; }
基于数据的表需要qdatatable.h和qsqlcursor.h这两个头文件。我们创建应用程序对象,然后调用createConnections()并创建游标。之后使用游标的指针创建QDataTable对象,设置autoPopulate标志为TRUE。再把QDataTable设为主窗口部件,调用 refresh()来获取数据,调用show()使其为用户可见。
autoPopulate标志告诉QDataTable是否根据当前游标创建列。该标志并不影响数据的的读入。数据的读入是由refresh()完成的。
QSqlCursor staffCursor( "staff" ); QDataTable *staffTable = new QDataTable( &staffCursor ); app.setMainWidget( staffTable ); staffTable->addColumn( "forename", "Forename" ); staffTable->addColumn( "surname", "Surname" ); staffTable->addColumn( "salary", "Annual Salary" ); QStringList order = QStringList() << "surname" << "forename"; staffTable->setSort( order ); staffTable->refresh(); staffTable->show();
在以上代码中我们创建了一个空的QDataTable,然后人工指定将显示的列,每一列我们都可以指定显示的标题。
我们可以在表中对记录进行排序,或者也可以让设置游标让其完成排序工作。
当所有设置工作完成后就可以调用refresh()以便从数据库中获得数据,然后调用show()使其可见。
QDataTables只接收可见的记录集(依赖于驱动程序),这样做使得即使是一张大表也可以被迅速地显示同时消耗较少的内存。
创建基于数据的窗体
创建一个基于数据的窗体要比使用基于数据的窗口部件要麻烦很多,这是因为我们必须单独处理每一个数据列,以下所示的代码绝大部分都可以由Qt设计器所产生,相关资料可参考Qt设计器的用户手册。
显示记录
#include <qapplication.h> #include <qdialog.h> #include <qlabel.h> #include <qlayout.h> #include <qlineedit.h> #include <qsqldatabase.h> #include <qsqlcursor.h> #include <qsqlform.h> #include "../login.h" bool createConnections(); class FormDialog : public QDialog { public: FormDialog(); }; FormDialog::FormDialog() { QLabel *forenameLabel = new QLabel( "Forename:", this ); QLabel *forenameDisplay = new QLabel( this ); QLabel *surnameLabel = new QLabel( "Surname:", this ); QLabel *surnameDisplay = new QLabel( this ); QLabel *salaryLabel = new QLabel( "Salary:", this ); QLineEdit *salaryEdit = new QLineEdit( this ); QGridLayout *grid = new QGridLayout( this ); grid->addWidget( forenameLabel, 0, 0 ); grid->addWidget( forenameDisplay, 0, 1 ); grid->addWidget( surnameLabel, 1, 0 ); grid->addWidget( surnameDisplay, 1, 1 ); grid->addWidget( salaryLabel, 2, 0 ); grid->addWidget( salaryEdit, 2, 1 ); grid->activate(); QSqlCursor staffCursor( "staff" ); staffCursor.select(); staffCursor.next(); QSqlForm sqlForm( this ); sqlForm.setRecord( staffCursor.primeUpdate() ); sqlForm.insert( forenameDisplay, "forename" ); sqlForm.insert( surnameDisplay, "surname" ); sqlForm.insert( salaryEdit, "salary" ); sqlForm.readFields(); } int main( int argc, char *argv[] ) { QApplication app( argc, argv ); if ( ! createConnections() ) return 1; FormDialog *formDialog = new FormDialog(); formDialog->show(); app.setMainWidget( formDialog ); return app.exec(); }
我们需要引用各组件所需要的头文件,我们还加入qsqldatabase.h和qsqlcursor.h,同时还有qsqlform.h。
本窗体是以对话框形式出现的,窗体类FormDialog继承于QDialog。我们使用了一个QLineEdit来显示薪水同时也允许用户对其进行修改。所有的组件显示在一个网格中。
我们为staff表创建了一个游标。让它从数据库取出所有的记录并定位至第一条记录。
现在我们创建了一个QSqlForm对象并把QSqlForm的记录缓冲区与游标的更新缓冲区对应起来。对于每个我们希望其数据感知的组件都把与相应的字段名结合进QSqlForm,最后我们调用readFields()把游标缓冲区中的数据刷新至关联窗口部件。
在数据窗体中显示记录
QDataView是一个可以拥有只读QSqlForm。与QSqlForm类似它也提供了一个槽refresh( QSqlRecord * ),因此它可以比较容易地与QDataTable连接在一起用来显示数据:
connect( myDataTable, SIGNAL( currentChanged( QSqlRecord* ) ), myDataView, SLOT( refresh( QSqlRecord* ) ) );
编辑记录
这个实例与上述的比较相似,所以我们讨论不同部分。
class FormDialog : public QDialog { Q_OBJECT public: FormDialog(); ~FormDialog(); public slots: void save(); private: QSqlCursor staffCursor; QSqlForm *sqlForm; QSqlIndex idIndex; };
这个save槽将被用于一个按纽以便让用户进行数据刷新的确认。 我们也保存QSqlCursor和QSqlForm的指针,因为它们将需要在构造函数之外被访问。
staffCursor.setTrimmed( "forename", TRUE ); staffCursor.setTrimmed( "surname", TRUE );
我们在文本字段上调用setTrimmed()以便获取到数据的时候自动消除右面的空格。
我们可以根据需要设置一些属性,比如对齐方式与有效性检查。相对应的函数为QLineEdit::setAlignment与QLineEdit::setValidator。
QLineEdit *forenameEdit = new QLineEdit( this );
QPushButton *saveButton = new QPushButton( "&Save", this ); connect( saveButton, SIGNAL(clicked()), this, SLOT(save()) );
FormDialog的构造函数类似于先前的实例。我们更改forename与surname窗口部件的类型至QLineEdits以便使其内容可编辑。然后又加入QPushButton以便用户点击确认保存他们的更新。
grid->addWidget( saveButton, 3, 0 );
我们在网格中添加了一行来容纳这个save按钮。
idIndex = staffCursor.index( "id" ); staffCursor.select( idIndex ); staffCursor.first();
我们创建了一个QSqlIndex对象然后使用索引执行一个select()操作。然后定位至第一条记录。
sqlForm = new QSqlForm( this ); sqlForm->setRecord( staffCursor.primeUpdate() );
我们创建了一个新的QSqlForm对象并把它的缓冲区与游标的更新缓冲区关联起来。
sqlForm->insert( forenameEdit, "forename" ); sqlForm->insert( surnameEdit, "surname" ); sqlForm->insert( salaryEdit, "salary" ); sqlForm->readFields();
现在我们把缓冲区的各个字段与QLineEdit控件关联起来。(在先前的实例中我们把它与游标的各个字段相关联)。函数readFields()使得各控件获得数据。
FormDialog::~FormDialog() { }
在析构函数中我们无需关注各控件或QSqlForm的处理,由于它们都是窗体的孩子,因此将被Qt在适当的时候清除。
void FormDialog::save() { sqlForm->writeFields(); staffCursor.update(); staffCursor.select( idIndex ); staffCursor.first(); }
最后我们加入了保存这一个功能,当用户点击保存按纽时该函数将被执行。最后我们调用writeFields()使得数据回写入QSqlRecord缓冲区。然后我们利用游标的update()函数来更新数据。此后游标不再可用,因此使用QSqlIndex再次调用select()来获得数据并定位至第一条记录。
QDataBrowser和QDataView这两个窗口部件提供了以上这些功能的许多函数。QDataBrowser提供了一个数据标识来指明是否允许编辑与记录浏览。QDataView提供了一个数据的只读形式。相关资料可查看类文档或Qt设计器的手册。
链接到sql/overview/form2/main.cpp
自定义编辑器窗口部件
QSqlForm使用了QSqlPropertyMap来控制数据在组件与数据库字段间的传输。自定义窗口部件同样可以使用属性映射,其中包含了如何转递数据的信息。
本实例基于前述的form2实例。因此这里只讲述不同部分,完整源码在sql/overview/custom1/main.h和sql/overview/custom1/main.cpp
class CustomEdit : public QLineEdit { Q_OBJECT Q_PROPERTY( QString upperLine READ upperLine WRITE setUpperLine ) public: CustomEdit( QWidget *parent=0, const char *name=0 ); QString upperLine() const; void setUpperLine( const QString &line ); public slots: void changed( const QString &line ); private: QString upperLineText; };
我们从QLineEdit这儿继承了一个类,加入了属性:upperLineText,以便其中存放字符串的大写。加入一个changed槽。
QSqlPropertyMap *propMap;
由于我们将使用属性映射因此我们把属性映射的指针与FormDialog的私有数据关联起来。
CustomEdit::CustomEdit( QWidget *parent, const char *name ) : QLineEdit( parent, name ) { connect( this, SIGNAL(textChanged(const QString &)), this, SLOT(changed(const QString &)) ); }
在CustomEdit的构造函数中,我们执行了QLineEdit的构造函数,然后在textChanged信号与changed槽之间建立了连接。
void CustomEdit::changed( const QString &line ) { setUpperLine( line ); }
changed()槽调用setUpperLine()这个函数。
void CustomEdit::setUpperLine( const QString &line ) { upperLineText = line.upper(); setText( upperLineText ); }
setUpperLine()函数将对文本进行处理以获得一份大写后的拷贝并把它设置成窗口部件的数据。
我们的CustomEdit类确保输入的数据始终是大写并提供了一个属性以便在属性映射中使用,使得CustomEdit的实例可与数据库字段相连。
CustomEdit *forenameEdit = new CustomEdit( this );
CustomEdit *surnameEdit = new CustomEdit( this );
我们使用先前用过的FormDialog,只是这一次我们把原先使用的QLineEdit窗口部件替换成自己的CustomEdit窗口部件。
在网格中的分布以及游标的设置与先前相同。
propMap = new QSqlPropertyMap; propMap->insert( forenameEdit->className(), "upperLine" );
我们在堆中建立一个新的属性映射,注册CustomEdit类并把upperLine属性关联进属性映射。
sqlForm = new QSqlForm( this ); sqlForm->setRecord( staffCursor->primeUpdate() ); sqlForm->installPropertyMap( propMap );
最后的变化是把属性映射装入QSqlForm,这样可把属性映射的内存加载入QSqlForm,由于QSqlForm属于FormDialog,因此Qt将在适当的时候删除它们。
这个实例与前述实例其本相同,例外的是forename与surname字段由于使用了自定义的CustomEdit窗口部件因此它们都将被大写。
为表格自定义编辑器窗口部件
我们必须重新实现QSqlEditorFactory以便使用自定义编辑器窗口部件。在下面的实例中我们将以QComboBox为基类创建自定义窗口部件件并定重载QSqlEditorFactory以便在QDataTable使用自定义窗口部件。
class StatusPicker : public QComboBox { Q_OBJECT Q_PROPERTY( int statusid READ statusId WRITE setStatusId ) public: StatusPicker( QWidget *parent=0, const char *name=0 ); int statusId() const; void setStatusId( int id ); private: QMap< int, int > index2id; };
我们创建了一个属性:statusid,定义了它的读取与写入的方法,由于statusid并不与combobox的索引对应因此我们创建了一个QMap以映射combobox的索引与statusid的关系。
class CustomSqlEditorFactory : public QSqlEditorFactory { Q_OBJECT public: QWidget *createEditor( QWidget *parent, const QSqlField *field ); };
我们同样需要继承QSqlEditorFactory,声明一个createEditor()函数,该函数是我们唯一需要重新实现的。
StatusPicker::StatusPicker( QWidget *parent, const char *name ) : QComboBox( parent, name ) { QSqlCursor cur( "status" ); cur.select( cur.index( "name" ) ); int i = 0; while ( cur.next() ) { insertItem( cur.value( "name" ).toString(), i ); index2id[i] = cur.value( "id" ).toInt(); i++; }
在StatusPicker的构造函数中我们创建了一个游标,遍历整个记录集把每一个name插入combobox,同时通过QMap型的变量index2id把combobox的索引与id列的值关联起来。
int StatusPicker::statusId() const { return index2id[ currentItem() ]; }
statusid的读取函数只是简单地利用combobox的索引从index2id中取值。
void StatusPicker::setStatusId( int statusid ) { QMap<int,int>::Iterator it; for ( it = index2id.begin(); it != index2id.end(); ++it ) { if ( it.data() == statusid ) { setCurrentItem( it.key() ); break; } } }
statusId()函数实现statusid的定入方法。我们创建了一个迭代器遍历整个QMap,取出其中的数据如果有与statusid相等的就把combobox的索引设成statusid对应的索引,然后离开循环。
当用户在QDataTable中编辑status字段时,就有一个combobox出现以便用户选值。为了不编辑的时候在表中显示status的名称我们需要继承QDataTable并重新实现paintField()函数。
class CustomTable : public QDataTable { Q_OBJECT public: CustomTable( QSqlCursor *cursor, bool autoPopulate = FALSE, QWidget * parent = 0, const char * name = 0 ) : QDataTable( cursor, autoPopulate, parent, name ) {} void paintField( QPainter * p, const QSqlField* field, const QRect & cr, bool ); };
我们只需简单地调用QDataTable的原始构造函数而不需做任何改动。同时还声明了paintField函数。
void CustomTable::paintField( QPainter * p, const QSqlField* field, const QRect & cr, bool b) { if ( !field ) return; if ( field->name() == "statusid" ) { QSqlQuery query( "SELECT name FROM status WHERE id=" + field->value().toString() ); QString text; if ( query.next() ) { text = query.value( 0 ).toString(); } p->drawText( 2,2, cr.width()-4, cr.height()-4, fieldAlignment( field ), text ); } else { QDataTable::paintField( p, field, cr, b) ; }
paintField函数的代码基于QDataTable中的源码。我们需要做三个改动,首先加入一个if语句:field->name() == "statusid",此情况下使用QSqlQuery获得数据。 其次为其他字段调用基类函数。最后的变化是在主函数staffTable由QDataTable改为自定义的CustomTable。
继承QSqlCursor
#include <qapplication.h> #include <qsqldatabase.h> #include <qsqlcursor.h> #include <qdatatable.h> #include "../login.h" bool createConnections(); int main( int argc, char *argv[] ) { QApplication app( argc, argv ); if ( createConnections() ) { QSqlCursor invoiceItemCursor( "invoiceitem" ); QDataTable *invoiceItemTable = new QDataTable( &invoiceItemCursor ); app.setMainWidget( invoiceItemTable ); invoiceItemTable->addColumn( "pricesid", "PriceID" ); invoiceItemTable->addColumn( "quantity", "Quantity" ); invoiceItemTable->addColumn( "paiddate", "Paid" ); invoiceItemTable->refresh(); invoiceItemTable->show(); return app.exec(); } return 1; }
这个实例与前述的table1实例很相似。我们创建了一个游标,在QDataTable中加入字段与对应的标题。调用refresh()获取数据,再执行show()来显示组件。
不幸的是这个实例并不完整,在设置时我们需要输入每一个字段,这是一个单调的工作。如果在显示价格代码的时候显示产品名牌可能会更合适一些。既然我们知道产品的价格与数量我们还可以显示产品的花费。最后如果能填入默认值(或者是一些主键)将更加实用 。
class InvoiceItemCursor : public QSqlCursor { public: InvoiceItemCursor(); };
我们创建了一个单独的头文件并继承了QSqlCursor。
InvoiceItemCursor::InvoiceItemCursor() : QSqlCursor( "invoiceitem" ) { // 空操作 }
在继承类的构造函数中我们仅仅调用了QSqlCursor的构造函数。
InvoiceItemCursor invoiceItemCursor;
当我们需要在invoiceitem表上使用的游标时我们可以创建InvoiceItemCursor而还是常用的QSqlCursor。
我们仍需要显示产品的名称而不是价格代码。
protected: QVariant calculateField( const QString & name );
在头文件中的改动很小:我们简单地加入calculateField()以便重新实现。
InvoiceItemCursor::InvoiceItemCursor() : QSqlCursor( "invoiceitem" ) { QSqlFieldInfo productName( "productname", QVariant::String ); append( productName ); setCalculated( productName.name(), TRUE ); } QVariant InvoiceItemCursor::calculateField( const QString & name ) { if ( name == "productname" ) { QSqlQuery query( "SELECT name FROM prices WHERE id=" + field( "pricesid" )->value().toString() + ";" ); if ( query.next() ) return query.value( 0 ); } return QVariant( QString::null ); }
我们已更改了InvoiceItemCursor的构造函数,我们创建了一个QSqlField的对象productname,然后把它加入 InvoiceItemCursor的字段集。我们在productname上调用setCalculated()使其成为一个计算字段。在setCalculated()中第一个参数是字段名,第二个参数是一个布尔参数,当其为TRUE时意味着calculateField()必须被调用以获得字段的值。
invoiceItemTable->addColumn( "productname", "Product" );
我们调用addColumn()来加入一个字段并指定显示名称。
我们必须定义自己的calculateField()函数。在我们的实例数据库中,invoiceitem表内价格代码(pricesid)是价格表的一个外键,我们使用pricesid在价格表中执行Sql语句以获得产品名称。
现在我们可以扩展实例,包括进一个计算字段并执行真正的计算工作。
头文件sql/overview/subclass4/main.h与前述实例一样。但构造函数与calculateField()函数需要一些简单扩展。我们来逐行地看一下。
InvoiceItemCursor::InvoiceItemCursor() : QSqlCursor( "invoiceitem" ) { QSqlFieldInfo productName( "productname", QVariant::String ); append( productName ); setCalculated( productName.name(), TRUE ); QSqlFieldInfo productPrice( "price", QVariant::Double ); append( productPrice ); setCalculated( productPrice.name(), TRUE ); QSqlFieldInfo productCost( "cost", QVariant::Double ); append( productCost ); setCalculated( productCost.name(), TRUE ); }
我们创建了两个额外的字段:价格price与花费cost。把它们加入游标中字段集,此两个都通过调用setCalculated()被注册成为计算字段。
QVariant InvoiceItemCursor::calculateField( const QString & name ) { if ( name == "productname" ) { QSqlQuery query( "SELECT name FROM prices WHERE id=" + field( "pricesid" )->value().toString() + ";" ); if ( query.next() ) return query.value( 0 ); } else if ( name == "price" ) { QSqlQuery query( "SELECT price FROM prices WHERE id=" + field( "pricesid" )->value().toString() + ";" ); if ( query.next() ) return query.value( 0 ); } else if ( name == "cost" ) { QSqlQuery query( "SELECT price FROM prices WHERE id=" + field( "pricesid" )->value().toString() + ";" ); if ( query.next() ) return QVariant( query.value( 0 ).toDouble() * value( "quantity").toDouble() ); } return QVariant( QString::null ); }
calculateField()函数的内容进行了一些增加,因为我们必须计算三个字段。productname与price字段是通过价格代码pricesid被选出来的。cost字段是计算出来的:价格与数量的乘积。请注意cost字段是以QVariant类型返回的,这是calculateField()函数所要求的。
我们使用了三个独立的查询而不是一个,这使得应用程序更为真实就好象它们是从三个不同的表或视图中取得的一样。
最后一个特征是当用户插入一条新记录时我们为它加入默认值。
QSqlRecord *primeInsert();
我们申明了自已的primeInsert()函数以便重新实现。
构造函数与calculateField()函数没有变化。
QSqlRecord *InvoiceItemCursor::primeInsert() { QSqlRecord *buffer = editBuffer(); QSqlQuery query( "SELECT NEXTVAL( 'invoiceitem_seq' );" ); if ( query.next() ) buffer->setValue( "id", query.value( 0 ) ); buffer->setValue( "paiddate", QDate::currentDate() ); buffer->setValue( "quantity", 1 ); return buffer; }
我们取得了一个指向可编辑缓冲区的指针以便游标用来插入与更新。字段id是一个用invoiceitem_seq产生的唯一整形,字段paiddate则默认为今天的日期。数量默认为1。最后我们返回一个指向该缓冲区的指针。余下的代码与前述实例没有区别。
实例表
所使用的实例表可以用以下标准SQL语句产生,你可能需要改变数据库名称以便与你所使用的匹配。
create table people (id integer primary key, name char(40)) create table staff (id integer primary key, forename char(40), surname char(40), salary float, statusid integer) create table status (id integer primary key, name char(30)) create table creditors (id integer primary key, forename char(40), surname char(40), city char(30)) create table prices (id integer primary key, name char(40), price float) create table invoiceitem (id integer primary key, pricesid integer, quantity integer, paiddate date)
在以上的calculateField()实例中使用了一个顺序(sequence),请注意并不是所有的数据库都支持它。
create sequence invoiceitem_seq
Copyright © 2002 Trolltech | Trademarks | 译者:Cavendish | Qt 3.0.5版
|