Pull to refresh

Работа с базами данных в Qt в многопоточном окружении

Reading time 9 min
Views 19K
Все кто разрабатывают приложения на Qt, рано или поздно сталкиваются с работой с БД в многопоточном окружении. И если невнимательно читать Ассистант, то можно натолкнуться на одни очень интересные грабли.

Описание окружения


Рассмотрим типовую проблему. Вы пишите приложение, работающее с бд, при этом вам необходимо обращаться к БД из нескольких потоков (не важно одновременно или нет). Например такое бывает если вы пишите серверную часть некоего приложения или просто у вас есть логгирующий поток с записью в БД.

Грабли


Описание граблей


Если внимательно читать ассистант (то есть не только описание классов, но и общие статьи), то мы увидим следующую надпись в описании многопоточного программирования под Qt: «Подключение может быть использовано только тем потоком, который его создал. При этом, перенос самого подключения либо использование запросов из других потоков не поддерживается» (вольный перевод).

Объяснение граблей


Таким образом, запланированный нами банальный мутекс на подключение к БД работать к сожалению не будет. Так же (если нам не нужно все время делить подключение между несколькими потоками, а в первой половине работы программы его должен использовать один поток, а потом другой) не будет и работать метод moveToThread(), так хорошо показывающий себя при работе например с сокетами.

Попытка поразмыслить над проблемой


Ок, мы не можем делить одно подключение между несколькими потоками. Но как мы можем обойти эту проблему? Я вижу два пути:
  1. Не париться и делать в каждом потоке свое подключение
  2. Обратиться к старому доброму паттерну Singleton

Ну первый вариант слишком прост и не подходит для варианта, когда потоки создаются, что-то делают с БД и почти сразу умирают (будут лишник накладные расходы на коннект). Хотя для части случаев первый вариант подходит идеально;)
Итак, второй способ.

Немного теории о паттерне


Паттерн Singleton подразумевает, что у нас может быть только один объект определенного класса и при любом обращении будет исполняться код в рамках этого объекта. Далее в статье мы слегка отступим от канонической формы этого паттерна, но об этом позже.

Реализация Singleton на C++


Чтобы реализовать данный паттерн мы должны запретить следующее для класса:
  1. Создание нового объекта
  2. Создание копии объекта
  3. Операцию присвоения объекта

Также мы должны дать возможность получить этот самый единственный экземпляр класса.
Класс назовем DatabaseAccessor. Напишем минимальную реализацию синглтона.
//databaseaccessor.h

class DatabaseAccessor
{
public:
    static DatabaseAccessor* getInstance();

private:
    DatabaseAccessor();
    DatabaseAccessor(const DatabaseAccessor& );
    DatabaseAccessor& operator=(const DatabaseAccessor& );
};

//databaseaccessor.cpp

DatabaseAccessor::DatabaseAccessor()
{
}

DatabaseAccessor* DatabaseAccessor::getInstance()
{
    static DatabaseAccessor instance;
    return &instance;
}


* This source code was highlighted with Source Code Highlighter.

То есть мы просто при первом обращении к DatabaseAccessor::getInstance() создаем объект и его возвращаем. В дальнейшем мы возвращаем этот же объект.

Добавляем подключение к БД


Ну тут все просто, добавляем в конструктор подключение к БД.
//databaseaccessor.h

class DatabaseAccessor
{
public:
    static DatabaseAccessor* getInstance();
    static QString dbHost;
    static QString dbName;
    static QString dbUser;
    static QString dbPass;
private:

    DatabaseAccessor();
    DatabaseAccessor(const DatabaseAccessor& );
    DatabaseAccessor& operator=(const DatabaseAccessor& );
    QSqlDatabase db;
};

//databaseaccessor.cpp

DatabaseAccessor::DatabaseAccessor()
{

    db = QSqlDatabase::addDatabase("QMYSQL");
    db.setHostName(dbHost);
    db.setDatabaseName(dbName);
    db.setUserName(dbUser);
    db.setPassword(dbPass);
    if (db.open())
    {
        qDebug("connected to database");
    }
    else
    {
        qDebug("Error occured in connection to database");
    }
}

DatabaseAccessor* DatabaseAccessor::getInstance()
{
    static DatabaseAccessor instance;
    return &instance;
}

//main.cpp

int main(int argc, char *argv[])
{
//...
    DatabaseAccessor::dbHost = "localhost";
    DatabaseAccessor::dbName = "our_db";
    DatabaseAccessor::dbUser = "root";
    DatabaseAccessor::dbPass = "";
    DatabaseAccessor::getInstance();
//...

}


* This source code was highlighted with Source Code Highlighter.

При инциализации программы мы просто прописали нужные данные для доступа к БД и создали объект подключения к БД.

И что дальше?


Теперь нам необходимо дать возможность работать с этой БД. Для начала реализуем возможность простого запроса без получения данных обратно (апдейт, удаление, вставка без необходимости знания нового id).
//databaseaccessor.h

class DatabaseAccessor
{
public:
    static DatabaseAccessor* getInstance();
    static QString dbHost;
    static QString dbName;
    static QString dbUser;
    static QString dbPass;

public slots:
    void executeSqlQuery(QString);

private:

    DatabaseAccessor();
    DatabaseAccessor(const DatabaseAccessor& );
    DatabaseAccessor& operator=(const DatabaseAccessor& );
    QSqlDatabase db;
};

//databaseaccessor.cpp

DatabaseAccessor::DatabaseAccessor()
{

    db = QSqlDatabase::addDatabase("QMYSQL");
    db.setHostName(dbHost);
    db.setDatabaseName(dbName);
    db.setUserName(dbUser);
    db.setPassword(dbPass);
    if (db.open())
    {
        qDebug("connected to database");
    }
    else
    {
        qDebug("Error occured in connection to database");
    }
}

DatabaseAccessor* DatabaseAccessor::getInstance()
{
    static DatabaseAccessor instance;
    return &instance;
}

void DatabaseAccessor::executeSqlQuery(QString query)
{
    QSqlQuery sqlQuery(query, db);
}

//ourthread.h

class OurThread : public QThread
{
    Q_OBJECT
//...
signals:
    void executeSqlQuery(QString);
//...
}

//ourthread.cpp

OurThread::OurThread()
{
    connect(this, SIGNAL(executeSqlQuery(QString)), DatabaseAccessor::getInstance(), SLOT(executeSqlQuery(QString)));
}

void OurThread::run()
{
    emit executeSqlQuery("DELETE FROM users WHERE uid=5");
}


* This source code was highlighted with Source Code Highlighter.

Тут мы создаем в нашем синглтоне паблик слот, который принимает строку запроса и посылает ее БД. В типовом потоке мы создаем сигнал и соединяем его со слотом синглтона. При запуске потока отправляем запрос на удаление юзера с id 5.

А как же получить результат запроса?


Во-первых нам необходимо сначала решить, что мы хотим от нашего синглтона. Либо мы хотим чтобы он выполнял большое количество разнообразных запросов (аналог обычного класса для работы с БД), либо у нас есть некий набор типовых запросов, которые нам необходимо выполнять. Во втором варианте мы можем перенести всю валидацию в наш синглтон и тем самым уменьшить количество кода в проекте;) По старой традиции будем реализовывать второй вариант;) Добавим метод, который будет проверять логин/пароль пользователя.
//databaseaccessor.h

class DatabaseAccessor
{
public:
    static DatabaseAccessor* getInstance();
    static QString dbHost;
    static QString dbName;
    static QString dbUser;
    static QString dbPass;

public slots:
    void executeSqlQuery(QString);
    void validateUser(QString, QString);

private:

    DatabaseAccessor();
    DatabaseAccessor(const DatabaseAccessor& );
    DatabaseAccessor& operator=(const DatabaseAccessor& );
    QSqlDatabase db;
};

//databaseaccessor.cpp

DatabaseAccessor::DatabaseAccessor()
{

    db = QSqlDatabase::addDatabase("QMYSQL");
    db.setHostName(dbHost);
    db.setDatabaseName(dbName);
    db.setUserName(dbUser);
    db.setPassword(dbPass);
    if (db.open())
    {
        qDebug("connected to database");
    }
    else
    {
        qDebug("Error occured in connection to database");
    }
}

DatabaseAccessor* DatabaseAccessor::getInstance()
{
    static DatabaseAccessor instance;
    return &instance;
}

void DatabaseAccessor::executeSqlQuery(QString query)
{
    QSqlQuery sqlQuery(query, db);
}

void DatabaseAccessor::validateUser(QString login, QString pass)
{
    login.remove(QRegExp("['\"]"));
    pass.remove(QRegExp("['\"]"));
    QString query = "SELECT IFNULL(uid, -1) as user_id FROM users WHERE username='"+login+"' AND password='"+pass+"'";
    QSqlQuery sqlQuery(query, db);
    if (sqlQuery.first())
    {
        long userId = sqlQuery.value(0).toInt();
        QMetaObject::invokeMethod(sender(), "setUserId", Qt::DirectConnection, Q_ARG(long, userId));
    }
    else
    {
        QMetaObject::invokeMethod(sender(), "setUserId", Qt::DirectConnection, Q_ARG(long, -1));
    }
}

//ourthread.h

class OurThread : public QThread
{
    Q_OBJECT
//...
signals:
    void executeSqlQuery(QString);
    void validateUser(QString, QString);

public slots:
    void setUserId(long);

private:
    bool lastResultQueryIsReallyLast;
    long userId;
    bool checkUser(const char*, const char*);

//...
}

//ourthread.cpp

OurThread::OurThread()
{
    lastResultQueryIsReallyLast = false;
    connect(this, SIGNAL(validateUser(QString,QString)), DatabaseAccessor::getInstance(), SLOT(validateUser(QString,QString)), Qt::BlockingQueuedConnection);
    connect(this, SIGNAL(executeSqlQuery(QString)), DatabaseAccessor::getInstance(), SLOT(executeSqlQuery(QString)));
}

void OurThread::run()
{
    checkUser("user", "password");
}

bool OurThread::checkUser(const char* login, const char* pass)
{
    emit validateUser(login, pass);
    while (!lastResultQueryIsReallyLast)
    {
        msleep(1);
    }
    lastResultQueryIsReallyLast = false;
    return (userId > 0);
}

void OurThread::setUserId(long userId)
{
    this->userId = userId;
    lastResultQueryIsReallyLast = true;
}


* This source code was highlighted with Source Code Highlighter.

Тут мы добавили еще один слот в наш синглтон, который принимает 2 параметра (логин и пароль). Также в нашем потоке мы его соединили с сигналом в режиме «очереди с блокировкой» (то есть слот будет выполняться в контексте потока с синглтоном, но наш поток будет ждать пока сигнал не дойдет до адресата). Также мы добавили в наш поток слот, который принимает id найденного пользователя. При старте поток емитит сигнал на проверку пользователя и ждет пока не придет ответ (за это отвечает переменная lastResultQueryIsReallyLast). Естественно, синглтон не знает всех своих потоков-пользователей, поэтому используется метод invokeMethod() для вызова метода у объекта, которого вернет sender() (это метод, который возвращает отправителя сигнала если мы находимся в слоте). Причем метод сендера вызывается напрямую, чтобы не ждать следующего прохода цикла событий.
Впринципе, первый метод (когда мы делаем более общие методы доступа к БД) легко получается из второго. Просто надо в методе синглтона обойти все ряды, возвращенные БД и запихнуть их в какой-нибудь QList, который вернуть отправителю запроса.

В заключение


Впринципе, получилось не сложно и вполне приятно.
Плюс, мы имеем возможность разбить на несколько коннектов (помните я говорил об отступлении от паттерна). В этом случае нам надо слегка переписать метод получения инстанции (надо добавить балансировку по нескольким инстанциям и возврат наименее занятой плюс конечно надо запоминать кто какую инстанцию взял), также надо добавить в создание БД наименование подключения (например название можно генерировать по первому объекту, который получил доступ к этой инстанции) и добавить метод, который будет возвращать нужное название подключение исходя из отправителя запроса.
Tags:
Hubs:
+25
Comments 23
Comments Comments 23

Articles