マルチスレッド環境でQtのデータベースを操作する

遅かれ早かれQtでアプリケーションを開発するすべての人は、マルチスレッド環境でデータベースを操作することに遭遇します。 アシスタントを不注意に読んだ場合、非常に興味深いレーキに出くわす可能性があります。



環境の説明



典型的な問題を考えてください。 データベースを操作するアプリケーションを作成し、複数のスレッドからデータベースにアクセスする必要があります(同時にかどうかは関係ありません)。 たとえば、特定のアプリケーションのサーバー部分を記述した場合、またはデータベース内のレコードを含むロギングストリームがある場合に発生します。



すくい



レーキの説明



アシスタントを注意深く読んだ場合(つまり、クラスの説明だけでなく、一般的な記事も)、Qtの下のマルチスレッドプログラミングの説明に次の碑文が表示されます:「接続は、それを作成したスレッドによってのみ使用できます。 同時に、接続自体の転送または他のフローからの要求の使用はサポートされていません」(無料翻訳)。



熊手の説明



したがって、データベースに接続するために私たちが計画した一般的なミューテックスは、残念ながら機能しません。 また、(複数のスレッド間で接続を常に分割する必要がなく、プログラムの前半で1つのスレッドを使用し、次にもう1つのスレッドを使用する必要がある場合)、たとえばソケットを操作する場合、moveToThread()メソッドはあまりうまく機能しません。



問題を振り返ってみる



OK、複数のスレッド間で単一の接続を共有することはできません。 しかし、どうすればこれを回避できますか? 次の2つの方法があります。

  1. 各スレッドでスチームや接続を行わないでください
  2. 古き良きシングルトンパターンを参照してください。


さて、最初のオプションは単純すぎて、スレッドの作成時のオプションには適さず、データベースで何かをし、すぐに死にます(接続のオーバーヘッドにオーバーヘッドがあります)。 場合によっては、最初のオプションが完全に適合しますが;)

だから、第二の方法。



パターンに関する少しの理論



シングルトンパターンは、特定のクラスのオブジェクトを1つだけ持つことができ、呼び出しがこのオブジェクト内でコードを実行することを意味します。 さらにこの記事では、このパターンの標準形式から少し離れますが、それについては後で詳しく説明します。



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のユーザーを削除するリクエストを送信します。



しかし、クエリの結果を取得する方法は?



最初に、シングルトンから何を望むかを最初に決定する必要があります。 多数のさまざまなクエリ(データベースを操作するための通常のクラスの類似物)を実行するか、満たす必要がある典型的なクエリの特定のセットがあります。 2番目のオプションでは、検証全体をシングルトンに転送して、プロジェクト内のコードの量を減らすことができます;)古い伝統に従って、2番目のオプションを実装します;)ユーザーのユーザー名/パスワードを確認するメソッドを追加します。

//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()が返すオブジェクトのメソッドを呼び出します(これは、スロットにいる場合に信号の送信者を返すメソッドです)。 さらに、イベントループの次のパスを待たないように、senderメソッドが直接呼び出されます。

原則として、最初の方法(データベースへのより一般的なアクセス方法を作成する場合)は、2番目の方法から簡単に取得できます。 データベースから返されたすべての行をsingletonメソッドで調べ、それらを要求リストに返すQListに詰め込むだけです。



結論として



原則として、難しくはなく、非常に良い結果が得られました。

さらに、いくつかの接続に分割する機会があります(パターンからの逸脱について話したことを思い出してください)。 この場合、インスタンスを取得するメソッドをわずかに書き換える必要があります(複数のインスタンス間でバランスを追加し、最もビジーでない状態を返す必要があります。もちろん、誰がどのインスタンスを使用したかを覚えておく必要があります)。また、データベース作成に接続の名前を追加する必要がありますこのインスタンスへのアクセス)、リクエストの送信者に基づいて目的の接続名を返すメソッドを追加します。




All Articles