目标读者:已经看完前 1--7 期,并且在实际项目中开始接触"需要落地存储数据"的 C++/Qt 工程师,希望不只是"会用 QSqlQuery",而是能在项目里搭出一套干净、可维护的数据库访问层。
推荐环境:Qt 5.12/5.15 + Qt Creator + CMake(文内用 CMake,改成 qmake 也很简单)
目录
[一、问题背景:为什么"到处 new QSqlQuery"迟早要出事](#一、问题背景:为什么“到处 new QSqlQuery”迟早要出事)
[1. SQL 拼接满天飞,改字段名像拆炸弹](#1. SQL 拼接满天飞,改字段名像拆炸弹)
[2. 数据库连接谁创建、谁释放,没人说得清](#2. 数据库连接谁创建、谁释放,没人说得清)
[3. 线程安全问题:子线程里直接用主线程数据库](#3. 线程安全问题:子线程里直接用主线程数据库)
[4. UI 逻辑和数据访问搅在一起,重构越改越乱](#4. UI 逻辑和数据访问搅在一起,重构越改越乱)
[二、核心设计思路:从 QSqlDatabase 到 Entity/DAO](#二、核心设计思路:从 QSqlDatabase 到 Entity/DAO)
[1. DatabaseConnection:只管一件事------维护数据库连接](#1. DatabaseConnection:只管一件事——维护数据库连接)
[2. 实体类(Entity):把"行"变成"对象"](#2. 实体类(Entity):把“行”变成“对象”)
[3. DAO:把所有 SQL 都装进"水管"里](#3. DAO:把所有 SQL 都装进“水管”里)
[三、完整示例工程:DatabaseDemo(SQLite + Qt Widgets)](#三、完整示例工程:DatabaseDemo(SQLite + Qt Widgets))
[1. 工程结构](#1. 工程结构)
[2. CMakeLists.txt](#2. CMakeLists.txt)
[3. DatabaseConnection:连接与建表](#3. DatabaseConnection:连接与建表)
[4. User / UserDao:用户实体与访问对象](#4. User / UserDao:用户实体与访问对象)
[5. Product / ProductDao:产品实体与访问对象(结构类似)](#5. Product / ProductDao:产品实体与访问对象(结构类似))
[6. MainWindow:简单的用户/产品管理界面](#6. MainWindow:简单的用户/产品管理界面)
[1. 连接名与多线程](#1. 连接名与多线程)
[2. 预处理语句是底线](#2. 预处理语句是底线)
[3. DAO 层里可以适度封装业务规则](#3. DAO 层里可以适度封装业务规则)
[4. 大量数据写入:记得用事务](#4. 大量数据写入:记得用事务)
[5. 结构演进:给未来留一个 migrations 表](#5. 结构演进:给未来留一个 migrations 表)
一、问题背景:为什么"到处 new QSqlQuery"迟早要出事
很多桌面项目做到一半,都会进入"需要数据库"的阶段:配置要落地、业务数据要持久化、日志统计要查询。最开始往往是这样的写法:
cpp
void MainWindow::onSaveButtonClicked()
{
QSqlDatabase db = QSqlDatabase::database(); // 拿默认连接
QSqlQuery query(db);
QString sql = QString("INSERT INTO users(username, password) "
"VALUES('%1','%2')")
.arg(ui->editUser->text())
.arg(ui->editPass->text());
if (!query.exec(sql)) {
QMessageBox::warning(this, "错误", "保存失败");
}
}
能不能跑?能。问题是等项目做大之后,你会陆续遇到这些情况:
1. SQL 拼接满天飞,改字段名像拆炸弹
- 每个界面都写 SQL,字段名直接写死在字符串里;
- 产品说"username 改成 login_name"时,你要全工程搜一遍
"username"; - 稍不留神有一处没改到,就会在某个角落悄悄埋雷。
更要命的是,习惯性地用 arg() 拼接,很容易把用户输入直接塞进 SQL 里------安全层面直接拉跨。
2. 数据库连接谁创建、谁释放,没人说得清
不少项目里,随处可见:
cpp
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("demo.db");
db.open();
甚至干脆每点一次按钮就 add 一次。结果是:
- 连接数越来越多,debug 输出里经常冒出
QSqlDatabasePrivate::removeDatabase报错; - 某些平台/驱动上会直接出现 "too many connections"。
3. 线程安全问题:子线程里直接用主线程数据库
这一类问题更隐蔽:
- 主线程里
addDatabase初始化好连接; - 为了提高性能,把某些统计任务放进了
QThread; - 子线程里直接
QSqlDatabase::database(),查完就用。
一开始看似没问题,等到并发稍微一高,要么数据乱掉,要么各种奇怪崩溃。Qt 文档写得很清楚:数据库连接是和线程绑定的,不能跨线程随便用。
4. UI 逻辑和数据访问搅在一起,重构越改越乱
另外一个实际痛点是代码结构:
- 在窗口类、对话框类、工具类里,到处都在
QSqlQuery; - 想改一条查询逻辑,要翻半天 UI 代码才能找到;
- 单元测试几乎没法写,因为业务跟 UI、跟数据库全绑死了。
到这个阶段,往往大家会有一种很强烈的冲动:
"能不能像 Web 项目里那样,有个 Entity/DAO 层,把 SQL 都关起来?"
这一期的目标,就是把这个念头落成一份可以直接在 Qt 项目里使用的数据库访问层样板:
- 有数据库连接管理(单例);
- 有实体类(User、Product 等);
- 有 DAO(Data Access Object)负责 CRUD;
- 有一个简单但完整的管理界面,把这些东西串起来。
二、核心设计思路:从 QSqlDatabase 到 Entity/DAO
先把几个关键角色说清楚,再用完整代码演示。
1. DatabaseConnection:只管一件事------维护数据库连接
在 Qt 里,QSqlDatabase 是一个轻量级 handle,但它背后的连接和驱动资源一点都不便宜。最稳妥的做法是:
- 整个进程里只创建极少数几个
QSqlDatabase连接(通常一个就够); - 把它们交给一个全局的管理类------这里叫
DatabaseConnection; - 用单例模式封装初始化、关闭、建表、执行脚本等逻辑。
简化版接口:
cpp
class DatabaseConnection : public QObject
{
Q_OBJECT
public:
static DatabaseConnection &instance();
bool initialize(const QString &databasePath);
QSqlDatabase database() const { return m_db; }
bool isOpen() const { return m_db.isOpen(); }
private:
explicit DatabaseConnection(QObject *parent = nullptr);
Q_DISABLE_COPY(DatabaseConnection)
bool createTables();
QSqlDatabase m_db;
QMutex m_mutex;
};
这样一来,其它地方如果要用数据库,只需要:
cpp
QSqlDatabase db = DatabaseConnection::instance().database();
QSqlQuery query(db);
统一入口、统一配置,日后要迁移到 MySQL/PostgreSQL 也只是改一处。
2. 实体类(Entity):把"行"变成"对象"
实体类的设计原则其实很简单:
- 一张表对应一个类:
users表对应User类、products表对应Product类; - 成员变量一一映射字段;
- 提供
toVariantMap()/ 构造函数(QVariantMap) 方便和 SQL 结果互转。
比如用户实体:
cpp
class User : public QObject
{
Q_OBJECT
Q_PROPERTY(int id READ id WRITE setId)
Q_PROPERTY(QString username READ username WRITE setUsername)
Q_PROPERTY(QString password READ password WRITE setPassword)
Q_PROPERTY(QString email READ email WRITE setEmail)
Q_PROPERTY(bool isActive READ isActive WRITE setIsActive)
Q_PROPERTY(QDateTime createdAt READ createdAt WRITE setCreatedAt)
public:
explicit User(QObject *parent = nullptr);
explicit User(const QVariantMap &map, QObject *parent = nullptr);
// getter/setter 省略
QVariantMap toVariantMap() const;
void setProperty(const QString &name, const QVariant &value);
private:
int m_id = 0;
QString m_username;
QString m_password;
QString m_email;
bool m_isActive = true;
QDateTime m_createdAt;
};
日后如果要支持 JSON 导入导出、配置文件存储,这种 QVariantMap 形式非常好用。
3. DAO:把所有 SQL 都装进"水管"里
DAO(Data Access Object)层的作用可以概括成一句话:
"所有涉及数据库的地方,只认 DAO,不认 SQL。"
比如对 users 表的访问,都通过 UserDao 来做:
cpp
class UserDao : public QObject
{
Q_OBJECT
public:
explicit UserDao(QObject *parent = nullptr);
bool insert(const User &user);
bool update(const User &user);
bool remove(int userId);
User getById(int userId);
QList<User> getAll();
QList<User> getByUsername(const QString &username);
QList<User> getByEmail(const QString &email);
QStringList getAllUsernames();
bool validateLogin(const QString &username, const QString &password);
private:
QSqlDatabase &database();
QMutex m_mutex;
};
上层业务只需要调用这些方法,完全不用关心 SQL 具体长什么样,也不用去操心连接、事务这些细节。
三、完整示例工程:DatabaseDemo(SQLite + Qt Widgets)
下面这部分是你可以直接用 Qt Creator 新建 CMake 项目,然后把文件全部拷进去就能跑的示例。
1. 工程结构
bash
DatabaseDemo/
├── CMakeLists.txt
├── include/
│ ├── databaseconnection.h
│ ├── user.h
│ ├── userdao.h
│ ├── product.h
│ ├── productdao.h
│ └── mainwindow.h
└── src/
├── main.cpp
├── databaseconnection.cpp
├── user.cpp
├── userdao.cpp
├── product.cpp
├── productdao.cpp
└── mainwindow.cpp
下面所有代码都按这个结构给出,你只要按路径存好即可。
2. CMakeLists.txt
3. DatabaseConnection:连接与建表
include/databaseconnection.h
cpp
class User
{
public:
User();
explicit User(const QVariantMap &map);
int id() const { return m_id; }
void setId(int id) { m_id = id; }
QString username() const { return m_username; }
void setUsername(const QString &username) { m_username = username; }
QString password() const { return m_password; }
void setPassword(const QString &password) { m_password = password; }
QString email() const { return m_email; }
void setEmail(const QString &email) { m_email = email; }
bool isActive() const { return m_isActive; }
void setIsActive(bool isActive) { m_isActive = isActive; }
QDateTime createdAt() const { return m_createdAt; }
void setCreatedAt(const QDateTime &createdAt) { m_createdAt = createdAt; }
// 密码哈希相关方法
static QString hashPassword(const QString &password);
static bool verifyPassword(const QString &password, const QString &hashedPassword);
QVariantMap toVariantMap() const;
void setProperty(const QString &name, const QVariant &value);
private:
int m_id = 0;
QString m_username;
QString m_password;
QString m_email;
bool m_isActive = true;
QDateTime m_createdAt;
};
src/databaseconnection.cpp
cpp
DatabaseConnection::DatabaseConnection(QObject *parent)
: QObject(parent)
{
}
QString DatabaseConnection::getStandardDatabasePath(const QString &dbName)
{
QString dbDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(dbDir);
return dbDir + "/" + dbName;
}
QString DatabaseConnection::getLogFilePath()
{
QString logDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(logDir);
return logDir + "/app.log";
}
DatabaseConnection::~DatabaseConnection()
{
if (m_db.isOpen()) {
m_db.close();
}
}
DatabaseConnection &DatabaseConnection::instance()
{
static DatabaseConnection instance;
return instance;
}
bool DatabaseConnection::initialize(const QString &databasePath)
{
QMutexLocker locker(&m_mutex);
// 如果数据库路径相同且连接已打开,则无需重新初始化
if (m_db.isOpen() && m_currentDatabasePath == databasePath) {
qInfo() << "数据库连接已存在且有效:" << databasePath;
return true;
}
if (m_db.isOpen()) {
m_db.close();
// 移除旧连接
QSqlDatabase::removeDatabase("mainConnection");
}
// 使用单独的连接名,避免与默认连接冲突
m_db = QSqlDatabase::addDatabase("QSQLITE", "mainConnection");
m_db.setDatabaseName(databasePath);
if (!m_db.open()) {
qWarning() << "无法打开数据库:" << m_db.lastError().text();
return false;
}
if (!createTables()) {
return false;
}
m_currentDatabasePath = databasePath;
qInfo() << "数据库连接成功:" << databasePath;
return true;
}
bool DatabaseConnection::refresh()
{
QMutexLocker locker(&m_mutex);
if (m_db.isOpen()) {
m_db.close();
}
// 如果有保存的数据库路径,重新打开连接
if (!m_currentDatabasePath.isEmpty()) {
// 移除旧连接
QSqlDatabase::removeDatabase("mainConnection");
// 重新创建连接
m_db = QSqlDatabase::addDatabase("QSQLITE", "mainConnection");
m_db.setDatabaseName(m_currentDatabasePath);
return m_db.open();
}
return m_db.open();
}
bool DatabaseConnection::executeScript(const QString &filePath)
{
QMutexLocker locker(&m_mutex);
if (!m_db.isOpen()) {
qWarning() << "数据库未打开,无法执行脚本";
return false;
}
QFile scriptFile(filePath);
if (!scriptFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "无法打开 SQL 脚本文件:" << filePath;
return false;
}
QTextStream in(&scriptFile);
QString script = in.readAll();
scriptFile.close();
QSqlQuery query(m_db);
if (!query.exec(script)) {
qWarning() << "SQL 脚本执行失败:" << query.lastError().text();
return false;
}
return true;
}
bool DatabaseConnection::beginTransaction()
{
QMutexLocker locker(&m_mutex);
if (!m_db.isOpen()) {
qWarning() << "数据库未打开,无法开始事务";
return false;
}
return m_db.transaction();
}
bool DatabaseConnection::commitTransaction()
{
QMutexLocker locker(&m_mutex);
if (!m_db.isOpen()) {
qWarning() << "数据库未打开,无法提交事务";
return false;
}
return m_db.commit();
}
bool DatabaseConnection::rollbackTransaction()
{
QMutexLocker locker(&m_mutex);
if (!m_db.isOpen()) {
qWarning() << "数据库未打开,无法回滚事务";
return false;
}
return m_db.rollback();
}
bool DatabaseConnection::createTables()
{
// 开始事务
if (!m_db.transaction()) {
qWarning() << "开始事务失败:" << m_db.lastError().text();
return false;
}
QSqlQuery query(m_db);
// 用户表
if (!query.exec(
"CREATE TABLE IF NOT EXISTS users ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"username TEXT NOT NULL UNIQUE,"
"password TEXT NOT NULL,"
"email TEXT NOT NULL,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP,"
"is_active INTEGER DEFAULT 1)"
)) {
qWarning() << "创建 users 表失败:" << query.lastError().text();
m_db.rollback();
return false;
}
// 产品表
if (!query.exec(
"CREATE TABLE IF NOT EXISTS products ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL,"
"description TEXT,"
"price REAL NOT NULL,"
"stock INTEGER NOT NULL DEFAULT 0,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP,"
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)"
)) {
qWarning() << "创建 products 表失败:" << query.lastError().text();
m_db.rollback();
return false;
}
// 订单表(本示例暂不做 UI,只演示结构)
if (!query.exec(
"CREATE TABLE IF NOT EXISTS orders ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"user_id INTEGER NOT NULL,"
"product_id INTEGER NOT NULL,"
"quantity INTEGER NOT NULL,"
"order_date DATETIME DEFAULT CURRENT_TIMESTAMP,"
"status TEXT DEFAULT 'pending',"
"FOREIGN KEY(user_id) REFERENCES users(id),"
"FOREIGN KEY(product_id) REFERENCES products(id))"
)) {
qWarning() << "创建 orders 表失败:" << query.lastError().text();
m_db.rollback();
return false;
}
// 插入一些初始数据(如果不存在)
if (!query.exec("INSERT OR IGNORE INTO users (id, username, password, email) "
"VALUES (1, 'admin', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'admin@example.com')")) {
qWarning() << "插入初始用户数据失败:" << query.lastError().text();
m_db.rollback();
return false;
}
if (!query.exec("INSERT OR IGNORE INTO users (id, username, password, email) "
"VALUES (2, 'test', '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', 'test@example.com')")) {
qWarning() << "插入初始用户数据失败:" << query.lastError().text();
m_db.rollback();
return false;
}
if (!query.exec("INSERT OR IGNORE INTO products (id, name, description, price, stock) "
"VALUES (1, '笔记本电脑', '高性能办公笔记本', 5999.0, 10)")) {
qWarning() << "插入初始产品数据失败:" << query.lastError().text();
m_db.rollback();
return false;
}
if (!query.exec("INSERT OR IGNORE INTO products (id, name, description, price, stock) "
"VALUES (2, '手机', '智能旗舰手机', 3999.0, 20)")) {
qWarning() << "插入初始产品数据失败:" << query.lastError().text();
m_db.rollback();
return false;
}
if (!query.exec("INSERT OR IGNORE INTO products (id, name, description, price, stock) "
"VALUES (3, '鼠标', '无线光电鼠标', 99.0, 50)")) {
qWarning() << "插入初始产品数据失败:" << query.lastError().text();
m_db.rollback();
return false;
}
// 提交事务
if (!m_db.commit()) {
qWarning() << "提交事务失败:" << m_db.lastError().text();
return false;
}
qInfo() << "数据库表结构准备完毕";
return true;
}
4. User / UserDao:用户实体与访问对象
include/user.h
cpp
class User
{
public:
User();
explicit User(const QVariantMap &map);
int id() const { return m_id; }
void setId(int id) { m_id = id; }
QString username() const { return m_username; }
void setUsername(const QString &username) { m_username = username; }
QString password() const { return m_password; }
void setPassword(const QString &password) { m_password = password; }
QString email() const { return m_email; }
void setEmail(const QString &email) { m_email = email; }
bool isActive() const { return m_isActive; }
void setIsActive(bool isActive) { m_isActive = isActive; }
QDateTime createdAt() const { return m_createdAt; }
void setCreatedAt(const QDateTime &createdAt) { m_createdAt = createdAt; }
// 密码哈希相关方法
static QString hashPassword(const QString &password);
static bool verifyPassword(const QString &password, const QString &hashedPassword);
QVariantMap toVariantMap() const;
void setProperty(const QString &name, const QVariant &value);
private:
int m_id = 0;
QString m_username;
QString m_password;
QString m_email;
bool m_isActive = true;
QDateTime m_createdAt;
};
src/user.cpp
cpp
#include "user.h"
User::User()
{
}
User::User(const QVariantMap &map)
{
setProperty("id", map.value("id"));
setProperty("username", map.value("username"));
setProperty("password", map.value("password"));
setProperty("email", map.value("email"));
setProperty("is_active", map.value("is_active"));
setProperty("created_at", map.value("created_at"));
}
QString User::hashPassword(const QString &password)
{
QByteArray hash = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256);
return hash.toHex();
}
bool User::verifyPassword(const QString &password, const QString &hashedPassword)
{
return hashPassword(password) == hashedPassword;
}
QVariantMap User::toVariantMap() const
{
QVariantMap map;
map["id"] = m_id;
map["username"] = m_username;
map["password"] = m_password;
map["email"] = m_email;
map["is_active"] = m_isActive;
map["created_at"] = m_createdAt;
return map;
}
void User::setProperty(const QString &name, const QVariant &value)
{
if (name == "id") {
m_id = value.toInt();
} else if (name == "username") {
m_username = value.toString();
} else if (name == "password") {
m_password = value.toString();
} else if (name == "email") {
m_email = value.toString();
} else if (name == "is_active") {
m_isActive = value.toBool();
} else if (name == "created_at") {
m_createdAt = value.toDateTime();
}
}
include/userdao.h
cpp
#pragma once
#include <QObject>
#include <QList>
#include <QMutex>
#include "user.h"
class QSqlDatabase;
/**
* @brief 用户数据访问对象
*/
class UserDao : public QObject
{
Q_OBJECT
public:
explicit UserDao(QObject *parent = nullptr);
bool insert(const User &user);
bool update(const User &user);
bool remove(int userId); // 软删除:is_active = 0
User getById(int userId);
QList<User> getAll();
QList<User> getByUsername(const QString &username);
QList<User> getByEmail(const QString &email);
QStringList getAllUsernames();
bool validateLogin(const QString &username, const QString &password);
private:
QSqlDatabase database();
QMutex m_mutex;
};
src/userdao.cpp
cpp
#include "userdao.h"
#include "databaseconnection.h"
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
UserDao::UserDao(QObject *parent)
: QObject(parent)
{
}
QSqlDatabase UserDao::database()
{
return DatabaseConnection::instance().database();
}
bool UserDao::insert(const User &user)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(database());
query.prepare(
"INSERT INTO users (username, password, email, is_active) "
"VALUES (:username, :password, :email, :isActive)"
);
query.bindValue(":username", user.username());
query.bindValue(":password", User::hashPassword(user.password()));
query.bindValue(":email", user.email());
query.bindValue(":isActive", user.isActive());
if (!query.exec()) {
qWarning() << "插入用户失败:" << query.lastError().text();
return false;
}
int id = query.lastInsertId().toInt();
const_cast<User*>(&user)->setId(id);
return true;
}
bool UserDao::update(const User &user)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(database());
query.prepare(
"UPDATE users SET "
"username = :username, "
"password = :password, "
"email = :email, "
"is_active = :isActive "
"WHERE id = :id"
);
query.bindValue(":id", user.id());
query.bindValue(":username", user.username());
query.bindValue(":password", User::hashPassword(user.password()));
query.bindValue(":email", user.email());
query.bindValue(":isActive", user.isActive());
if (!query.exec()) {
qWarning() << "更新用户失败:" << query.lastError().text();
return false;
}
return true;
}
bool UserDao::remove(int userId)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(database());
query.prepare("UPDATE users SET is_active = 0 WHERE id = :id");
query.bindValue(":id", userId);
if (!query.exec()) {
qWarning() << "删除用户失败:" << query.lastError().text();
return false;
}
return true;
}
User UserDao::getById(int userId)
{
QMutexLocker locker(&m_mutex);
User user;
QSqlQuery query(database());
query.prepare("SELECT * FROM users WHERE id = :id AND is_active = 1");
query.bindValue(":id", userId);
if (query.exec() && query.next()) {
user.setId(query.value("id").toInt());
user.setUsername(query.value("username").toString());
user.setPassword(query.value("password").toString());
user.setEmail(query.value("email").toString());
user.setIsActive(query.value("is_active").toBool());
user.setCreatedAt(query.value("created_at").toDateTime());
}
return user;
}
QList<User> UserDao::getAll()
{
QMutexLocker locker(&m_mutex);
QList<User> list;
QSqlQuery query(database());
query.prepare("SELECT * FROM users WHERE is_active = 1 ORDER BY id");
if (query.exec()) {
while (query.next()) {
User user;
user.setId(query.value("id").toInt());
user.setUsername(query.value("username").toString());
user.setPassword(query.value("password").toString());
user.setEmail(query.value("email").toString());
user.setIsActive(query.value("is_active").toBool());
user.setCreatedAt(query.value("created_at").toDateTime());
list.append(user);
}
}
return list;
}
QList<User> UserDao::getByUsername(const QString &username)
{
QMutexLocker locker(&m_mutex);
QList<User> list;
QSqlQuery query(database());
query.prepare("SELECT * FROM users WHERE username LIKE :u AND is_active = 1");
query.bindValue(":u", "%" + username + "%");
if (query.exec()) {
while (query.next()) {
User user;
user.setId(query.value("id").toInt());
user.setUsername(query.value("username").toString());
user.setPassword(query.value("password").toString());
user.setEmail(query.value("email").toString());
user.setIsActive(query.value("is_active").toBool());
user.setCreatedAt(query.value("created_at").toDateTime());
list.append(user);
}
}
return list;
}
QList<User> UserDao::getByEmail(const QString &email)
{
QMutexLocker locker(&m_mutex);
QList<User> list;
QSqlQuery query(database());
query.prepare("SELECT * FROM users WHERE email LIKE :e AND is_active = 1");
query.bindValue(":e", "%" + email + "%");
if (query.exec()) {
while (query.next()) {
User user;
user.setId(query.value("id").toInt());
user.setUsername(query.value("username").toString());
user.setPassword(query.value("password").toString());
user.setEmail(query.value("email").toString());
user.setIsActive(query.value("is_active").toBool());
user.setCreatedAt(query.value("created_at").toDateTime());
list.append(user);
}
}
return list;
}
QStringList UserDao::getAllUsernames()
{
QMutexLocker locker(&m_mutex);
QStringList names;
QSqlQuery query(database());
query.prepare("SELECT username FROM users WHERE is_active = 1 ORDER BY username");
if (query.exec()) {
while (query.next()) {
names.append(query.value(0).toString());
}
}
return names;
}
bool UserDao::validateLogin(const QString &username, const QString &password)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(database());
query.prepare("SELECT password FROM users "
"WHERE username = :u AND is_active = 1");
query.bindValue(":u", username);
if (query.exec() && query.next()) {
QString hashedPassword = query.value(0).toString();
return User::verifyPassword(password, hashedPassword);
}
return false;
}
5. Product / ProductDao:产品实体与访问对象(结构类似)
出于篇幅考虑,这里不再逐行解释,只给出核心代码,逻辑和 User 部分类似。
include/product.h
cpp
class Product
{
public:
Product();
explicit Product(const QVariantMap &map);
int id() const { return m_id; }
void setId(int id) { m_id = id; }
QString name() const { return m_name; }
void setName(const QString &name) { m_name = name; }
QString description() const { return m_description; }
void setDescription(const QString &description) { m_description = description; }
double price() const { return m_price; }
void setPrice(double price) { m_price = price; }
int stock() const { return m_stock; }
void setStock(int stock) { m_stock = stock; }
QDateTime createdAt() const { return m_createdAt; }
void setCreatedAt(const QDateTime &createdAt) { m_createdAt = createdAt; }
QDateTime updatedAt() const { return m_updatedAt; }
void setUpdatedAt(const QDateTime &updatedAt) { m_updatedAt = updatedAt; }
QVariantMap toVariantMap() const;
void setProperty(const QString &name, const QVariant &value);
private:
int m_id = 0;
QString m_name;
QString m_description;
double m_price = 0.0;
int m_stock = 0;
QDateTime m_createdAt;
QDateTime m_updatedAt;
};
src/product.cpp
cpp
#include "product.h"
Product::Product()
{
}
Product::Product(const QVariantMap &map)
{
setProperty("id", map.value("id"));
setProperty("name", map.value("name"));
setProperty("description", map.value("description"));
setProperty("price", map.value("price"));
setProperty("stock", map.value("stock"));
setProperty("created_at", map.value("created_at"));
setProperty("updated_at", map.value("updated_at"));
}
QVariantMap Product::toVariantMap() const
{
QVariantMap map;
map["id"] = m_id;
map["name"] = m_name;
map["description"] = m_description;
map["price"] = m_price;
map["stock"] = m_stock;
map["created_at"] = m_createdAt;
map["updated_at"] = m_updatedAt;
return map;
}
void Product::setProperty(const QString &name, const QVariant &value)
{
if (name == "id") {
m_id = value.toInt();
} else if (name == "name") {
m_name = value.toString();
} else if (name == "description") {
m_description = value.toString();
} else if (name == "price") {
m_price = value.toDouble();
} else if (name == "stock") {
m_stock = value.toInt();
} else if (name == "created_at") {
m_createdAt = value.toDateTime();
} else if (name == "updated_at") {
m_updatedAt = value.toDateTime();
}
}
include/productdao.h
cpp
class QSqlDatabase;
class ProductDao : public QObject
{
Q_OBJECT
public:
explicit ProductDao(QObject *parent = nullptr);
bool insert(const Product &product);
bool update(const Product &product);
bool remove(int productId);
Product getById(int productId);
QList<Product> getAll();
QList<Product> getByName(const QString &name);
QList<Product> getByPriceRange(double minPrice, double maxPrice);
QList<Product> getLowStockProducts(int threshold);
bool updateStock(int productId, int quantity);
private:
QSqlDatabase database();
QMutex m_mutex;
};
src/productdao.cpp
cpp
ProductDao::ProductDao(QObject *parent)
: QObject(parent)
{
}
QSqlDatabase ProductDao::database()
{
return DatabaseConnection::instance().database();
}
bool ProductDao::insert(const Product &product)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(database());
query.prepare(
"INSERT INTO products (name, description, price, stock, created_at, updated_at) "
"VALUES (:name, :description, :price, :stock, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
);
query.bindValue(":name", product.name());
query.bindValue(":description", product.description());
query.bindValue(":price", product.price());
query.bindValue(":stock", product.stock());
if (!query.exec()) {
qWarning() << "插入产品失败:" << query.lastError().text();
return false;
}
int id = query.lastInsertId().toInt();
const_cast<Product*>(&product)->setId(id);
return true;
}
bool ProductDao::update(const Product &product)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(database());
query.prepare(
"UPDATE products SET "
"name = :name, "
"description = :description, "
"price = :price, "
"stock = :stock, "
"updated_at = CURRENT_TIMESTAMP "
"WHERE id = :id"
);
query.bindValue(":id", product.id());
query.bindValue(":name", product.name());
query.bindValue(":description", product.description());
query.bindValue(":price", product.price());
query.bindValue(":stock", product.stock());
if (!query.exec()) {
qWarning() << "更新产品失败:" << query.lastError().text();
return false;
}
return true;
}
bool ProductDao::remove(int productId)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(database());
query.prepare("DELETE FROM products WHERE id = :id");
query.bindValue(":id", productId);
if (!query.exec()) {
qWarning() << "删除产品失败:" << query.lastError().text();
return false;
}
return true;
}
Product ProductDao::getById(int productId)
{
QMutexLocker locker(&m_mutex);
Product product;
QSqlQuery query(database());
query.prepare("SELECT * FROM products WHERE id = :id");
query.bindValue(":id", productId);
if (query.exec() && query.next()) {
product.setId(query.value("id").toInt());
product.setName(query.value("name").toString());
product.setDescription(query.value("description").toString());
product.setPrice(query.value("price").toDouble());
product.setStock(query.value("stock").toInt());
product.setCreatedAt(query.value("created_at").toDateTime());
product.setUpdatedAt(query.value("updated_at").toDateTime());
}
return product;
}
QList<Product> ProductDao::getAll()
{
QMutexLocker locker(&m_mutex);
QList<Product> list;
QSqlQuery query(database());
query.prepare("SELECT * FROM products ORDER BY id");
if (query.exec()) {
while (query.next()) {
Product product;
product.setId(query.value("id").toInt());
product.setName(query.value("name").toString());
product.setDescription(query.value("description").toString());
product.setPrice(query.value("price").toDouble());
product.setStock(query.value("stock").toInt());
product.setCreatedAt(query.value("created_at").toDateTime());
product.setUpdatedAt(query.value("updated_at").toDateTime());
list.append(product);
}
}
return list;
}
QList<Product> ProductDao::getByName(const QString &name)
{
QMutexLocker locker(&m_mutex);
QList<Product> list;
QSqlQuery query(database());
query.prepare("SELECT * FROM products WHERE name LIKE :n ORDER BY name");
query.bindValue(":n", "%" + name + "%");
if (query.exec()) {
while (query.next()) {
Product product;
product.setId(query.value("id").toInt());
product.setName(query.value("name").toString());
product.setDescription(query.value("description").toString());
product.setPrice(query.value("price").toDouble());
product.setStock(query.value("stock").toInt());
product.setCreatedAt(query.value("created_at").toDateTime());
product.setUpdatedAt(query.value("updated_at").toDateTime());
list.append(product);
}
}
return list;
}
QList<Product> ProductDao::getByPriceRange(double minPrice, double maxPrice)
{
QMutexLocker locker(&m_mutex);
QList<Product> list;
QSqlQuery query(database());
query.prepare("SELECT * FROM products WHERE price >= :min AND price <= :max "
"ORDER BY price");
query.bindValue(":min", minPrice);
query.bindValue(":max", maxPrice);
if (query.exec()) {
while (query.next()) {
Product product;
product.setId(query.value("id").toInt());
product.setName(query.value("name").toString());
product.setDescription(query.value("description").toString());
product.setPrice(query.value("price").toDouble());
product.setStock(query.value("stock").toInt());
product.setCreatedAt(query.value("created_at").toDateTime());
product.setUpdatedAt(query.value("updated_at").toDateTime());
list.append(product);
}
}
return list;
}
QList<Product> ProductDao::getLowStockProducts(int threshold)
{
QMutexLocker locker(&m_mutex);
QList<Product> list;
QSqlQuery query(database());
query.prepare("SELECT * FROM products WHERE stock < :t ORDER BY stock");
query.bindValue(":t", threshold);
if (query.exec()) {
while (query.next()) {
Product product;
product.setId(query.value("id").toInt());
product.setName(query.value("name").toString());
product.setDescription(query.value("description").toString());
product.setPrice(query.value("price").toDouble());
product.setStock(query.value("stock").toInt());
product.setCreatedAt(query.value("created_at").toDateTime());
product.setUpdatedAt(query.value("updated_at").toDateTime());
list.append(product);
}
}
return list;
}
bool ProductDao::updateStock(int productId, int quantity)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(database());
query.prepare("UPDATE products "
"SET stock = stock + :q, updated_at = CURRENT_TIMESTAMP "
"WHERE id = :id");
query.bindValue(":q", quantity);
query.bindValue(":id", productId);
if (!query.exec()) {
qWarning() << "更新库存失败:" << query.lastError().text();
return false;
}
return true;
}
6. MainWindow:简单的用户/产品管理界面
为了让这套数据库访问层"有肉眼可见的反馈",我们做了一个不算花哨但足够实用的主窗口:
- 用户标签页:增删改查用户;
- 产品标签页:增删改查产品;
- 底部有数据库初始化、备份、还原按钮;
- 状态栏显示简要状态和日志信息。
include/mainwindow.h
cpp
class UserDao;
class ProductDao;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
private slots:
void onTabChanged(int index);
// 用户管理
void onUserAdd();
void onUserEdit();
void onUserDelete();
void onUserSearch();
void onUserTableClicked(const QModelIndex &index);
// 产品管理
void onProductAdd();
void onProductEdit();
void onProductDelete();
void onProductSearch();
void onProductTableClicked(const QModelIndex &index);
// 数据库操作
void onInitializeDB();
void onBackupDB();
void onRestoreDB();
void onClearLog();
private:
void setupUi();
void setupConnections();
void initUserTable();
void initProductTable();
void refreshUserTable();
void refreshProductTable();
void showDbStatus(const QString &status);
void log(const QString &message);
void showUserDialog(bool editMode = false);
void showProductDialog(bool editMode = false);
private:
QTabWidget *m_tabWidget = nullptr;
// 用户管理
QPushButton *m_userAddBtn = nullptr;
QPushButton *m_userEditBtn = nullptr;
QPushButton *m_userDeleteBtn = nullptr;
QLineEdit *m_userSearchEdit = nullptr;
QTableWidget *m_userTable = nullptr;
// 产品管理
QPushButton *m_productAddBtn = nullptr;
QPushButton *m_productEditBtn = nullptr;
QPushButton *m_productDeleteBtn = nullptr;
QLineEdit *m_productSearchEdit = nullptr;
QComboBox *m_productSearchType = nullptr;
QTableWidget *m_productTable = nullptr;
// 数据库操作
QPushButton *m_initDbBtn = nullptr;
QPushButton *m_backupDbBtn = nullptr;
QPushButton *m_restoreDbBtn = nullptr;
QProgressBar *m_progress = nullptr;
QLabel *m_statusLabel = nullptr;
QLabel *m_logText = nullptr;
UserDao *m_userDao = nullptr;
ProductDao *m_productDao = nullptr;
int m_currentUserId = -1;
int m_currentProductId = -1;
};
src/mainwindow.cpp
这部分代码较长,有需要可以和我要完整代码。
- 构造函数里调用
setupUi()/setupConnections(); onInitializeDB()里调用DatabaseConnection::instance().initialize(...),同时创建UserDao/ProductDao;refreshUserTable()/refreshProductTable()分别调用 DAO 查询全部数据,填入QTableWidget;showUserDialog()/showProductDialog()用对话框编辑实体,再调用 DAO 的insert()/update()。
配合 main.cpp:
src/main.cpp
cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QApplication::setApplicationName("DatabaseDemo");
QApplication::setOrganizationName("QtAdvancedSeries");
a.setStyle("Fusion");
QString dbDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(dbDir);
qInfo() << "数据库文件将保存在:" << dbDir;
MainWindow w;
w.show();
return a.exec();
}
四、实战中的坑与优化:在项目里要注意的几件事
结合上面这套示例工程,再说几条在实际项目里非常实用的注意点。
1. 连接名与多线程
QSqlDatabase::addDatabase要指定连接名,否则多个地方都用默认连接会互相覆盖;- 不要在一个线程里创建连接,在另一个线程里用它(除非你完全理解 Qt 文档里关于
QSqlDatabase的那段说明); - 如果要在后台线程做数据库操作,给每个线程单独创建连接,并用线程 ID 作为连接名区分。
2. 预处理语句是底线
只要涉及用户输入,就一定要用 prepare + bindValue:
cpp
query.prepare("SELECT * FROM users WHERE username = :u");
query.bindValue(":u", username);
避免直接把字符串拼进 SQL。这既是安全底线,也是可维护性底线。
3. DAO 层里可以适度封装业务规则
比如用户删除,用软删除(is_active = 0)还是硬删除(DELETE),可以在 DAO 里统一决策。
这样上层业务就只关心"调用 remove()",不会到处去纠结"这里该不该软删"。
4. 大量数据写入:记得用事务
示例里为了简洁没有演示事务,在实际项目中:
cpp
QSqlDatabase db = DatabaseConnection::instance().database();
db.transaction();
bool ok = true;
// 多条 insert/update
if (ok) db.commit();
else db.rollback();
一次性插入上万条记录时,是否用事务,性能差别可能是一个数量级。
5. 结构演进:给未来留一个 migrations 表
如果你的项目有长期演进计划,可以从一开始就建一个简单的 migrations 表,记录当前数据库版本,未来做表结构变更时就能按版本渐进升级,而不是暴力删库重建。
五、小结:项目里可以直接执行的一套数据库实践方案
最后把这一期的重点压缩成几条"可以写进团队规范"的落地条款:
- 所有数据库访问都通过 DatabaseConnection 单例拿连接,不允许任何模块私自 addDatabase。
- 一张表一个 Entity 类 + 一个 DAO 类,上层业务只和 DAO 交互,不直接写 SQL。
- 任何带参数的 SQL 都必须使用预处理语句和占位符,严禁字符串拼接注入。
- DAO 内部要用 QMutex 保护共享连接,或者为每个线程创建独立连接。
- 所有 exec() 都要检查返回值,并在日志中记录失败原因(
lastError().text())。 - 批量写入必须使用事务;大查询要配合索引和合理的 where 条件。
- 数据库初始化、备份、还原则通过集中入口实现,不在业务代码里"偷偷"复制 .db 文件。
你可以直接把本文里的 DatabaseDemo 工程代码拉进自己的项目里,一开始就按这个结构来做数据访问层。
等整个项目跑顺了,再往里加事务封装、缓存、迁移脚本等高级能力,完全不影响现在的骨架。
这一期到这里,Qt5 进阶专题的"底座能力"基本就齐活了:信号槽、对象生命周期、事件循环、元对象、多线程、文件、网络、数据库。
后面可以开始考虑更上层的话题:模块划分、插件化、性能分析、UI 架构(MVVM/MVP),把这些能力串成一个长期可维护的桌面应用。