[Drogon]ORMを使用しないDBアクセス
本日はDrogonフレームワークのORM(Object Relation Mapping)を使用せずに、コントローラから直接データベースのデータを取り出し、Viewの画面にデータを表示していきます。
Drogonフレームワークは drogon_ctl コマンドのサブコマンド create model を使用して、データベースのテーブル構造からC++のオブジェクトやRestfulAPIを構築する機能がありました。
またこのORMでは、データベースからデータを取得するSQLも自動生成されます。
このような仕組みは小規模なシステムや、データベースへのアクセスのパフォーマンスを求められない事例では、開発コストを削減することが出来て大変便利なのですが、実際に企業で開発する現場ではそのような事例は多くなく、大抵の場合パフォーマンスを考慮しながらSQLを自身で作成し、それを実行することになります。
今回はDrogonフレームワークのDbClientクラスを使用して、コントローラ内でデータベースを直接取得するコードを書いていきます。
DbClientクラスとは
Drogonフレームワークにはデータベースの操作を行うためのDbClientというクラスがあります。
C++のソースコードから見た場合、このクラスは複数種類のデータベースに統一的なコードでデータベースへアクセスすることのできるインターフェースを提供してくれます。
つまり、PostgreSQLを使用している私の環境のソースコードが、MySQLなどの他の環境でも動作するのです。
このDbClientには主に使用する4つのインターフェースが含まれます。
それらの使い方や特徴などは、公式ドキュメントに詳しいですが、軽くまとめると。。。
execSqlAsync
SQLを非同期実行して、結果を与えられたコールバックに返します。
execSqlAsyncFuture
Future Promise パターンによりSQLを非同期実行して、結果をfutureオブジェクトとして返します。
今回の記事ではこのインターフェースを扱います。
execSqlSync
SQLを同期実行するもっとも単純なインターフェースです。
このインターフェースを実行されたスレッドはSQLの実行結果が戻ってくるまで停止します。
operator<<
ストリームのようにSQLの利用ができるインターフェースです。
内部的な動作はexecSqlAsyncとほぼ同等です。
Drogonではこのような4種類のインターフェースの何れかでデータベースへアクセスすることになります。
execSqlAsync は高速な実行が可能ですが、使用に際してはラムダ式や関数オブジェクトへの知識が必要で、データを永続化させる場合にはセマフォやクリティカルセクションなどのアクセス制御を自前で用意しなければなりません。
極端にミッションクリティカルな場合を除けば、execSqlAsyncFuture インターフェースの使用が安全で簡単です。
そのためこの記事では、execSqlAsyncFuture インターフェースに絞って使用方法を解説します。
データベースの配置
今回サンプルで使用するデータベースは、以前ORMの利用方法を解説した記事で構築したものをそのまま使います。
配置されていない方は、記事を参考に配置してください。
プロジェクトの作成
まずはプロジェクトを作成しましょう。
プロジェクト作成方法は以前の記事を参照することとし、今回はoriginal_model_sampleという名前でプロジェクトを作成します。
config.json
この内容も以前の記事で作成したものを流用します。
main.cc の loadconfig を忘れずにコメントアウト解除しましょう。
コントローラの配置
以前の記事を執筆した際には、コントローラを作る際に、dg_ctlコマンドに対して特にオプションを与えずコントローラ名のみで作成しました。
この場合HTTPSimpleControllerという、設定されたパスに対して任意のメソッドを割り振ることのできない代わりに解りやすいコントローラが作成されます。
データベースに接続するところまで必要な開発を行っているということは、HTTPSimpleControllerでは力不足になりつつあるかと思いますので、今回はより実用的なHTTPControllerクラスを使用してサンプルを作成します。
HTTPControllerクラスを作成するには、create controller サブコマンドに、-h オプションを付加して実行します。
また、名前空間も区切ってみます。
$ cd controllers
$ dg_ctl create controller -h external::ExternalModelController
dg_ctlコマンドに与えるコントローラ名にの前方に、::で区切られたセンテンスを追加して実行すると、::以前の最短の文節が名前空間として作成されます。
今回の場合、external名前空間に、external_ExternalModelControllerというクラスが作成されたかと思います。
複数人で同じ銘々規則で同時に開発する場合というのが企業においてはよくありますが、同じ銘々規則の下開発を行っているため、クラス名や変数名が被るということがたまに発生します(Windows driver開発してた時代はMicrosoftのモジュールと被って3日ハマったことも…)。
そのような場合でも名前空間を区切っておけば、コンパイルエラーやバグを防ぐことに繋がりますので、何らかのクラスを定義する際には名前空間も作ることを癖付けしておきましょう。
HTTPControllerのヘッダヘッダファイル
クラスが出力されたら、まずはexternal_ExternalModelController.hファイルの中を以下のように加工します。
#pragma once
#include <drogon/HttpController.h>
using namespace drogon;
namespace external
{
class ExternalModelController : public drogon::HttpController<ExternalModelController>
{
public:
METHOD_LIST_BEGIN
METHOD_ADD(ExternalModelController::getData, "/{1}", Get);
METHOD_LIST_END
void getData(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
std::string dataID) const; // ← method add のパス設定で{1}として指定されたものが入ります。
const std::string createSQL() const;
};
}
以前ボタンクリックのカウントを行うコントローラを作成した際に使用したのはHTTTPSimpleコントローラでした。
HTTPSimpleControllerの場合はPATH_ADD~PATH_ENDの間に記載されたパスへのアクセスが、すべて単一のハンドラメソッドに飛ばされていました。
HTTPControllerの場合は、METHOD_ADD~METHOD_ENDの間に、クラスメソッド毎にパスを設定することができますが、その分設定が若干複雑になっています。
今回はシンプルなサンプルなので、一つだけメソッドを追加しています。
ここでパス文字列に設定している{1}は、パスに記載されたそのセンテンスのデータを、登録されたメソッドの第三引数以降に渡すという指示です。
取得するデータのIDを引数として与え、それを基に検索を実行して一件のデータを取得します。
HTTPControllerのソースファイル
次にソースコードを編集します。
最終的には以下のようなコードになります。
#include "external_ExternalModelController.h"
using namespace external;
// Add definition of your processing function here
void ExternalModelController::getData(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
std::string dataID) const
{
try
{
auto futureObject =
drogon::app()
.getDbClient("default")
->execSqlAsyncFuture(createSQL(), dataID);
if(std::future_status::timeout
== futureObject.wait_for(std::chrono::seconds(3)) )
{
callback(HttpResponse::newHttpViewResponse("TimeOut.csp"));
}
else
{
auto viewData = HttpViewData();
auto result = futureObject.get();
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>());
callback(HttpResponse::newHttpViewResponse("SampleView.csp", viewData));
}
}
catch(const drogon::orm::DrogonDbException &e)
{
std::cerr << e.base().what() << '\n';
}
return;
}
const std::string ExternalModelController::createSQL() const
{
return std::string("SELECT * FROM public.products WHERE id=$1");
}
まずポイントとなるのは、データベースからデータ取得を行うDbClientの使用の仕方です。
サンプルコードでは一気にやってしまっていますが、分けて書けば以下のようになります。
auto client = drogon::app().getDbClient("default");
auto sql = createSQL()
auto futureObject = client->execSqlAsyncFuture(sql, dataID);
drogon::app()は、プロジェクトのメインインスタンスを取得するためのインターフェースです。
1行目ではプロジェクトのメインインスタンスが保持している、DbClientクラスのインスタンスへのポインタを取得しています。
2行目は独自のSQLを作成しています。
そして3行目では、execSqlAsyncFuture インターフェースの第一引数にsqlを、第2引数に検索のキーとなるdataIDを与えています。
このとき、CreateSQL()の中で設定された作成されたSQLは以下のようなものです。
"SELECT * FROM public.products WHERE id=$1"
お気づきでしょうか、WHERE 句で検索のキーとして設定されているidに対して、$1というキーを設定しています。
DrogonフレームワークのDbClientクラスのインターフェースはどれも、主となるSQL文に対して第2引数以降で与えられた引数の内容で、$1等の部分を置き換えるようにできています。
今回はexecSqlAsyncFutureインターフェースにパスとしてわたってきた{1}の内容が入ってきますので、例えば localhost:80/1 というアドレスに対してHTTPアクセスを受けた場合、この1という値が文字列として作成したSQL分の$1を置き換え、データベースに対しては
"SELECT * FROM public.products WHERE id=1"
というクエリが送信されることになります。
そして最終的に、
std::futuredrogon::orm::Result
というfuter promiseパターンのインスタンスが別スレッドを作成して非同期的にデータベースへの問い合わせを行います。
そのため、例えばサンプルのようにデータベース問い合わせのタイムアウト時間を設定して、タイムアウトした場合にそれを知らせる画面を表示させる、
// execSqlAsyncFutureが返してくるのは、C++標準の std::future オブジェクトなので、
// このようにタイムアウトを設定した処理待ちもできます。
if(std::future_status::timeout
== futureObject.wait_for(std::chrono::seconds(3)) )
{
callback(HttpResponse::newHttpViewResponse("TimeOut.csp"));
return;
}
問い合わせを行いながらビューへ送るデータのインスタンスを作成する、
// DB問い合わせ中でも出来る処理を、
// futureから結果を取り出すまでの間に行うことができます。
auto viewData = HttpViewData();
などの処理を実装することができます。
実際にデータベースから帰ってきたデータに関しては、以下のようにしてデータを取り出します。
auto result = futureObject.get();
resultはDrogonのormで規定されたResult型のオブジェクトです。
値が一つでも帰ってきた場合、Result のサイズは0より大きくなりますので、サンプルのようにサイズを確認するようにしましょう。
Result型に格納されたデータのアクセスには、まずResultの何行目かを一つ目の添え字で指定した上で、二つ目の添え字にデータベースのカラム名か、カラムの番号を入れることで取得することができます。
result[0]["id"].asstd::string()
ただしそのまま添え字で指定した領域にアクセスしても、データを格納されたポインタのアドレスが返ってくるのみであるため、実際にデータを取る際にはas<T>()インターフェースで、自分が意図した型でのデータの取得が必要となります。
ここでは全て値を文字列型として、ビューへ渡すデータに格納しています。
viewData.insert("id", result[0]["id"].asstd::string());
viewData.insert("name", result[0]["name"].asstd::string());
viewData.insert("value", result[0]["value"].asstd::string());
execSqlAsyncFutureによるデータの取得は、データベースからのデータ取得で何らかのエラーが生じた場合にDrogonDbException型の例外を送出しますので、これについてもtry-cattchステートメントで必ず処理をします。
catch(const drogon::orm::DrogonDbException &e)
{
std::cerr << e.base().what() << '\n';
}
最後におなじみのビューの呼び出しを行って、このコントローラの役目は終了です。
callback(HttpResponse::newHttpViewResponse("SampleView.csp", viewData));
ビューに表示してみる
今回は2種類のビューを用意します。
一つはタイムアウトした際に表示させるTimeOut.csp
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>タイムアウトしちゃったよ…</title>
</head>
<body>
<div>タイムアウトしちゃったよ…</div>
</body>
</html>
もう一つが取得したデータを実際に表示するSampleView.csp
<!DOCTYPE html>
<html>
<%c++
auto displayID=@@.get<std::string>("id");
auto displayName=@@.get<std::string>("name");
auto displayValue=@@.get<std::string>("value");
%>
<head>
<meta charset="UTF-8">
<title>Button Count Example</title>
</head>
<body>
<div>The ID is {% displayID %}.</div>
<div>It's a {% displayName %} and price is {% displayValue %} yen. </div>
</body>
</html>
cspに関する解説は別のページで行います。
ビルドしてアクセスしてみる
ビルド手順は特筆すべきこともないので、こちらの記事を参照してください。
ビルドに成功したら、サーバを起動しましょう。
$ ./original_model_sample
サーバが起動したらブラウザのアドレスバーにURLを記入して実際にアクセスしてみます。
この時注意が必要なのが、Drogonのルーティングの仕方として、特に何も指定せずに名前空間付きのクラスを作成した場合、アクセスするURLがホスト/名前空間/コントローラクラス名となることです。
このURLの末尾に、external_ExternalModelController.hでMETHOD_ADDしたパスが追加されます。
ですので、ID3のレコードへアクセスするexternal::ExternalModelController へのルーティングは以下のようになります。
localhost:80/external/ExternalModelController/3
実際にアクセスしてみて、以下のような画面が表示されたら成功です。
さいごに
今回はORMを使用せず独自にデータベースへアクセスを行い、そのデータをビューに表示するサンプルに挑戦してみました。
ある程度の規模のアプリケーションを作成する場合、実行計画を参照しながらSQL文のチューニングを行ってデータベースアクセスのパフォーマンスを向上させる必要が出てきます。ORMを使用したデータベースへのアクセスは、コードの実装が手軽ではありますが、SQLを独自にチューニングすることが難しいため、その点で不利です。
そのため、実施の開発の現場では、今回紹介したようなある程度の機能を使用しつつSQLなどは独自に実装するなどの対応が必要となります。
今回紹介した内容を応用すれば、自由度をもってDrogonでのアプリ開発が行えますので、皆様もぜひお役立てください。