3、Qt SQL API核心类:QSqlDatabase、QSqlQuery、QSqlRecord、QSqlField、QSqlError详解
好,咱们直接切入正题。上一章我们把Qt SQL的架构捋了一遍,知道了它分驱动层、API层和用户界面层。这一章,咱们就深入API层的几个核心类。说白了,你写任何数据库程序,天天打交道的就这几个家伙:QSqlDatabase、QSqlQuery、QSqlRecord、QSqlField 和 QSqlError。
我个人习惯,学一个新东西,先搞清楚它「是什么」和「怎么用」。咱们一个一个来。
3.1 QSqlDatabase:你的数据库连接管家
QSqlDatabase 是啥?它就是你和数据库之间的那条「连接线」。你想想看,你要跟MySQL、SQLite或者PostgreSQL说话,总得先打个招呼、建立个会话吧?QSqlDatabase 干的就是这个活儿。
它负责管理连接参数:数据库类型、主机名、端口、用户名、密码、数据库名。嗯,这里要注意,QSqlDatabase 本身不存储数据,它只是一个连接句柄。
看个最简单的例子,连接SQLite:
// 创建一个默认连接
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("mydb.sqlite");
if (!db.open()) {
qDebug() << "数据库打开失败:" << db.lastError().text();
return;
}
// 连接成功,可以干活了
我在项目中遇到过一个问题:多个线程同时操作同一个数据库连接,结果各种崩溃。后来才明白,QSqlDatabase 不是线程安全的。每个线程最好用自己的连接,或者加锁保护。这是个坑,我踩过。
3.2 QSqlQuery:执行SQL的利刃
连接建好了,接下来就是执行SQL语句。QSqlQuery 就是干这个的。它支持所有标准的SQL操作:SELECT、INSERT、UPDATE、DELETE,以及存储过程调用。
我个人习惯把 QSqlQuery 比作「数据库的嘴巴」——你告诉它要说什么(SQL语句),它帮你传达给数据库,再把结果带回来。
来看一个查询的例子:
QSqlQuery query;
query.exec("SELECT id, name, age FROM users WHERE age > 18");
while (query.next()) {
int id = query.value(0).toInt();
QString name = query.value(1).toString();
int age = query.value(2).toInt();
qDebug() << id << name << age;
}
这里有个细节:query.next() 每次移动一行,返回 true 表示还有数据。第一次调用 next() 会定位到第一行。如果你想知道有多少行,可以用 query.size(),但注意,不是所有数据库驱动都支持这个。
query.value("column_name") 按列名取值,代码可读性更好。我一般不用索引,除非列名特别长。
说到SQL注入,这可是个大问题。千万别用字符串拼接来构造SQL!用 bindValue 或者 prepare 才是正道:
QSqlQuery query;
query.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
query.addBindValue("张三");
query.addBindValue(25);
query.exec();
我曾经接手过一个项目,里面全是字符串拼接的SQL,看得我心惊肉跳。后来花了一周时间全部改成参数绑定。安全第一,没得商量。
3.3 QSqlRecord:结果集的元数据
当你执行完一个查询,QSqlQuery 里其实藏着两个东西:数据本身,以及数据的「描述信息」。这个描述信息就是 QSqlRecord。
QSqlRecord 告诉你:结果集有多少列?每列叫什么名字?每列是什么类型?说白了,它就是结果集的「目录」。
怎么拿到它?很简单:
QSqlQuery query("SELECT * FROM users");
QSqlRecord record = query.record();
// 遍历所有列
for (int i = 0; i < record.count(); ++i) {
QString fieldName = record.fieldName(i);
QVariant::Type fieldType = record.field(i).type();
qDebug() << "列名:" << fieldName << "类型:" << fieldType;
}
嗯,这里要注意,record() 返回的是当前查询的元数据,不是数据库表的元数据。如果你想知道表结构,得用 QSqlDatabase::record("table_name")。
我在做动态表单生成器的时候,就是靠 QSqlRecord 来获取字段信息,然后自动生成输入控件的。省了不少事。
3.4 QSqlField:单个字段的详细信息
QSqlField 是比 QSqlRecord 更细粒度的东西。它描述的是「一个字段」的属性:字段名、字段类型、是否允许为空、默认值、最大长度等等。
你可以通过 QSqlRecord::field() 拿到它:
QSqlRecord record = query.record();
QSqlField field = record.field("age");
qDebug() << "字段名:" << field.name();
qDebug() << "类型:" << field.typeID(); // 数据库原生类型ID
qDebug() << "是否可为空:" << field.isNull();
qDebug() << "默认值:" << field.defaultValue();
qDebug() << "最大长度:" << field.length();
说实话,QSqlField 在日常开发中用得不多。但如果你要写一个数据库管理工具,或者做数据校验,它就派上大用场了。比如,你可以根据字段类型自动选择输入控件:字符串用 QLineEdit,整数用 QSpinBox,日期用 QDateEdit。
3.5 QSqlError:错误处理的救星
写数据库程序,不出错是不可能的。连接失败、SQL语法错误、约束违反……各种问题防不胜防。QSqlError 就是 Qt 给咱们准备的错误信息容器。
它包含三个关键信息:
- driverText():数据库驱动返回的错误信息
- databaseText():数据库服务器返回的错误信息
- nativeErrorCode():数据库原生的错误码
用法很简单:
QSqlQuery query;
if (!query.exec("SELECT * FROM non_existent_table")) {
QSqlError err = query.lastError();
qDebug() << "驱动错误:" << err.driverText();
qDebug() << "数据库错误:" << err.databaseText();
qDebug() << "错误码:" << err.nativeErrorCode();
}
我个人习惯,在关键操作后都检查一下错误。别偷懒,不然出了问题排查起来很痛苦。我曾经因为没检查 QSqlDatabase::open() 的返回值,导致程序在用户机器上静默失败,用户还以为程序没反应。后来加了个错误弹窗,问题立刻暴露了。
另外,QSqlError::type() 可以告诉你错误的严重级别:
| 类型 | 含义 |
|---|---|
| NoError | 没有错误 |
| ConnectionError | 连接错误 |
| StatementError | SQL语句错误 |
| TransactionError | 事务错误 |
| UnknownError | 未知错误 |
3.6 它们之间的关系
说了这么多,这几个类到底怎么配合?我画了张图,一看就明白:
这张图很清楚地展示了:QSqlDatabase 创建 QSqlQuery,QSqlQuery 执行后通过 record() 拿到 QSqlRecord,再通过 field() 获取 QSqlField。而 QSqlError 则贯穿始终,任何操作出错都可以通过 lastError() 获取。
3.7 实战小贴士
最后,分享几个我实际项目中的经验:
- 连接命名:多连接时,给每个连接起个有意义的名字,比如
"main_connection"、"backup_connection"。别用默认的空字符串,不然排查起来很头疼。 - 查询重用:同一个SQL反复执行时,用
prepare()+bindValue()的方式,效率比exec()高很多。数据库会缓存执行计划。 - 错误日志:把
QSqlError的详细信息记录到日志文件里。用户反馈问题时,日志就是你的救命稻草。 - 字段索引:用
record.indexOf("column_name")获取字段索引,比硬编码索引号更安全。表结构变了,代码也不会立刻崩。
好了,这一章的内容就到这儿。这几个类虽然基础,但用好它们,你的Qt数据库程序就稳了一大半。下一章咱们聊聊事务处理和高级查询技巧,到时候见。
可可精品资料库