一、Qt 中使用 SQLite 数据库
SQLite 是一款轻量级、嵌入式的关系型数据库,它不需要独立的服务器进程,整个数据库就是一个单一的跨平台文件。这种"零配置、自包含"的特性,让它在桌面软件、移动应用(Android/iOS)、甚至主流浏览器(Chrome、Firefox)中都得到了广泛应用。如果你的应用对数据安全性要求不高,但希望拥有完整的 SQL 能力,同时避免安装和维护额外的数据库软件,SQLite 几乎是完美的选择。
Qt 自带了 SQLite 驱动,开发者无需额外编译或配置,直接在代码中就可以使用 SQLite 数据库。本文会先介绍 Qt 中操作 SQLite 的基本流程,并给出一个完整的控制台示例,最后讨论如何将数据库访问代码写得更加优雅、可维护。
一、Qt 中操作 SQLite 的基本步骤
-
添加 SQL 模块 :在
.pro文件中加入QT += sql。 -
加载驱动并建立连接 :使用
QSqlDatabase::addDatabase("QSQLITE")。 -
指定数据库文件路径 :调用
setDatabaseName(),文件不存在时会自动创建。 -
打开数据库 :
open()返回true表示成功。 -
执行 SQL 语句 :通过
QSqlQuery执行建表、增删改查等操作。 -
处理结果集 :使用
next()遍历查询结果,通过value()获取字段值。
需要注意:
QSQLITE驱动默认不支持用户名/密码验证,文件本身就是一个普通的二进制文件,安全性由文件系统权限保证。
二、完整示例:创建表、插入数据并查询
下面演示一个完整的控制台程序:
-
在当前目录下创建(或打开)
demo.db文件 -
建立
user表(id, username, password, email, mobile) -
插入一条记录
-
查询并打印所有用户
2.1 准备工程文件
cpp
# test_sqlite.pro
QT += core sql
CONFIG += c++11 console
2.2 main.cpp 代码
cpp
#include <QCoreApplication>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
void runSqliteDemo()
{
// 1. 注册 SQLite 驱动(第二个参数是连接名,可以省略)
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "demo_conn");
// 2. 设置数据库文件路径(相对或绝对路径)
db.setDatabaseName("demo.db");
// 3. 打开连接
if (!db.open()) {
qCritical() << "无法打开数据库:" << db.lastError().text();
return;
}
qDebug() << "数据库连接成功";
// 4. 建表(如果表已存在,exec 会返回 false,但不会影响后续操作)
QSqlQuery createQuery(db);
QString createSql = R"(
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
password TEXT NOT NULL,
email TEXT,
mobile TEXT
)
)";
if (!createQuery.exec(createSql)) {
qDebug() << "建表失败(可能已存在):" << createQuery.lastError().text();
} else {
qDebug() << "表创建成功";
}
// 5. 插入一条数据
QSqlQuery insertQuery(db);
insertQuery.prepare("INSERT INTO user (username, password) VALUES (?, ?)");
insertQuery.addBindValue("Alice");
insertQuery.addBindValue("passw0rd");
if (!insertQuery.exec()) {
qDebug() << "插入失败:" << insertQuery.lastError().text();
} else {
qDebug() << "插入成功,影响行数:" << insertQuery.numRowsAffected();
}
// 6. 查询数据
QSqlQuery selectQuery(db);
selectQuery.exec("SELECT id, username, password FROM user");
while (selectQuery.next()) {
int id = selectQuery.value("id").toInt();
QString name = selectQuery.value("username").toString();
QString pwd = selectQuery.value("password").toString();
qDebug() << QString("id=%1, username=%2, password=%3").arg(id).arg(name).arg(pwd);
}
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
runSqliteDemo();
return 0;
}
2.3 运行结果(第一次运行)
数据库连接成功
表创建成功
插入成功,影响行数: 1
id=1, username=Alice, password=passw0rd
同时,程序所在目录下会生成 demo.db 文件。你可以用 SQLite 命令行工具或图形化工具(如 SQLiteStudio)查看其结构和数据。
2.4 再次运行程序
第二次运行时,因为 demo.db 已经存在,建表语句会失败(表已存在),但插入语句仍会成功,因此结果集中会多出一条记录:
建表失败(可能已存在): "table user already exists Unable to execute statement"
id=1, username=Alice, password=passw0rd
id=2, username=Alice, password=passw0rd
这正是 SQLite 单文件特性的体现:数据库持久化保存在本地,程序重新启动后仍能访问之前的数据。
三、从"能用"到"好用":工程化改造思路
上面的示例虽然能跑通,但存在几个明显的工程缺陷:
-
SQL 语句硬编码:修改 SQL 需要重新编译程序。
-
无资源管理:没有处理连接池、错误重试等。
-
代码重复 :每次操作都要写
QSqlQuery、绑定参数、遍历结果等模板代码。 -
不可复用:无法方便地替换为其他数据库(如 MySQL、PostgreSQL)。
一个好的数据库访问层应当支持:
-
SQL 语句外置:将 SQL 存在配置文件或资源文件中,运行时加载。
-
通用 DAO 模板 :封装
selectBean、insert、update等通用方法。 -
参数化查询 :自动处理
QMap<QString, QVariant>到prepare+bindValue的映射。 -
结果映射 :通过回调函数或 Lambda 将
QSqlRecord转换为业务对象(如User)。
3.1 理想的使用方式(最终目标)
经过良好封装后,业务代码可以变得非常简洁:
cpp
// 查询单个用户
User user;
int ret = UserDao::selectUserById(101, &user);
// 查询所有用户
QList<User*> list;
UserDao::selectAllUsers(&list);
// 插入新用户
User newUser{"Bob", "123456"};
int newId = 0;
UserDao::insert(newUser, &newId);
// 更新用户
user.password = "newpass";
UserDao::update(user);
而底层的 DaoTemplate 和 SqlUtil 等工具类可以完全复用,不关心具体的表结构。
3.2 如何一步步实现?
-
SqlUtil 类 :负责从 XML 或 JSON 文件读取 SQL 语句,提供
getSql(namespace, id)方法。 -
DaoTemplate 类 :提供静态模板方法,接受 SQL 语句和参数映射,内部自动管理
QSqlDatabase和QSqlQuery,处理事务、错误日志等。 -
RowMapper 函数:由具体 DAO 提供,用于将一行结果转换为业务对象。
-
连接管理:使用单例或依赖注入,确保数据库连接只初始化一次。
由于篇幅限制,这里不展开全部实现细节。后续文章会一步步展示如何构建这样一个轻量级的 ORM 工具,让 Qt 中的数据库操作既高效又优雅。
四、小结
-
SQLite 非常适合嵌入式场景和中小型桌面应用,Qt 对其提供了开箱即用的支持。
-
基本操作只需
QSqlDatabase+QSqlQuery即可完成,数据库文件可随程序分发。 -
示例代码展示了建表、插入、查询的完整流程,并解释了第二次运行时表已存在的原因。
-
初学者的"面条代码"虽然能用,但在实际项目中需要进一步封装,实现 SQL 外置、参数自动绑定、结果自动映射等能力,从而提高可维护性和可复用性。
如果你对 Qt 数据库编程感兴趣,建议继续阅读官方文档中关于 QSqlQueryModel、QSqlTableModel 以及事务处理的部分
二、Qt数据库连接池的设计与实现
在之前的文章中,我们演示了如何通过 QSqlDatabase::addDatabase() 创建连接,并用 QSqlDatabase::database() 获取连接。为了复用连接,我们曾封装出类似下面这样的一组函数:
cpp
void createConnectionByName(const QString &connName) {
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", connName);
db.setHostName("127.0.0.1");
db.setDatabaseName("qt");
db.setUserName("root");
db.setPassword("root");
if (!db.open()) {
qDebug() << "Connection failed:" << db.lastError().text();
}
}
QSqlDatabase getConnectionByName(const QString &connName) {
return QSqlDatabase::database(connName);
}
这种写法虽然比直接写在业务代码中好一些,但仍然暴露出不少问题:
-
调用者必须自己维护连接名字,稍不注意就会重名。
-
获取连接时需要显式传入名字,代码冗长且容易出错。
-
无法判断某个连接是否正在被其他线程使用 ------ Qt 5.4 之后,一个线程创建的连接不能再被其他线程直接使用,否则会出错。
-
每次调用
createConnectionByName()都会新建一个连接,造成资源浪费。 -
网络闪断后连接不会自动恢复。
-
连接需要手动关闭或移除,否则可能累积过多。
为了解决这些痛点,本文会实现一个简易的数据库连接池。使用连接池之后,业务代码只需关心"拿连接、用连接",剩下的创建、复用、释放全部由连接池自动管理。
一、连接池的使用体验
先看一下最终我们希望达到的调用方式:
cpp
#include "ConnectionPool.h"
void doSomeQuery() {
// 从池中获取一个可用的数据库连接
QSqlDatabase db = ConnectionPool::openConnection();
QSqlQuery query(db);
query.exec("SELECT username FROM user WHERE id = 1");
while (query.next()) {
qDebug() << query.value("username").toString();
}
// 连接不需要显式归还,线程退出时会自动释放
}
int main() {
QApplication app(argc, argv);
doSomeQuery();
return app.exec();
}
关键特性:
-
不需要知道连接的名字,连接池内部根据线程 ID 自动生成唯一标识。
-
同一线程内多次调用
openConnection()会返回同一个连接(复用),避免重复创建。 -
不同线程自动获得不同的连接,杜绝跨线程使用。
-
如果连接因为网络原因断开,再次使用时会自动重连。
-
线程结束后,该线程持有的连接会被自动清理。
二、连接池的设计思路
2.1 连接与线程绑定
从 Qt 5.4 开始,QSqlDatabase 实例不再允许跨线程使用。因此最简单的线程安全策略就是:每个线程只拥有属于自己的连接 。连接池内部使用 QThread::currentThread() 的地址(转换为十六进制字符串)作为连接名前缀,保证不同线程的连接名不会冲突。
如果需要同一个线程内使用多个独立的连接(比如同时操作两个不同的数据库或事务状态需要隔离),可以通过给 openConnection() 传入一个自定义后缀来实现:
cpp
QSqlDatabase db1 = ConnectionPool::openConnection("read");
QSqlDatabase db2 = ConnectionPool::openConnection("write");
2.2 连接的创建与复用
当请求一个连接时,连接池会:
-
根据当前线程指针 + 传入的后缀,构造完整的连接名
fullConnectionName。 -
调用
QSqlDatabase::contains(fullConnectionName)检查是否已经存在该连接。 -
如果存在,则取出并测试连接是否有效(执行一条简单查询
SELECT 1)。若连接已断开,尝试重新打开;若打开失败则返回无效连接。 -
如果不存在,则调用内部
createConnection()新建一个连接,并保存到 Qt 的连接管理器中。
2.3 自动释放连接
为了防止连接泄露,连接池会在线程结束时自动移除该线程创建的所有连接。实现方式是在第一次为某线程创建连接时,连接一个信号槽:
cpp
connect(QThread::currentThread(), &QThread::finished,
qApp, [fullConnectionName]() {
if (QSqlDatabase::contains(fullConnectionName))
QSqlDatabase::removeDatabase(fullConnectionName);
});
注意这里使用 qApp(全局 QApplication 指针)作为接收者,确保槽函数在主线程或其他合适的事件循环中执行。移除操作本身是线程安全的。
三、核心代码实现
3.1 头文件 ConnectionPool.h
cpp
#ifndef CONNECTIONPOOL_H
#define CONNECTIONPOOL_H
#include <QSqlDatabase>
class ConnectionPool {
public:
// 获取一个连接,connectionName 为同一线程内区分不同连接的标识
static QSqlDatabase openConnection(const QString &connectionName = QString());
private:
static QSqlDatabase createConnection(const QString &connectionName);
};
#endif
3.2 实现文件 ConnectionPool.cpp
cpp
#include "ConnectionPool.h"
#include <QDebug>
#include <QSqlQuery>
#include <QSqlError>
#include <QThread>
#include <QCoreApplication>
QSqlDatabase ConnectionPool::openConnection(const QString &suffix) {
// 1. 生成线程相关的连接名
QString baseName = "conn_" + QString::number(quint64(QThread::currentThread()), 16);
QString fullName = baseName + suffix;
// 2. 检查连接是否已存在
if (QSqlDatabase::contains(fullName)) {
QSqlDatabase db = QSqlDatabase::database(fullName);
// 3. 测试连接有效性,如果断开则尝试重连
QSqlQuery test("SELECT 1", db);
if (test.lastError().type() == QSqlError::NoError) {
return db;
}
if (!db.open()) {
qWarning() << "Reopen failed:" << db.lastError().text();
return QSqlDatabase();
}
return db;
}
// 4. 首次创建:注册线程结束时的清理动作
if (qApp) {
QObject::connect(QThread::currentThread(), &QThread::finished,
qApp, [fullName]() {
if (QSqlDatabase::contains(fullName)) {
QSqlDatabase::removeDatabase(fullName);
qDebug() << "Removed connection:" << fullName;
}
});
}
// 5. 创建新连接
return createConnection(fullName);
}
QSqlDatabase ConnectionPool::createConnection(const QString &name) {
static int serial = 0;
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", name);
db.setHostName("localhost");
db.setDatabaseName("qt");
db.setUserName("root");
db.setPassword("root");
if (db.open()) {
qDebug() << "New connection created:" << name << ", serial:" << ++serial;
return db;
} else {
qWarning() << "Create connection failed:" << db.lastError().text();
return QSqlDatabase();
}
}
注意:示例中使用了固定的 MySQL 参数,实际项目中应从配置文件读取。
四、多线程测试与结果分析
为了验证连接池在多线程环境下的行为,我们编写一个简单的测试线程类:
cpp
// Thread.h
#include <QThread>
class TestThread : public QThread {
void run() override;
};
// Thread.cpp
#include "Thread.h"
#include "ConnectionPool.h"
void TestThread::run() {
// 第一次获取连接
QSqlDatabase db = ConnectionPool::openConnection();
QSqlQuery query(db);
query.exec("SELECT username FROM user WHERE id=1");
if (query.next())
qDebug() << query.value(0).toString();
// 模拟一些工作后再次获取连接(应该复用同一个)
QThread::sleep(1);
QSqlDatabase db2 = ConnectionPool::openConnection();
QSqlQuery query2(db2);
query2.exec("SELECT username FROM user WHERE id=1");
if (query2.next())
qDebug() << query2.value(0).toString();
}
在主函数中启动 10 个线程,每个线程启动间隔 100 毫秒(避免瞬间大量连接请求导致 MySQL 拒绝连接):
cpp
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
for (int i = 0; i < 10; ++i) {
TestThread *t = new TestThread();
t->start();
QThread::msleep(100);
}
return app.exec();
}
运行结果(部分截取):
New connection created: conn_7f8a1c000800, serial: 1
Alice
New connection created: conn_7f8a1c001200, serial: 2
Alice
New connection created: conn_7f8a1c001a00, serial: 3
Alice
...
Removed connection: conn_7f8a1c000800
Removed connection: conn_7f8a1c001200
...
可以看到:
-
每个线程都创建了自己独立的连接(
serial递增)。 -
同一个线程内的两次
openConnection()返回的是同一个连接(没有创建新连接)。 -
线程退出时,连接被自动移除,没有内存或资源泄漏。
五、关于连接数量控制的思考
本文实现的连接池没有对最大连接数 进行限制。在实际生产环境中,数据库服务器(尤其是 MySQL)往往有 max_connections 限制,如果客户端无节制地创建连接,可能导致服务器拒绝服务。
那么为什么在这个实现中我们没有添加连接数上限呢?原因如下:
-
Qt 应用场景:Qt 多用于桌面客户端或嵌入式设备,通常一个进程内不会同时需要几十上百个数据库连接。如果确实需要很多连接,那可能是设计上值得商榷的地方。
-
服务器端很少用 Qt:在编写高并发后端服务时,Java、Go 等语言及其生态(如 HikariCP、druid)才是更主流的选择。Qt 并不适合这类场景。
-
SQLite 更常见:很多 Qt 程序直接使用本地 SQLite 文件,不需要远程 MySQL,连接数本身就不是问题。
如果你确实需要限制连接总数(例如在一个大型 Qt 服务中),可以在此基础上扩展:使用一个全局计数器或 QSemaphore 控制并发创建数量,并配合 QWaitCondition 实现获取连接时的超时等待。早期 Qt 允许跨线程使用数据库连接时,我曾实现过一个更复杂的连接池(支持连接复用、空闲超时、最大连接数等),感兴趣的读者可以参考旧版本的实现思路(限于篇幅,本文不再展开)。
六、总结
通过本文我们实现了一个轻量级、线程安全的 Qt 数据库连接池。它的优点包括:
-
零配置使用 :业务代码只需调用
ConnectionPool::openConnection()。 -
自动复用:同一线程内多次请求返回同一个连接,节省资源。
-
自动清理:线程结束时连接自动移除,无需手动管理。
-
断线重连:使用前检测连接有效性,无效则尝试重开。
-
支持多连接:同一线程可通过后缀获取多个不同连接。
这个连接池特别适合中小型 Qt 桌面应用,尤其是使用 MySQL 等远程数据库的场景。你可以将它直接集成到自己的项目中,并根据需要调整数据库参数或增加最大连接数控制。希望这篇文章能帮助你告别混乱的数据库连接管理,写出更干净、更健壮的代码。