Qt 中使用 SQLite 数据库以及数据库连接池的设计与实现

一、Qt 中使用 SQLite 数据库

SQLite 是一款轻量级、嵌入式的关系型数据库,它不需要独立的服务器进程,整个数据库就是一个单一的跨平台文件。这种"零配置、自包含"的特性,让它在桌面软件、移动应用(Android/iOS)、甚至主流浏览器(Chrome、Firefox)中都得到了广泛应用。如果你的应用对数据安全性要求不高,但希望拥有完整的 SQL 能力,同时避免安装和维护额外的数据库软件,SQLite 几乎是完美的选择。

Qt 自带了 SQLite 驱动,开发者无需额外编译或配置,直接在代码中就可以使用 SQLite 数据库。本文会先介绍 Qt 中操作 SQLite 的基本流程,并给出一个完整的控制台示例,最后讨论如何将数据库访问代码写得更加优雅、可维护。


一、Qt 中操作 SQLite 的基本步骤

  1. 添加 SQL 模块 :在 .pro 文件中加入 QT += sql

  2. 加载驱动并建立连接 :使用 QSqlDatabase::addDatabase("QSQLITE")

  3. 指定数据库文件路径 :调用 setDatabaseName(),文件不存在时会自动创建。

  4. 打开数据库open() 返回 true 表示成功。

  5. 执行 SQL 语句 :通过 QSqlQuery 执行建表、增删改查等操作。

  6. 处理结果集 :使用 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)。

一个好的数据库访问层应当支持:

  1. SQL 语句外置:将 SQL 存在配置文件或资源文件中,运行时加载。

  2. 通用 DAO 模板 :封装 selectBeaninsertupdate 等通用方法。

  3. 参数化查询 :自动处理 QMap<QString, QVariant>prepare + bindValue 的映射。

  4. 结果映射 :通过回调函数或 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);

而底层的 DaoTemplateSqlUtil 等工具类可以完全复用,不关心具体的表结构。

3.2 如何一步步实现?

  • SqlUtil 类 :负责从 XML 或 JSON 文件读取 SQL 语句,提供 getSql(namespace, id) 方法。

  • DaoTemplate 类 :提供静态模板方法,接受 SQL 语句和参数映射,内部自动管理 QSqlDatabaseQSqlQuery,处理事务、错误日志等。

  • RowMapper 函数:由具体 DAO 提供,用于将一行结果转换为业务对象。

  • 连接管理:使用单例或依赖注入,确保数据库连接只初始化一次。

由于篇幅限制,这里不展开全部实现细节。后续文章会一步步展示如何构建这样一个轻量级的 ORM 工具,让 Qt 中的数据库操作既高效又优雅。


四、小结

  • SQLite 非常适合嵌入式场景和中小型桌面应用,Qt 对其提供了开箱即用的支持。

  • 基本操作只需 QSqlDatabase + QSqlQuery 即可完成,数据库文件可随程序分发。

  • 示例代码展示了建表、插入、查询的完整流程,并解释了第二次运行时表已存在的原因。

  • 初学者的"面条代码"虽然能用,但在实际项目中需要进一步封装,实现 SQL 外置、参数自动绑定、结果自动映射等能力,从而提高可维护性和可复用性。

如果你对 Qt 数据库编程感兴趣,建议继续阅读官方文档中关于 QSqlQueryModelQSqlTableModel 以及事务处理的部分

二、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 连接的创建与复用

当请求一个连接时,连接池会:

  1. 根据当前线程指针 + 传入的后缀,构造完整的连接名 fullConnectionName

  2. 调用 QSqlDatabase::contains(fullConnectionName) 检查是否已经存在该连接。

  3. 如果存在,则取出并测试连接是否有效(执行一条简单查询 SELECT 1)。若连接已断开,尝试重新打开;若打开失败则返回无效连接。

  4. 如果不存在,则调用内部 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 等远程数据库的场景。你可以将它直接集成到自己的项目中,并根据需要调整数据库参数或增加最大连接数控制。希望这篇文章能帮助你告别混乱的数据库连接管理,写出更干净、更健壮的代码。

相关推荐
YL200404261 小时前
MySQL-进阶篇-视图/存储过程/触发器
数据库·mysql
斜阳日落1 小时前
Qt 框架深度解析与性能优化
qt·性能优化·系统架构
arronKler1 小时前
数据库性能优化三:程序操作优化
数据库·oracle
墨着染霜华1 小时前
MySQL字符串数字筛选与转换 + Java Integer/Long数值长度避坑指南
java·数据库·mysql
2401_850491651 小时前
Bootstrap和OpenLayers结合开发的示例
jvm·数据库·python
Royzst1 小时前
集合进阶(Map集合)
java·前端·数据库
Java识堂1 小时前
MongoDB架构详解
数据库·mongodb·架构
jran-1 小时前
Redis NoSQL&Redis架构&数据结构
数据库·redis·缓存
文青小兵1 小时前
云计算Linux——数据库MySQL主从复制和读写分离(十七)
linux·运维·服务器·数据库·mysql·云计算