跳转到内容
新建笔记

Qt SQL:命名连接、参数查询、表格模型与清理

Qt SQL 把连接、查询和表格模型分开:QSqlDatabase 表示连接,QSqlQuery 执行语句,QSqlTableModel 为视图提供表数据。示例先用 SQLite 的内存数据库演示完整流程,再说明切换到 MySQL 时真正需要改变的配置。

原片段中的 QMYSYQL、setPassward、dp.open()、QMessgateBox、qDebug 少括号,以及中文逗号和分号,都不是可编译的 Qt 写法。修正拼写之后,还需要解决 SQL 参数绑定、连接身份和对象寿命。

连接名称、数据库名称与驱动

跳转到“连接名称、数据库名称与驱动”
项目SQLite 示例MySQL 对应
驱动标识"QSQLITE""QMYSQL"
Qt 连接名称如 "example-sql",用来区分本进程中的连接同样需要唯一名称
setDatabaseName文件路径,或 ":memory:"服务端数据库名称
服务地址不需要网络服务器setHostName、需要时 setPort
身份认证不使用服务器用户名和密码setUserName、setPassword 或带凭据的 open 重载

连接名称不是数据库名称。反复不带名称调用 addDatabase 会替换默认连接;多连接程序使用唯一名称,并明确构造 QSqlQuery(db) 或 QSqlTableModel(parent, db)。QSqlDatabase 的复制值仍指向同一连接,不能把复制当成新增独立连接。QSqlDatabase

MySQL 的主机、端口、数据库、用户名及密码由应用配置提供。不要把原例的 root/root 作为通用默认值。QSqlDatabase::drivers() 和 isDriverAvailable("QMYSQL") 可帮助检查插件,但驱动存在不等于连接一定成功:插件依赖库、认证、服务地址与数据库权限仍需分别排查。Qt SQL 驱动

姓名可能包含单引号,例如 O'Brien。用 QString::arg() 将它拼入 SQL 字符串,会把数据混进语句语法。改用 prepare() 加 bindValue(),并检查准备与执行结果。

占位符用于值,不能把任意表名、列名或排序表达式当作值绑定。动态标识符应从明确允许的集合中选择。不同驱动可能将语句检查推迟到 exec(),因此 prepare() 成功也不能省略执行结果检查。QSqlQuery

完整例子:两张表、绑定查询与只读表格

跳转到“完整例子:两张表、绑定查询与只读表格”

程序使用 Qt 6、C++17,链接 Qt6::Widgets 与 Qt6::Sql。默认执行检查后退出;加 --show 才显示表格并进入界面事件循环。以 Debug 配置保留断言。

保留两份原笔记的用途:mytable 中有自增 id 和 name,插入 John 并查询;student 中有 id/name/birth,交给 QTableView 展示。额外加入单引号值、事务回滚和错误 SQL 的检查。

#include <QAbstractItemView>
#include <QApplication>
#include <QDebug>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlTableModel>
#include <QStringList>
#include <QTableView>
#include <QVariant>
#include <cassert>
#include <stdexcept>
void require(bool success, const QString& message) {
if (!success)
throw std::runtime_error(message.toUtf8().constData());
}
void execute(QSqlQuery& query, const QString& sql) {
const bool success = query.exec(sql);
require(success, query.lastError().text());
}
void prepare(QSqlQuery& query, const QString& sql) {
const bool success = query.prepare(sql);
require(success, query.lastError().text());
}
void executePrepared(QSqlQuery& query) {
const bool success = query.exec();
require(success, query.lastError().text());
}
void prepareData(const QSqlDatabase& db) {
QSqlQuery query(db);
execute(query, "CREATE TABLE mytable ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)");
execute(query, "CREATE TABLE student ("
"id INTEGER PRIMARY KEY, name TEXT NOT NULL, birth TEXT NOT NULL)");
prepare(query, "INSERT INTO mytable(name) VALUES(:name)");
for (const QString& name : {QStringLiteral("John"), QStringLiteral("O'Brien")}) {
query.bindValue(":name", name);
executePrepared(query);
}
prepare(query, "INSERT INTO student(id, name, birth) VALUES(:id, :name, :birth)");
query.bindValue(":id", 7);
query.bindValue(":name", "John");
query.bindValue(":birth", "2000-01-01");
executePrepared(query);
execute(query, "SELECT id, name FROM mytable ORDER BY id");
QStringList names;
while (query.next()) {
assert(query.value(0).toInt() > 0);
names.append(query.value(1).toString());
qDebug() << "ID:" << query.value(0).toInt() << "Name:" << names.last();
}
require(!query.lastError().isValid(), query.lastError().text());
assert((names == QStringList{"John", "O'Brien"}));
}
void checkRollback(QSqlDatabase& db) {
const bool started = db.transaction();
require(started, db.lastError().text());
try {
QSqlQuery query(db);
execute(query, "INSERT INTO student(id, name, birth) "
"VALUES(8, 'Temporary', '2001-01-01')");
} catch (...) {
if (!db.rollback())
qWarning() << "Rollback failed:" << db.lastError().text();
throw;
}
const bool rolledBack = db.rollback();
require(rolledBack, db.lastError().text());
QSqlQuery check(db);
execute(check, "SELECT COUNT(*) FROM student");
require(check.next(), QStringLiteral("Missing count row"));
assert(check.value(0).toInt() == 1);
check.finish();
const bool invalidSucceeded = check.exec("SELECT missing_column FROM student");
assert(!invalidSucceeded && check.lastError().isValid());
}
int showOrCheckTable(QApplication& app, const QSqlDatabase& db, bool show) {
QSqlTableModel model(nullptr, db);
model.setTable("student");
model.setEditStrategy(QSqlTableModel::OnManualSubmit);
const bool selected = model.select();
require(selected, model.lastError().text());
assert(model.rowCount() == 1 && model.columnCount() == 3);
assert(model.data(model.index(0, 1)).toString() == "John");
QTableView view;
view.setModel(&model);
view.setEditTriggers(QAbstractItemView::NoEditTriggers);
view.setWindowTitle("Student table");
if (!show)
return 0;
view.show();
return app.exec();
} // view 与 model 在连接关闭之前销毁
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
const QString connection = QStringLiteral("example-sql");
int result = 1;
{
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", connection);
db.setDatabaseName(":memory:");
try {
require(db.isValid(), QStringLiteral("QSQLITE driver is unavailable"));
const bool opened = db.open();
require(opened, db.lastError().text());
prepareData(db);
checkRollback(db);
result = showOrCheckTable(app, db, app.arguments().contains("--show"));
} catch (const std::exception& error) {
qCritical().noquote() << QString::fromUtf8(error.what());
}
db.close();
} // db 的句柄也已经销毁
QSqlDatabase::removeDatabase(connection);
assert(!QSqlDatabase::contains(connection));
return result;
}

这不是通用的跨数据库建表脚本:示例中的自增语法属于 SQLite。使用 MySQL 时,应按自己的表结构与服务端 SQL 方言调整建表语句,不能仅换驱动名就假定全部 SQL 不变。上述程序没有连接 MySQL 服务器。

QSqlTableModel::select() 加载表数据。只读界面关闭编辑触发器;若需要编辑,再明确选择 OnFieldChange、OnRowChange 或 OnManualSubmit,并处理提交与回滚错误。模型可能分批读取较大结果集,因此不要从本例的一行表推导所有 rowCount() 都立即表示服务器总行数。QSqlTableModel

先销毁依赖连接的查询和模型,再关闭连接;所有 QSqlDatabase 句柄离开作用域后,才调用 removeDatabase。close() 影响该连接的其他副本,也不能替代“所有使用者都已结束”的条件。

一次性同步程序做完查询后可以正常退出。原示例在打开失败、建表失败和查询结束后都调用 a.exec(),却没有安排任何后续事件来结束程序,会让它继续等待。真正的界面或异步程序才需要相应事件循环。

连接应在所属线程内使用。工作线程需要独立连接与清理流程;不能把主线程创建的连接复制一份就当作线程隔离。本文也没有验证多线程数据库操作或服务端并发事务行为。