之前写的笔记里有写在qt中数据库如何使用,数据库相关代码编写完毕,软件运行也正常。
但是在项目需求中需要设置1s保存一次数据存储10000组数据,当一秒保存一次的时候,程序就会卡顿。本篇文章主要解决使用数据库耗时操作时主界面卡顿问题。
文章的内容涉及到了多线程任务,定时器,线程池QThreadPool,任务类QRunable的理解,以及数据库的具体详细操作。
目录
2.知识点,关于线程池QThreadPool,以及任务类Qrunnable,QConcuurent,以及自定义线程类的一些理解与使用
3.1error executing query: "Driver not loaded Driver not loaded"
3.2引发了异常: 读取访问权限冲突。 this->**** 是 nullptr
1.整体逻辑以及超详细代码
1.1在需要保存数据的时候,发射保存数据信号
//处理存储在bms数据库中的数据
processBmsData(addrArray,swapBcmuArray, swapBmuArray, swapHtemArray, bmsParamArray);
//发射信号
emit signalBmsData();
1.2在主界面的构造函数中,连接保存数据的信号与槽
connect(this, &YF_bcmuInfo::signalBmsData, this, &YF_bcmuInfo::slotBmsData);
1.3在主界面中编写槽函数
在此槽函数中,开启一个定时器,设置多久保存一次数据。
只有第一次触发未开启的时候才打开定时器。所以每次触发这个保存数据槽函数的时候需要判断定时器是否开启。
void YF_bcmuInfo::slotBmsData() { //在此槽函数中开启一个定时器,用来决定几秒存储一次。每次触发了这个槽函数,首先判断定时器是否开启 if (!sqlTimer->isActive()&& flagManualLock) { //表明已获取新数据,启动定时器,1s/2s/3s保存一次存储数据 sqlTimer->start(); } } //flagManualLock是我设置的一个标志位,其为true才进行下一步,因为之后,可能会有数据库连接不上的情况,数据库多次尝试连接失败,设置此标志位为false,打开失败就关闭定时器不要再次打开了,不然就会反复不断的去打开根本不可能打开的数据库
记得使用此定时器变量之前在主界面的构造函数中初始化
//这个定时器用来决定几秒存一次数据到数据库中,默认为1s存储一次 sqlTimer = new QTimer(this); sqlTimer->setInterval(1000);
1.4在主界面构造函数中连接定时器以及其对应的槽函数
void YF_bcmuInfo::slotSqlTimeOut()
{
//创建一个任务,在此任务类中实现数据库保存数据的逻辑
//给这个任务类传入this,这个主界面类对象,因为要在任务类中调用主界面类的函数
//这里用线程池还有一个很重要的原因,因为这里是保存基础数据的逻辑,在程序的其他地方还有另外的逻辑只有数据有变化时才保存,不同的逻辑使用不同的任务,在不同的地方触发。
SqlOperationRunnable* runnable = new SqlOperationRunnable(this);
// 提交任务给线程池执行
//threadPool是我定义的一个线程池对象作为主界面类的成员变量使用
threadPool->start(runnable);
//runnable 对象会在任务执行完毕后由 QThreadPool 负责管理和释放
//所以不需要担心内存泄露的问题
//这是另一种方式在2.1中会解释
//QtConcurrent::run(this, &YF_bcmuInfo::handleDatabaseOperation);
}
1.5任务类的定义和实现
继承于QRunable,实现一个自定义任务类
//SqlOperationRunable.h
#pragma once
#include <qrunnable.h>
#include "YF_bcmuInfo.h"
//保存基础数据任务类
//由于在YF_bcmuInfo类中需要引用这个类的头文件,在这个类中也需要引用YF_bcmuInfo的头文件,有循环包含的问题,使用class YF_bcmuInfo,进行前向声明
class YF_bcmuInfo;
class SqlOperationRunnable :
public QRunnable
{
public:
SqlOperationRunnable(YF_bcmuInfo* parent = nullptr);
void run() override;
private:
YF_bcmuInfo* m_parent;
};
//SqlOperationRunnable.cpp
#include "SqlOperationRunnable.h"
SqlOperationRunnable::SqlOperationRunnable(YF_bcmuInfo* parent)
: m_parent(parent)
{
//在构造函数的初始化列表中传入主界面类的对象parent赋值给m_parent
}
void SqlOperationRunnable::run()
{
//在这里调用主界面类的函数
m_parent->handleDatabaseOperation();
}
1.6主界面类关于任务类具体操作的函数
void YF_bcmuInfo::handleDatabaseOperation()
{
QMutexLocker locker(&sqlMutex);
QDateTime currentDateTime = QDateTime::currentDateTime();
QString uniqueName = "TEST_" + currentDateTime.toString("yyyy-MM-dd hh:mm:ss.zzz");
QDataBaseManager dbTest(uniqueName);
//qDebug() << "保存基础数据线程" << QThread::currentThreadId();
//qDebug() << "当前操作的数据库对象" << dbTest.db<<&dbTest.db;
QVector<QString> bmsSqlData=getBmsData();
if (bmsSqlData.isEmpty()) {
return;
}
QVector<QString> bmsSqlDataEnd;
for (int i = 2; i < bmsSqlData.size();++i) {
bmsSqlDataEnd.append(bmsSqlData[i]);
}
bool bOk=false;
if (!dbTest.dbIsOpen())
{
bOk= dbTest.open();
if (!bOk) {
qDebug() << "dataBase connect error";
//如果没有打开再次尝试打开
bOk = dbTest.open();
if (!bOk) {
//关闭定时器不需要再保存数据
sqlTimer->stop();
flagManualLock = false;
return;
}
}
}
else
{
bOk = true;
}
if (bOk) {
if (bmsSqlData.size() >= 2) {
QString basicDataTaName = "bcmu" + bmsSqlData[1] + "_" + "电池包" + bmsSqlData[0] + "_基础数据";
dbTest.createTable(basicDataTaName, bms_basic_header);
dbTest.insertDataToTable(basicDataTaName, bmsSqlDataEnd);
//bmsDb->exportTableToExcel();
}
}
}
1.7自定义数据库类
#pragma once
#include <QObject>
#include <QtSql/QSqlDatabase>
#include <qDebug>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlRecord>
#include <QtXlsx>
#include <QApplication>
#include <QMessageBox>
#include <QProgressDialog>
#include <QtConcurrent>
#include "SignalManager.h"
#include <QMutex>
#include <QMutexLocker>
#include <QFutureWatcher>
//表示数据库中存几组
#define MAX_ROW_COUNT 10000
class QDataBaseManager : public QObject
{
Q_OBJECT
public:
bool dbIsOpen();
QDataBaseManager( QString connectName,QObject* parent = nullptr );
~QDataBaseManager();
//static QDataBaseManager* getDataBase();
void createTable(QString tableName,const QStringList& bms_header);
void insertDataToTable(const QString& tableName, const QVector<QString>& data);
bool open();
bool openTest();
void exportTableToExcel();
private:
QString sanitizeColumnName(const QString& columnName) {
// 替换括号和空格为下划线
QString sanitized = columnName;
sanitized.replace("(", "_");
sanitized.replace(")", "_");
sanitized.replace(" ", "_");
return sanitized;
}
private:
//static QDataBaseManager *bmsDb;
QSqlDatabase db;
QMutex sqlMutex;
};
#include "QDataBaseManager.h"
QDataBaseManager::QDataBaseManager(QString connectName, QObject *parent)
: QObject(parent)
{
db = QSqlDatabase::addDatabase("QMYSQL",connectName);
//db = QSqlDatabase::addDatabase("QMYSQL");
db.setHostName("47.98.20.112");
//db.setHostName("localhost");
db.setDatabaseName("bmsdatabase");
db.setUserName("root");
db.setPassword("root");
// 设置连接超时时间为3秒
db.setConnectOptions(QString("MYSQL_OPT_CONNECT_TIMEOUT=3"));
/* db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("bmsdatabase.sqlite");*/
}
void QDataBaseManager::createTable(QString tableName,const QStringList& bms_header) {
//创建表语句 create table if not exists tablename(`columnname1` 数据格式,`columnname2` 数据类型);
//判断列名列表是否为空
//构建创建表的语句 语句中由于列名有特殊符号%等所以要使用反引号,反引号还是不行那就需要把()和空格变为_,由于列名不能重复,所以需要给每一项前面加上唯一的数字
//构建的时候最后一项数据是时间戳 默认值current_timestamp
//创建qsqlquery查询对象
//执行操作,成功打印成功,失败打印失败
qDebug() << "创建表格" << QThread::currentThreadId();
qDebug() << "当前操作的数据库对象" << db << &db;
if (bms_header.isEmpty()) {
return;
}
QString createTableQuery = "CREATE TABLE IF NOT EXISTS " + tableName + "(";
for (int i = 0; i < bms_header.size(); ++i) {
QString columnName = QString::number(i + 1) + "_" + sanitizeColumnName(bms_header[i]);
createTableQuery += "`" + columnName + "` TEXT, ";
}
//createTableQuery += "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP";
createTableQuery += "created_at TEXT";
createTableQuery += ")";
QSqlQuery createQuery(db);
if (createQuery.exec(createTableQuery)) {
qDebug() << "create table"<<tableName<<" ok";
}
else
{
qDebug() << "create table" <<tableName<< "error"<< createQuery.lastError().text();
}
}
void QDataBaseManager::insertDataToTable(const QString& tableName, const QVector<QString>& data) {
//插入数据语句 insert into tablename(`columnname1` 数据格式,`columnname2` 数据格式)values(:value1,:value2);
//已知表的名字,需要先获取表的列名集合,record.count()
//构建语句,要注意构建语句的时候,创建的value个数要和列数相匹配,要想最后一列时间戳数据显示默认值,不用给它绑定value,直接不给最后一列设置数据就会显示默认数据
//QSqlQuery prepare 绑定要插入的数据
//exec执行
qDebug() << "插入数据" << QThread::currentThreadId();
qDebug() << "当前操作的数据库对象" << db << &db;
//获取行数
QSqlQuery countQuery("SELECT COUNT(*) FROM" + tableName,db);
int rowCount;
if (countQuery.next()) {
rowCount = countQuery.value(0).toInt();
}
//获取列名
QStringList columnNames;
QSqlQuery query("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" + tableName + "'",db);
while (query.next()) {
QString columnName = query.value(0).toString();
columnNames << columnName;
}
QString insertTableQuery = "INSERT INTO " + tableName + " (";
for (const QString& columnName : columnNames) {
insertTableQuery += "`" + columnName + "` ,";
}
insertTableQuery.chop(1);
insertTableQuery += ") VALUES (";
for (int i = 0; i < columnNames.size(); ++i) {
insertTableQuery += ":value" + QString::number(i + 1)+",";
}
insertTableQuery.chop(1);
insertTableQuery += ")";
QSqlQuery bmsInsertQuery(db);
bmsInsertQuery.prepare(insertTableQuery);
//data中本来就没有最后一项数据
for (int i = 0; i < data.size(); ++i) {
bmsInsertQuery.bindValue(":value" + QString::number(i + 1), data[i]);
}
//显式插入时间值 因为timestamp需要的格式是yy-MM-dd hh:mm:ss格式,所以qdatetime需要转换为字符串
//然后给列的最后一行 created_at设置参数
QDateTime currentDateTime = QDateTime::currentDateTime();
QString formattedDateTime = currentDateTime.toString("yyyy-MM-dd hh:mm:ss.zzz");
bmsInsertQuery.bindValue(":value" + QString::number(columnNames.size()), formattedDateTime);
if (bmsInsertQuery.exec()) {
qDebug() << "insert into " << tableName << "ok";
}
else {
qDebug() << "insert into " << tableName << "error"<< bmsInsertQuery.lastError ().text();
}
//获得总行数 SELECT COUNT(*) FROM tablename
//判断是否超过20行 if 超过20行
//根据created_at排序,获得第一组数据的created_at值 SELECT created_at tablename ORDER BY created_at ASC LIMIT 1
//删除刚刚获取的created_at这一行的值 DELETE FROM tablename WHERE created_at=刚刚获取的值;
QSqlQuery totalCountQuery("SELECT COUNT(*) FROM " + tableName,db);
if (totalCountQuery.next()) {
int totalCount = totalCountQuery.value(0).toInt();
if (totalCount > MAX_ROW_COUNT) {
QSqlQuery firstDataQuery("SELECT created_at FROM " + tableName + " ORDER BY created_at ASC LIMIT 1",db);
if (firstDataQuery.next()) {
QString firstData = firstDataQuery.value(0).toString();
//QString firstData = firstDataQuery.value(0).toDateTime().toString("yyyy-MM-dd HH:mm:ss");
QSqlQuery deleteQuery(db);
QString deleteString="DELETE FROM " + tableName + " WHERE created_at = :created_at";
deleteQuery.prepare(deleteString);
deleteQuery.bindValue(":created_at", firstData);
if(deleteQuery.exec() ){
qDebug() << "delete ok" << firstData;
}
else {
qDebug() << "delete error" << firstData<< deleteQuery.lastError().text();
}
}
}
}
}
void QDataBaseManager::exportTableToExcel()
{
//获取数据,所有数据库表格
//写入数据,遍历所有表格,创建qxlsx对象,将数据库表格的信息写入xlsx
//保存,获取当前应用程序运行路径,创建文件夹,在该文件夹中添加一个xlsx文件
QStringList tableNames = db.tables();
int tableNumbers = tableNames.count();
//发送信号创建保存数据进度条
//emit signalCreateProgress();
SignalManager::getInstance()->sendCreateProgress(tableNumbers);
int currentIndex = 0;
for (QString& tableName : tableNames) {
++currentIndex;
QString currentText = QString(" %1( %2/%3)").arg(tableName).arg(currentIndex).arg(tableNumbers);
//QSqlQuery allDataQuery("SELECT * FROM " + tableName, db);
QSqlQuery allDataQuery(db);
allDataQuery.prepare("SELECT * FROM " + tableName);
if (!allDataQuery.exec()) {
qDebug() << "Error executing query:" << allDataQuery.lastError().text();
return;
}
else {
//处理查询结果
QSqlRecord record = allDataQuery.record();
QXlsx::Document xlsx(tableName);
for (int i = 0; i < record.count(); ++i) {
xlsx.write(1, i + 1, record.fieldName(i));
//xlsx.setColumnWidth(i+1, 25);
}
int row = 2;
while (allDataQuery.next()) {
for (int i = 0; i < record.count(); ++i) {
QVariant type = allDataQuery.value(i).type();
if (type == QVariant::DateTime) {
QDateTime dateTime = allDataQuery.value(i).toDateTime();
QString createAt = dateTime.toString("yy-MM-dd HH:mm:ss");
xlsx.write(row, i + 1, createAt);
}
else {
xlsx.write(row, i + 1, allDataQuery.value(i).toString());
}
}
row++;
}
QString currentDir = QDir::currentPath();
QString historyDataDir = "historyData";
if (!QDir().exists(historyDataDir)) {
QDir().mkdir(historyDataDir);
}
QString filePath = currentDir + "/" + historyDataDir + "/" + tableName + ".xlsx";
QFile file(filePath);
if (file.exists() && !file.isOpen()) {
file.remove();
}
xlsx.saveAs(filePath);
//设置进度条的值
SignalManager::getInstance()->sendSetProgressValue(currentIndex, currentText);
}
}
}
//QDataBaseManager* QDataBaseManager::getDataBase()
//{
// if (bmsDb == nullptr) {
// bmsDb = new QDataBaseManager;
// }
// return bmsDb;
//}
bool QDataBaseManager::dbIsOpen()
{
return db.isOpen();
}
bool QDataBaseManager::open()
{
qDebug() << "打开数据库"<<QThread::currentThreadId();
qDebug() << "当前操作的数据库对象" << db << &db;
// 检查当前数据库连接状态,如果已经打开,则直接返回 true
if (dbIsOpen()) {
return true;
}
else {
if (!db.open()) {
// 打开数据库失败,输出错误信息
qDebug() << "Failed to open database:" << db.lastError().text();
return false;
}
return true; // 打开成功
将数据库打开操作放进异步任务。如果打开数据库过程中卡顿,不阻塞主线程
使用 QtConcurrent::run 在一个单独线程中打开数据库
//QFuture<bool> future = QtConcurrent::run([this]() {
// return db.open(); // 尝试打开数据库,并返回操作结果
// });
等待异步操作完成
//while (!future.isFinished()) {
// QCoreApplication::processEvents(QEventLoop::AllEvents);
//}
返回异步操作的结果
//return future.result();
}
}
QDataBaseManager::~QDataBaseManager()
{
db.close();
}
1.8ui界面中让用户自己选择存储数据的时间
1.9编写点击设置保存数据时间按钮时触发的槽函数
void YF_bcmuInfo::on_btnSetInterval_clicked()
{
int intervalNumber;
//获取旁边combobox的当前文本
QString interval = ui.cobBoxInterval->currentText();
//得到后缀
QString suffix=interval.right(1);
//如果后缀是min
if (suffix == "n") {
//去掉后缀
QString numberStr = interval.remove(interval.length() - 3, 3);
// 将提取的数字部分转换为整数
int number = numberStr.toInt(nullptr, 10);
//将数据转为毫秒形式
intervalNumber = number * 6 * 10000;
}
else {
//如果是s,去掉后缀
QString numberStr = interval.remove(interval.length() - 1, 1);
// 将提取的数字部分转换为整数
int number = numberStr.toInt(nullptr, 10);
//转为毫秒形式
intervalNumber = number * 1000;
}
//设置之前成员变量定时器的时间间隔
sqlTimer->setInterval(intervalNumber);
}
2.知识点,关于线程池QThreadPool,以及任务类Qrunnable,QConcuurent,以及自定义线程类的一些理解与使用
在 Qt 中,有几种不同的方式可以实现多线程任务,包括使用 QThreadPool
、自定义线程类,以及 QtConcurrent::run。
2.1QThreadPool
案例中使用到的是QThreadPool结合QRunable自定义的任务类。详细可查看案例中步骤。
详见1.4,1.5,1.6使用方式
QThreadPool:
- 优点:
- Qt 提供的高级线程管理类,能够轻松管理多个任务和线程资源。
- 可以管理和复用线程,减少线程创建和销毁的开销。(是因为线程池会在任务结束之后自动管理线程以及任务内存)
- 适用场景:
- 当需要在应用程序中执行多个并发任务,且希望通过控制线程池的大小来管理系统资源时,
QThreadPool
是一个很好的选择。( //我使用线程池还有一个很重要的原因,因为我的代码中不仅有保存基础数据的并行任务,在程序的其他地方还有另外的逻辑只有数据有变化时才保存,不同的逻辑使用不同的任务,在不同的地方触发。有多种并行任务,使用线程池是个很好的选择)- 特别适用于长期存在的后台任务或者需要长时间运行的任务。(我的代码需要运行非常非常非常久所以优先选择了这种方式)
2.2QtConcurrent::run
案例中的定时器槽函数中注释里有写这个的相关代码!!!记得在使用的时候加入Concurrent模块
void YF_bcmuInfo::slotSqlTimeOut()
{
//将handleDatabaseOperation此函数异步执行
QtConcurrent::run(this, &YF_bcmuInfo::handleDatabaseOperation);
}
- QtConcurrent::run:
- 优点:
- 简化了在多线程环境中执行函数的方式,无需显式创建线程或者管理线程的生命周期。(不同自定义类,也不用使用线程池对象,也不需要自定义任务类)
- 提供了一种简单的并行编程方式,将函数调用转化为并发任务执行。(直接调用函数就可以并发运行这个函数)
- 可以使用 Qt 的信号和槽机制来进行线程间通信。
- 适用场景:
- 当任务可以独立执行,不需要复杂的线程管理或者与其他任务的交互时,QtConcurrent::run 是一个方便的选择。(其实我的代码用这个也可以,但是需要长时间运行,所以选择使用了线程池的方式)
- 适合于简单的并行任务,例如一次性的、相互独立的数据处理或计算任务。
2.3自定义线程类
自定义线程类:
- 优点:
- 可以完全控制线程的生命周期、执行逻辑和线程间通信。(继承于QThread的类,重写run函数,创建线程类的对象,start之后,在后台自己运行的线程)
- 可以根据具体需求实现更复杂的线程行为和交互。
- 适用场景:
- 当需要精确控制线程的行为,例如自定义线程启动、停止和通信时,自定义线程类是一个好的选择。
- 适合于需要定制化、特定需求的线程任务。(可以在run函数中设立标志位控制线程的开启和关闭,我在之前的bms笔记中有写到这个方式)
- 再补充一点,自定义线程类特别适合那些需要从一开始到结束一直运行的后台任务或长期存在的线程,,个人理解仅供参考。从创建对象的时候就在后台运行的线程。
自定义线程类例子:
//.h文件 继承于QThread的线程类,需要重写run函数
#pragma once
#include <qrunnable.h>
#include "YF_bcmuInfo.h"
#include "QDataBaseManager.h"
#include <QVector>
class YF_bcmuInfo;
class SqlOperationRunnable :
public QThread
{
public:
SqlOperationRunnable(YF_bcmuInfo* parent = nullptr);
void run() override;
void stopThread();
private slots:
void saveDataToDatabase();
void stopTimer();
//void slotSaveParam();
private:
YF_bcmuInfo* m_parent;
QTimer sqlTimer;
QDataBaseManager dbTest;
//bool flagStart;
};
//.cpp文件
#include "SqlOperationRunnable.h"
SqlOperationRunnable::SqlOperationRunnable(YF_bcmuInfo* parent)
: m_parent(parent)
{
//开启定时器,在定时器槽函数中实现具体逻辑
sqlTimer.start(1000);
connect(&sqlTimer, &QTimer::timeout, this, &SqlOperationRunnable::saveDataToDatabase);
//连接信号与槽函数收到主界面发送的打开数据库失败的信号,关闭定时器
connect(m_parent, &YF_bcmuInfo::signalOpenDbFalse, this, &SqlOperationRunnable::stopTimer);
}
//重写run函数,开启事件循环,从定义对象时,就在后台运行此线程
void SqlOperationRunnable::run()
{
exec(); // 开始线程事件循环
}
//停止线程函数
void SqlOperationRunnable::stopThread()
{
quit();
}
//定时器的槽函数,调用父类主界面对象的handleDatabaseOperation函数,这个函数的定义在之前的详细代码中有写,就是一些数据库的操作
void SqlOperationRunnable::saveDataToDatabase()
{
// 调用主线程对象的函数,调用主线程对象的函数时有一个参数要传递使用Q_ARG
QMetaObject::invokeMethod(m_parent, "handleDatabaseOperation", Qt::AutoConnection,
Q_ARG(QDataBaseManager&, dbTest));
}
void SqlOperationRunnable::stopTimer()
{
sqlTimer.stop();
}
3.遇到的问题
3.1error executing query: "Driver not loaded Driver not loaded"
这是因为之前在操作数据库的时候只用了一个单例类的数据库对象,驱动的加载需要依赖自己的上下文环境,我们多线程操作的时候需要确保每个线程都操作自己的独立数据库连接对象,以避免多线程并发访问同一个连接对象而导致的竞态条件和数据不一致问题。还有一点就是要注意我们创建数据库对象的时候传了各自不同的连接名,记得在查询的时候绑定连接名,如果不绑定一定会出现driver not load的错误,例如下面这个db是需要传入的。
QSqlQuery totalCountQuery("SELECT COUNT(*) FROM " + tableName,db);
3.2引发了异常: 读取访问权限冲突。 this->**** 是 nullptr
报这个错误,是因为在不同的线程中操作数据库对象,对象虽然是不同的,但是构造函数中addDatabase时,没有创建独属的连接名在两个线程使用了同一个连接,QSqlDatabase::addDatabase 的第二个参数为 const QString &connectionName = QLatin1String(defaultConnection) ,默认值是默认连接,在每次创建数据库对象的时候,传入连接名就不会有这个错误了。db = QSqlDatabase::addDatabase("QMYSQL",connectName);具体的详细代码在上面都有贴上。
3.3烦死人的程序异常---QSqlQuery
int numFields = mysql_field_count(d->drv_d_func()->mysql);已引发异常
在我点击历史记录保存的时候,连续点击十次可能会有一次崩溃,这里提供一种这种错误的解决四路,因为这种异常错误没办法单步调试,调试也还是会直接崩溃
-->首先:大概过一下自己的代码,找出可能出问题的地方,将可能出问题的地方注释掉,运行,如果不报错,那就是这的问题了
-->粗略排除不行的话,就分块隐藏直到找到这个问题所在。
-->如果有跟我一样遇到同样的问题,我的错误是这里:
建议数据库的查询使用prepare函数来准备SQL查询语句,然后通过exec函数执行查询,相比于直接在QSqlQuery的构造函数中传入带有字符串的Sql查询语句更安全。
防止 SQL 注入:
使用 prepare 函数可以帮助预编译 SQL 查询语句,这样能够防止 SQL 注入攻击。如果直接在构造函数中传入带有变量的 SQL 查询语句,存在被恶意构造的可能,尤其是当 tableName 是由用户输入或其他不可控因素决定时。