前言
之前没怎么在项目中使用数据库,对数据库这块只了解一点皮毛,只能说能用。这次涉及了在多线程中使用数据库,看了看源码,和吸取了网上的一些经验,整理封装了一下。
环境
Qt5.15.2
QSqlDatabase原理
因为不太懂数据库连接的使用,就看了源码(Qt5.15.2版本源码),只是简单的看了一下,了解到:数据库连接是保存在一个容器中,可以称之为数据库连接池,它负责分配、管理和释放连接。这样的话,就可以重复使用数据库连接,而不需每次创建新的连接。
创建连接
创建连接源码如下,QConnectionDict 为所说的连接池,在添加连接之前使用了锁(QWriteLocker locker(&dict->lock);),所以它是线程安全的。若是连接名重复,会先移除之前的连接,再添加新的连接。
QSqlDatabase QSqlDatabase::addDatabase(const QString &type, const QString &connectionName)
{QSqlDatabase db(type);QSqlDatabasePrivate::addDatabase(db, connectionName);return db;
}void QSqlDatabasePrivate::addDatabase(const QSqlDatabase &db, const QString &name)
{QConnectionDict *dict = dbDict();Q_ASSERT(dict);QWriteLocker locker(&dict->lock);if (dict->contains(name)) {invalidateDb(dict->take(name), name);qWarning("QSqlDatabasePrivate::addDatabase: duplicate connection name '%s', old ""connection removed.", name.toLocal8Bit().data());}dict->insert(name, db);db.d->connName = name;
}
获取数据库连接
根据连接名获取之前创建的数据库连接,默认打开数据库连接。还可以看到里面有判断之前创建的数据库实例的线程是否为当前线程,如果不是,就返回了空的数据库实例,也就是说创建的数据库实例只能在本线程内使用。
我在网上的博客
https://www.cnblogs.com/findumars/p/5634462.html
看到说
“一个线程创建的数据库对象(如 addDatabase 的返回值)只能在同一线程使用,但是,addDatabase 注册的连接(名字是开发者定)可以跨线程使用,唯一需要注意的是,在调用全局方法的时候,要有原子保护。”
我理解的意思是可以在其他线程中通过连接名获取此连接,也就是使用database(),但是源码也看到了,连接名是跟addDatabase返回的db(数据库对象)成对存在连接池中的,所以就不存在“连接本身用名字可以多线程使用”。
所以,在多线程中使用数据库时候应该保持每个线程都使用一次addDatabase创建连接,创建后,可在当前线程中使用database(connectionName)获取连接,使用数据库。
QSqlDatabase QSqlDatabase::database(const QString& connectionName, bool open)
{return QSqlDatabasePrivate::database(connectionName, open);
}QSqlDatabase QSqlDatabasePrivate::database(const QString& name, bool open)
{const QConnectionDict *dict = dbDict();Q_ASSERT(dict);dict->lock.lockForRead();QSqlDatabase db = dict->value(name);dict->lock.unlock();if (!db.isValid())return db;if (db.driver()->thread() != QThread::currentThread()) {qWarning("QSqlDatabasePrivate::database: requested database does not belong to the calling thread.");return QSqlDatabase();}if (open && !db.isOpen()) {if (!db.open())qWarning() << "QSqlDatabasePrivate::database: unable to open database:" << db.lastError().text();}return db;
}
移除连接
移除连接:只要连接池中有此连接名,就会移除连接。如果有其他地方还在使用,只会警告,此连接该失效还是会失效。
void QSqlDatabase::removeDatabase(const QString& connectionName)
{QSqlDatabasePrivate::removeDatabase(connectionName);
}void QSqlDatabasePrivate::removeDatabase(const QString &name)
{QConnectionDict *dict = dbDict();Q_ASSERT(dict);QWriteLocker locker(&dict->lock);if (!dict->contains(name))return;invalidateDb(dict->take(name), name);
}
void QSqlDatabasePrivate::invalidateDb(const QSqlDatabase &db, const QString &name, bool doWarn)
{if (db.d->ref.loadRelaxed() != 1 && doWarn) {qWarning("QSqlDatabasePrivate::removeDatabase: connection '%s' is still in use, ""all queries will cease to work.", name.toLocal8Bit().constData());db.d->disable();db.d->connName.clear();}
}
有时候,数据库的对象和移除数据库连接在同一个函数内,就会有此警告,如果不想打印此警告,可将数据库的对象用大括号括起来:
将
void test()
{QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE",QString("Two"));db.open();db.close();QSqlDatabase::removeDatabase(QString("Two"));
}
改为
void test()
{{QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE",QString("Two"));db.open();db.close();}QSqlDatabase::removeDatabase(QString("Two"));
}
注意事项
在多线程中操作数据库前,需要注意以下几点:
- QSqlDatabase对象和QSqlQuery 对象只能在创建所在线程内使用。
- 连接使用完后记得删除QSqlDatabase::removeDatabase。
关于资源竞争问题的异常
在网上查资料时,看到“线程内注册与连接数据库的存在竞争问题(Qt 会动态的加载数据库的plugin, 加载 plug in 的部分,涉及到对本地库文件的管理,这一部分,出现了竞争),从 addDatabase / database到 open 的部分,要保证其原子性”。
我看了源码,只有在初始化时,而且仅限addDatabse第一个参数为数据库驱动类型时,使用了插件加载数据库驱动。
本想复现所说的资源竞争的问题:写了一个Demo两个线程内同时执行了数百次addDatabase、open、removeDatabase操作(未加锁),也没出现异常。
但是,害怕在此出问题,所以在后续封装时,还是加了锁。
QSqlDatabase QSqlDatabase::addDatabase(const QString &type, const QString &connectionName)
{QSqlDatabase db(type);QSqlDatabasePrivate::addDatabase(db, connectionName);return db;
}QSqlDatabase::QSqlDatabase(const QString &type)
{d = new QSqlDatabasePrivate(this);d->init(type);
}void QSqlDatabasePrivate::init(const QString &type)
{drvName = type;if (!driver) {DriverDict dict = QSqlDatabasePrivate::driverDict();for (DriverDict::const_iterator it = dict.constBegin();it != dict.constEnd() && !driver; ++it) {if (type == it.key()) {driver = ((QSqlDriverCreatorBase*)(*it))->createObject();}}}if (!driver && loader())driver = qLoadPlugin<QSqlDriver, QSqlDriverPlugin>(loader(), type);if (!driver) {qWarning("QSqlDatabase: %s driver not loaded", type.toLatin1().data());qWarning("QSqlDatabase: available drivers: %s",QSqlDatabase::drivers().join(QLatin1Char(' ')).toLatin1().data());if (QCoreApplication::instance() == nullptr)qWarning("QSqlDatabase: an instance of QCoreApplication is required for loading driver plugins");driver = shared_null()->driver;}
}
封装
根据QSqlDatabase的一些特性,封装了一个单例模式的数据库管理类:
- 由于创建的连接只能在本线程内使用,所以每个线程都会创建一个连接;
- 根据连接名跟数据库连接一一对应的关系,并且线程ID的唯一性,将线程ID作为连接名。
- 考虑到后续释放内存,移除数据库连接,用容器保存数据。
.h文件
class SQLiteHelper
{public:static SQLiteHelper* GetInstance();/*** @brief removeDatabases 释放内存*/static void removeDatabases();private:SQLiteHelper();~ SQLiteHelper();public:/*** @brief insertTableData 表格内插入数据* @param tableName 表名* @param rowData 需要插入的一行数据* @param id 返回的自增的ID值* @return*/bool insertTableData(const QString& tableName,const QVariantMap& rowData ,int& id);...private:static QMutex mutexSql;QString m_strConnName;static QHash<Qt::HANDLE, SQLiteHelper*> databaseMap;//所有数据库链接,key: 线程ID, //value 数据库操作实例指针}
.cpp文件
QMutex SQLiteHelper::mutexSql;
QHash<Qt::HANDLE, SQLiteHelper*> SQLiteHelper::databaseMap;SQLiteHelper *SQLiteHelper::GetInstance()
{if(!databaseMap.contains(QThread::currentThreadId())) {databaseMap.insert(QThread::currentThreadId(), new SQLiteHelper());}return databaseMap[QThread::currentThreadId()];
}void SQLiteHelper::removeDatabases()
{qDebug()<<"SQLiteHelper::removeDatabases()";QList<Qt::HANDLE> keys = databaseMap.keys();for(int i= 0; i<keys.count();i++){Qt::HANDLE id = keys[i];//释放内存delete databaseMap.take(id);QSqlDatabase::removeDatabase(QString::number(long(id)));}
}SQLiteHelper::SQLiteHelper()
{mutexSql.lock();m_strConnName = QString::number((long)QThread::currentThreadId());QSqlDatabase database = QSqlDatabase::addDatabase("QSQLITE", m_strConnName);database.setDatabaseName("car.db");mutexSql.unlock();
}SQLiteHelper::~SQLiteHelper()
{
}bool SQLiteHelper::insertTableData(const QString &tableName, const QVariantMap &rowData,int& id)
{bool r = false;QString fieldNames,placeholderVals;QStringList strList = rowData.keys();for(int i= 0; i< strList.count(); i++){const QString& name = strList[i];fieldNames.append(name);placeholderVals.append(QStringLiteral(":%1").arg(name));if(i != (strList.count()-1)){fieldNames.append(",");placeholderVals.append(",");}}QString sqlStr = QString("INSERT INTO %1 (%2) VALUES (%3);").arg(tableName,fieldNames,placeholderVals);QSqlDatabase sqlDb =QSqlDatabase::database(m_strConnName);if(!sqlDb.isOpen()){mutexSql.lock();sqlDb.open();mutexSql.unlock();}QSqlQuery sqlQuery(sqlDb);sqlQuery.prepare(sqlStr);QMap<QString,QVariant>::const_iterator it = rowData.constBegin();for(; it != rowData.constEnd(); ++it){sqlQuery.bindValue(QString(":%1").arg(it.key()),it.value());}r = sqlQuery.exec();if(r){sqlStr = QString("select last_insert_rowid() from %1").arg(tableName);if(sqlQuery.exec(sqlStr)){if(sqlQuery.next()){id = sqlQuery.value(0).toInt();}}}sqlDb.close();return r;
}...
使用
数据库添加一行数据示例:
QVariantMap varMap;varMap.insert("xm",QStringLiteral("张晓明"));varMap.insert("xb",QStringLiteral("男"));varMap.insert("sfzh",QStringLiteral("123123"));SQLiteHelper* sqlHelper = SQLiteHelper::GetInstance();for(int i= 0; i< size; i++){int id = -1;sqlHelper->insertTableData("studentInfo",varMap,id);qDebug()<<id;}
在不再使用数据库时,释放内存,移除连接
SQLiteHelper::removeDatabases();
结束语
根据自己的理解封装的代码,后续根据实践再调整。