見出し画像

[Drogon]DbClientのインターフェースについて

前回、データベースに独自にアクセスしてデータを取ってくる仕組みを解説した際に、DbClientクラスの execSqlAsync インターフェースについては解説しませんでした。

今回はそれについて参考程度に解説します。

execSqlAsync

execSqlAsyncは端的に書けばスレッド立て逃げインターフェースです。

このインターフェースは、データベースへアクセス後、
第二引数のコールバックに得られた結果を与えて呼び出すか、
第三引数のコールバックに例外を与えて呼び出すか、
このどちらかの処理を、呼び出し元とは異なるスレッドで実行します。

このため、httpリクエストで呼ばれるController内でこのインターフェースを使ってデータを受け取るには、少しコツがいります。

staticメンバを持つ関数オブジェクトを使用する

C++には、関数のように呼び出すことが出来ながら自身の中に状態変数を保持することのできる、
関数オブジェクトという概念が存在します。

具体的には、operator()をオーバーライドしたクラスで、メンバ変数などに値を格納するコードを記載することで、
インスタンス内に状態を保持することができるのです。

ただ、スレッド内でインスタンスを用意してしまうと、スレッドの終了と共にインスタンスが解放され、状態変数の値もまた消失してしまいます。
そこで異なるスレッド間でこのデータベースから取得した値を保持するため、関数オブジェクトの状態変数をスタティックメンバとして定義して利用します。

具体的には、以下のようなコールバック用の関数オブジェクトを定義します。

class functionObject
{
public:
static std::string id_;
static std::string name_;
static std::string value_;

functionObject()
{
};

void operator()(const drogon::orm::Result &result)
{
    if(0 >= result.size())
    {
      return;
    }

    id_ = result[0]["id"].as<std::string>();
    name_ = result[0]["name"].as<std::string>();
    value_ = result[0]["value"].as<std::string>();

    return;
}
};
std::string functionObject::id_;
std::string functionObject::name_;
std::string functionObject::value_;

そのうえで、execSqlAsync インターフェースにこの関数オブジェクトを与え、スレッドの終了を待って値を読み出し利用します。

void ExternalModelController::getData(const HttpRequestPtr &req,
              std::function<void(const HttpResponsePtr &)> &&callback,
              std::string dataID) const
{
    auto functionobj = functionObject();

    auto dbClientPtr = drogon::app().getDbClient("default");
    auto viewData = HttpViewData();

    dbClientPtr->execSqlAsync(
        createSQL(),
        functionobj,
        [](const drogon::orm::DrogonDbException &e){},
        dataID
    );

    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    viewData.insert("id",   functionObject::id_);
    viewData.insert("name", functionObject::name_);
    viewData.insert("value", functionObject::value_);

    callback(HttpResponse::newHttpViewResponse("SampleView.csp", viewData));
    return;
}

関数オブジェクトはコールバックの型さえ合っていれば問題なく呼び出されるため、このオブジェクトを皮切りに様々な処理を実装することもできるでしょう。

ただし、スレッドを跨いだデータのやり取りにstaticメンバを使用するため、
データ不整合やデッドロックを回避するための機構を独自に実装しなければなりません。

公式のサンプルでもコールバックにクロージャを設定しているように、
このインターフェースの使用用途はむしろそのスレッド内で処理が完結する仕組みに向けたものと考えるのが適切です。

  • クロージャを使用した例

void ExternalModelController::getData(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
std::string dataID) const
{
auto dbClientPtr = drogon::app().getDbClient("default");
dbClientPtr->execSqlAsync(
        createSQL(),
        [caller = std::move(callback)](const drogon::orm::Result &result){
            auto viewData = HttpViewData();

            if(0 >= result.size())
            {
            return;
            }

            viewData.insert("id",   result[0]["id"].as<std::string>());
            viewData.insert("name", result[0]["name"].as<std::string>());
            viewData.insert("value", result[0]["value"].as<std::string>());

            caller(HttpResponse::newHttpViewResponse("SampleView.csp", viewData));
        },
        [](const drogon::orm::DrogonDbException &e){},
        dataID
    );

    return;
}

この特性を考えるとデータを永続化して使用する場合にはFuterパターンを使用したexecSqlAsyncFuterの方が適すると言えますし、頻度が高くただ結果を返すだけの処理であれば呼び出しのオーバーヘッドの著しく小さいクロージャを使用したexecSqlAsyncの方が適すると言えるでしょう。

execSqlSync

DbClientクラスのインターフェースの中で、恐らく最もわかりやすいのがexecSqlSyncメソッドでしょう。
execSqlAsyncやexecSqlAsyncFutureは、扱うためにC++言語自体の文法に加え、非同期処理や関数オブジェクト、クロージャなどについての知見が必要となってきます。

ですがこのexecSqlSyncメソッドは、非同期での実行でない代わりにC++にあまりなじみのない方でも使いやすい、引数を与えて戻り値を受け取ることのできるインターフェースです。

引数は単純で、実行したいSQLと、SQLの引数を差し替えたい変数を与えるだけで実行できます。

void ExternalModelController::getData(const HttpRequestPtr &req,
              std::function<void(const HttpResponsePtr &)> &&callback,
              std::string dataID) const
{
    auto dbClientPtr = drogon::app().getDbClient("default");
    auto viewData = HttpViewData();
    try
    {
        auto result = 
        dbClientPtr->execSqlSync(
            createSQL(),
            dataID
        );

        if(0 >= result.size())
        {
        return;
        }

        viewData.insert("id",   result[0]["id"].as<std::string>());
        viewData.insert("name", result[0]["name"].as<std::string>());
        viewData.insert("value", result[0]["value"].as<std::string>());
    }
    catch (const DrogonDbException &e)
    {
        std::cerr << "error:" << e.base().what() << std::endl;
    }

    callback(HttpResponse::newHttpViewResponse("SampleView.csp", viewData));

    return;
}

この関数は同期実行されるので、データベースへの問い合わせを行うと何らかの結果が返ってくるまで、そのスレッドの実行はブロックされます。

C++初学者でも扱いやすく、またhttpリクエストへの処理はEventLoopと呼ばれるメインとは独立したスレッドで行われていますので、パフォーマンス要求がそれほどきつくない場合はこのメソッドによるアクセスでの実装でもそれほど困らないでしょう。

実際のところ、企業で開発を行う場合には要員のスキルのばらつきが大きい場合がありますし、自分が案件から抜けた後も他の誰かがメンテナンス出来る必要がありますので、容易に理解できるこのようなメソッドの使用が最適解である場合も往々にしてあります。

ストリーム風インターフェース

最後のインターフェースがoperator<<をオーバーライドされた、ストリーム風のインターフェースです。
このインターフェースはexecSqlAsyncとexecSqlSyncを足したような動作となります。

void ExternalModelController::getData(const HttpRequestPtr &req,
              std::function<void(const HttpResponsePtr &)> &&callback,
              std::string dataID) const
{
    auto dbClientPtr = drogon::app().getDbClient("default");

    *dbClientPtr 
        << createSQL()
        << dataID
        << Mode::Blocking
        >> [caller = std::move(callback)](const drogon::orm::Result &result){
            auto viewData = HttpViewData();

            if(0 >= result.size())
            {
            return;
            }

            viewData.insert("id",   result[0]["id"].as<std::string>());
            viewData.insert("name", result[0]["name"].as<std::string>());
            viewData.insert("value", result[0]["value"].as<std::string>());

            caller(HttpResponse::newHttpViewResponse("SampleView.csp", viewData));
        }
        >> [](const drogon::orm::DrogonDbException &e){};
       
    return;
}

構文としてはいささかトリッキーですが、全体的に見た目がすっきりしますね。

注目すべきは、3つ目のパラメータMode::Blockingです。
このパラメータは省略することもできますが、省略した場合はexecSqlAsyncと同等の動きとなり、Mode::Blockingを指定すると、execSqlSyncと同様呼び出したスレッドの実行がブロックされます。
また、Mode::NonBlockingを指定して明示的に非同期実行に切り替えることも可能です。

設計検討段階で細かく呼び方を変えて性能検証などを行う場合には、変更量が少なくて役に立ちます。
ただ、やはりC++の文法に十分熟達している必要があるので、複数人で開発する場合には配慮が必要です。

また、このインターフェースの特色として、結果を受け取るコールバックにdrogon::orm::Result型を使用しないコールバックを与えられます。

void ExternalModelController::getData(const HttpRequestPtr &req,
              std::function<void(const HttpResponsePtr &)> &&callback) const
{
    auto dbClientPtr = drogon::app().getDbClient("default");
    int i = 0;

    while(i < 10)
    {
        auto guard = i;
        *dbClientPtr 
        << "SELECT name, value FROM products WHERE id=$1"
        << i
        >> [&i](bool isNull, const std::string &name, const std::string &value){
            if(!isnull)
            {
                std::cout << "the "<< name << " price is "<< value << "yen" << std::endl;
                ++i;
            }
        }
        >> [](const drogon::orm::DrogonDbException &e){};

        if(i == guard) break;
    }

    return;
}

データベースに対してリニアに処理を実行したい場合には、このような書き方でも実行することができます。

さいごに

今回はDbClientクラスの様々なインターフェースについて解説しました。

C++で実行されるネイティブなコードとSQLの実行は往々にしてその実行速度差が大きく、Drogonに限らずC++によってデータベースを扱うためには、そのアクセス方法のどれが現在実現すべき性能を満足するのか、開発チームの実情と合うのか注意深く検討する必要があります。

今回の記事で紹介した内容が、開発する上での検討に役立ったなら幸いです。

この記事が気に入ったらサポートをしてみませんか?