Всем добрых суток времени!
Думаю, подавляющее большинство Qt разработчиков, трогавших работу с базами данных, знают, что QtSql создан однопоточным. Даже более того, соединения с базой данных нельзя передавать между потоками, нельзя передавать и курсоры, и запросы, и, вообще, всё. Почти столь же подавляющее большинство встречало вторую проблему - отсутствие стоянок "Free-Bike" в радиусе мили от поискового запроса, как же эти потоки добавить. Пора ударить коммунистической рукой по классовой несправедливости!
УВАГА: Перед вами настоящий велосипед из настоящих костылей без применения синей изоленты! Используйте на свой страх и риск!
tl;dr
| C++ (Qt) | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| typedef QVector< QVariant > VariantVector;
typedef QVector< VariantVector > VariantVector2;
class Worker : public QObject {
Q_OBJECT
public:
Worker();
virtual ~Worker();
bool init();
bool isInitiated();
int queryNum();
static int exec(Worker * w, QString query, QStringList bindNames = QStringList(), VariantVector bindList = VariantVector());
private slots:
void slotExec(int num, QString query, QStringList bindNames, VariantVector bindList);
signals:
void signalExec(int, VariantVector2);
void sqlError(int, QSqlError)
protected:
QThread * th;
QSqlDatabase db;
}; |
|
| C++ (Qt) | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| QString GenConnectionName() {
static int num = 0;
return QString("DBConnection%1").arg(num++);
}
Worker::Worker(): QObject() {
th = new QThread();
qRegisterMetaType<VariantVector>("VariantVector");
qRegisterMetaType<VariantVector2>("VariantVector2");
}
Worker::~Worker() {
db.close();
th->quit();
th->deleteLater();
}
bool Worker::init() {
if (db.isOpen()) return true;
if (!th->isRunning()) {
th->start(QThread::LowPriority);
moveToThread(th);
}
if (!db.isOpen()) {
db = QSqlDatabase::addDatabase("QPSQL",GenConnectionName());
db.setHostName("localhost");
db.setPort(5432);
db.setUserName("postgres");
db.setDatabaseName("postgres");
QTimer timer;
timer.start(10000);
forever {
if (db.isOpen()) break;
if (db.open()) break;
if (!timer.remainingTime()) break;
} }
if (!db.isOpen()) return false;
return true;
}
bool Worker::isInitiated() {
return th->isRunning() && db.isOpen();
}
int Worker::queryNum() { // Эта штука может сбоить без мутексов
static int i = 0;
return i++;
}
int Worker::exec(Worker*w, QString q, QStringList s, VariantVector v){
int i = queryNum();
QMetaObject::invokeMethod(w, "slotExec", Qt::QueuedConnection, Q_ARG(int,i), Q_ARG(QString,q), Q_ARG(QStringList,s), Q_ARG(VariantVector,v))
return i;
}
void Worker::slotExec(int num, QString query, QStringList bindNames, VariantVector bindList)
{
QSqlQuery q(db);
q.prepare(query);
for (int i = 0; i < bindNames.size(); ++i)
q.bindValue(bindNames[i],bindList[i]);
q.exec();
if (q.lastError().isValid())
emit sqlError(num,q.lastError());
VariantVector2 vv;
while(q.next()) {
static int j = -1; ++j;
vv.push_back(VariantVector());
for (int i = 0; q.value(i).isValid(); ++i)
vv[j].push_back(q.value(i));
}
emit signalExec(num,vv); |
|
Итак, как вынести соединение с базой в другой поток? Как и всё остальное, через QObject
| C++ (Qt) | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // *.H
class Worker : public QObject {
Q_OBJECT
public:
Worker();
virtual ~Worker();
bool init();
bool isInitiated();
protected:
QThread * th;
QSqlDatabase db;
};
// *.CPP
QString GenConnectionName() {
static int num = 0;
return QString("DBConnection%1").arg(num++);
}
Worker::Worker(): QObject() {
th = new QThread();
}
Worker::~Worker() {
db.close();
th->quit();
th->deleteLater();
}
bool Worker::init() {
if (db.isOpen()) return true;
if (!th->isRunning()) {
th->start(QThread::LowPriority);
moveToThread(th);
}
if (!db.isOpen()) {
db = QSqlDatabase::addDatabase("QPSQL",GenConnectionName());
db.setHostName("localhost");
db.setPort(5432);
db.setUserName("postgres");
db.setDatabaseName("postgres");
QTimer timer;
timer.start(10000);
forever {
if (db.isOpen()) break;
if (db.open()) break;
if (!timer.remainingTime()) break;
} }
if (!db.isOpen()) return false;
return true;
}
bool Worker::isInitiated() {
return th->isRunning() && db.isOpen();
} |
|
Отлично, БД вынесена в отдельный поток. Но какой в этом толк, если всё равно нужно ждать запросы? Никакого. Значит, нужно добавить очередь запросов и асинхронное возвращение результатов. Всё это очень просто реализуется через слоты.
Добавим поддержку запросов:
| C++ (Qt) | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // *.H
typedef QVector< QVariant > VariantVector;
typedef QVector< VariantVector > VariantVector2;
class Worker : public QObject {
int queryNum() { static int i = 0; return i++; } // Эта штука может сбоить без мутексов
// ...
static int exec(Worker * w, QString query, QStringList bindNames = QStringList(), VariantVector bindList = VariantVector());
private slots:
void slotExec(int num, QString query, QStringList bindNames, VariantVector bindList);
signals:
void signalExec(int, VariantVector2);
void sqlError(int, QSqlError)
//...
};
// *.CPP
int Worker::exec(Worker*w, QString q, QStringList s, VariantVector v)
{
int i = queryNum();
QMetaObject::invokeMethod(w, "slotExec", Qt::QueuedConnection, Q_ARG(int,i), Q_ARG(QString,q), Q_ARG(QStringList,s), Q_ARG(VariantVector,v))
return i;
}
void Worker::slotExec(int num, QString query, QStringList bindNames, VariantVector bindList)
{
QSqlQuery q(db);
q.prepare(query);
for (int i = 0; i < bindNames.size(); ++i)
q.bindValue(bindNames[i],bindList[i]);
q.exec();
if (q.lastError().isValid())
emit sqlError(num,q.lastError());
VariantVector2 vv;
while(q.next()) {
static int j = -1; ++j;
vv.push_back(VariantVector());
for (int i = 0; q.value(i).isValid(); ++i)
vv[j].push_back(q.value(i));
}
emit signalExec(num,vv);
} |
|
Что мы сделали? Воспользовались механизмом сигналов-слотов. Как гласит документация, при использовании слотов между потоками они преобразуются в сообщения. Однако нам пришлось сгородить небольшой костыль. Дело в том, что слоты в понимании Qt - обыкновенные функции, которые, при обычном синтаксисе, будут вызываться "в основном потоке", а точнее, блокировать оба потока до завершения работы. И потому [Боромир.gif] нельзя просто взять и вызвать слот. Затем и нужен весь этот ужас с QMetaObject::invokeMethod. Именно он поставит сообщение в очередь, не блокируя вызывающий поток.
Почти всё. Осталась одна деталь, нужно вызвать функцию инициализации метатипов:
| C++ (Qt) | 1
2
3
4
5
| Worker::Worker(): QObject() {
th = new QThread();
qRegisterMetaType<VariantVector>("VariantVector");
qRegisterMetaType<VariantVector2>("VariantVector2");
} |
|
Без этого вызова попытки соединиться будут отклоняться с жалобами на неизвестный тип. Вызывать можно практически в любой части кода, но не стоит этим злоупотреблять. Всё-таки moc очень неповоротлив.
В принципе, всё, далее этот класс можно использовать следующим образом:
| C++ (Qt) | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // public slots:
void onReturnQuery(int i, VariantVector2 v) {
if (id != i)
return;
foreach(auto i, v);
qDebug() << i[0] << i[1];
}
void call() {
Worker w;
w.init();
connect(&w,SIGNAL(signalExec(int,VariantVector2)),SLOT(onReturnQuery(int,VariantVector2)));
id = Worker::exec("select 1,2 from 1;");
} |
|
Или не таким дурацким. Выбор за вами.
Ну вот и всё, асинхронные многопоточные запросы реализованы. При желании таких рабочих можно объединить в пулы, реализовать в них больше от QSqlResult, но это уже другая история.
Не по теме:
Обо всех замечаниях по тексту и коду - кричите, как резанные.
Upd1: В статье был ряд опечаток и неточностей. В частности, класс в ряде мест назывался lpWorker, в одном из конструкторов ошибочно передавался this в QThread. Спасибо, Avazart, я верю в тебя, даже если ты не веришь в меня.
|