APR-util の DBD API を使ってみる

APR(の一部の APR-util)には,各種データベースを統一インタフェースで使うことのできる DBD API があります。のでちょっと使ってみました。

ちなみに普通に C でアプリケーションを書いている人にはあまりおすすめできないと思います。統一インタフェースというほど方言を吸収しているとはいえないので。もっとも portability layer としてすでに APR を使用していて,ちょっくら DB も(軽く)使用しようかというケースなら向いているかもしれません。どちらかというと,DB を使用する Apache のモジュールを開発している場合に,個別のデータベースクライアントライブラリにバインドするよりはこちらを使ったほうが,柔軟性(データベースの選択)が増すという用途です。さらに mod_dbd なんてのもあわせて使うとおもしろいかも。

どのデータベースエンジンがサポートされているのか

[http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#gbddb1fdcb2f8a5f5b83127485c78e8ae:title=apr_dbd_open_ex()]API ドキュメントによると,

がサポートされているようです。

実際にはどうなのか,[http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#g8ba85faccf7e8eea525812f8f2dfed25:title=apr_dbd_get_driver()] を利用してたしかめてみます(CentOS 5.2 の APR Util 1.2.7 で検証)。

#include <stdio.h>
#include <stdlib.h>
#include <apr_pools.h>
#include <apu.h>
#include <apr_dbd.h>

static void
print_apr_error(apr_status_t status, const char *message)
{
    char buf[1024];

    fprintf(stderr, "%s%s%s\n", apr_strerror(status, buf, sizeof(buf)),
                                message ? ": "    : "",
                                message ? message : "");
}

static void
error_exit(apr_status_t status)
{   
    print_apr_error(status, NULL);
    exit(status);
}

int
main(int argc, char *argv[])
{
    apr_pool_t *pool;
    apr_status_t r;
    const apr_dbd_driver_t *driver;
    const char **pdb;

    static const char *dbs[] = {
        "sqlite3",
        "mysql",
        "pgsql",
        NULL,
    };

    apr_pool_initialize();
    apr_pool_create(&pool, NULL);   /* root pool */

    r = apr_dbd_init(pool);
    if (r != APR_SUCCESS)
        error_exit(r);

    for (pdb = dbs; *pdb; pdb ++) {
        fprintf(stdout, "\n[%s]\n", *pdb);

        r = apr_dbd_get_driver(pool, *pdb, &driver);
        if (r != APR_SUCCESS) {
            print_apr_error(r, NULL);
        }   
        else {
            fputs("OK!\n", stdout);
        }   
    }

    return 0;
}

ビルドするのがちょっと骨なので pkg-config を利用した Makefile を置いておきます。

CFLAGS :=       $(shell pkg-config --cflags apr-1) \
                $(shell pkg-config --cflags apr-util-1)
LIBS :=         $(shell pkg-config --libs apr-1) \
                $(shell pkg-config --libs apr-util-1)

TARGET :=       drivers

all:    build

build:  $(TARGET)

clean:
        rm -f $(TARGET)

test:   $(TARGET)
        ./$(TARGET)

$(TARGET):      main.o
        $(CC) -o $@ $^ $(LIBS)

.PHONY: all build clean test

さて,実行すると,

$ ./drivers

[sqlite3]
OK!

[mysql]
This function has not been implemented on this platform

[pgsql]
OK!

のような結果になりました。

実は apu.h ヘッダに

#define APU_HAVE_PGSQL         1
#define APU_HAVE_MYSQL         0
#define APU_HAVE_SQLITE3       1
#define APU_HAVE_SQLITE2       0

とあります(もちろん環境により内容が異なります)。プログラム書いてまでたしかめるもんでもなかったな。

MySQL 用ライブラリが含まれていないのは,CentOS 5.2 採録Apache 2.2.3 のころはライセンス不整合問題のため含まれていなかったようです*1。今回は SQLite3 を使うつもりなので,まぁいいや。

データベースの準備

もちろん DBD API で query を発行してテーブルを作成してもよいんですが,面倒なので CLI から作成しておきます。

$ sqlite3 test.db

SQLite version 3.3.6
Enter ".help" for instructions

sqlite> CREATE TABLE t_user ( id INTEGER, name VARCHAR );

sqlite> INSERT INTO t_user (id, name) VALUES ( 1, 'dayflower' );

sqlite> SELECT * FROM t_user;
1|dayflower

sqlite> .q

なにはともあれ SELECT

めんどうなので一部実装(エラー表示やエラーチェック)をはしょっていますが,SELECT を行うプログラムを書いてみました。

#include <stdio.h>
#include <stdlib.h>
#include <apr_pools.h>
#include <apu.h>
#include <apr_dbd.h>

static void print_apr_error(apr_status_t status, const char *message);
static void error_exit(apr_status_t status);

int
main(int argc, char *argv[])
{
    apr_status_t r;
    apr_pool_t *pool;
    const apr_dbd_driver_t *driver;
    apr_dbd_t *dbh;
    apr_dbd_results_t *dbr;
    apr_dbd_row_t *row;
    int n, nrows, ncols;

    apr_pool_initialize();
    apr_pool_create(&pool, NULL);   /* root pool */

    r = apr_dbd_init(pool);
    if (r != APR_SUCCESS)
        error_exit(r);

    r = apr_dbd_get_driver(pool, "sqlite3", &driver);
    if (r != APR_SUCCESS)
        error_exit(r);

    r = apr_dbd_open(driver, pool, "test.db", &dbh);
    if (r != APR_SUCCESS)
        error_exit(r);

    dbr = NULL;
    apr_dbd_select(driver, pool, dbh, &dbr, "SELECT * FROM t_user", 0);

    ncols = apr_dbd_num_cols(driver, dbr);
    fprintf(stdout, "num_cols = %d\n", ncols);

    nrows = apr_dbd_num_tuples(driver, dbr);
    fprintf(stdout, "num_tuples = %d\n", nrows);

    row = NULL;
    for (n = 0; apr_dbd_get_row(driver, pool, dbr, &row, -1) == 0; n ++) {
        int i;

        fprintf(stdout, "record #%d\n", n);
        if (row != NULL)
            for (i = 0; i < ncols; i ++)
                fprintf(stdout, "[%d]: %s\n", i,
                                apr_dbd_get_entry(driver, row, i));
    }

    r = apr_dbd_close(driver, dbh);

    return 0;
}
  1. [http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#geff12b01f78ac78721acc4a0a318e673:title=apr_dbd_open()] で DB をオープンして
  2. [http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#g144d354a36140fade933c1ef72661004:title=apr_dbd_select()] で選択系 SQL を発行し
  3. [http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#gd4cdc5f4e8981b93f5a467a8c8a768f1:title=apr_dbd_get_row()] で各行を fetch(([http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#g144d354a36140fade933c1ef72661004:title=apr_dbd_select()]random パラメータを 1 にすると,行指定で fetch できます。今回は random = 0 なので,next row fetch しているだけです。))
  4. [http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#g1d6d3b38a0d677e3d65501074832a5b8:title=apr_dbd_get_entry()] で各行の各コラムを取得
  5. さいごに [http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#g4abe00d844cd547cc69880fe14af4aca:title=apr_dbd_close()] でクローズ

という手順です。

APR-util 1.4 だと [http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#g8eac8897bd1211564166b08492f458d7:title=apr_dbd_get_name()] という API でコラムの名前(テーブルの列名)を取得できるのですが,APR-util 1.3 ではできませんでした。残念。

    dbr = NULL;
    apr_dbd_select(driver, pool, dbh, &dbr, "SELECT * FROM t_user", 0);

のような表現が頻出しますが,このような場所では NULL を入れておかないとセグフォって落ちます。API ドキュメントに

res
pointer to result set pointer. May point to NULL on entry

と書いてあったので,取得する行によっては NULL が返ることもあるよ,とかのほほんと構えていたら落ちたのでびっくりしました。よくよく読んだら,最初に実行する時は,とかそういう意味ですね。

ほんとは変数宣言部分で NULL を代入すればいいんですが,このような経緯もあり,自戒をこめてあえて後段で NULL を代入しています。


さて,実行します。

$ ./select

num_cols = 2
num_tuples = 1
record #0
[0]: 1
[1]: dayflower

無事取得できました。

せっかくだから INSERT

つぎに,DBD API から INSERT をしてみます。

#include <stdio.h>
#include <stdlib.h>
#include <apr_pools.h>
#include <apr_strings.h>
#include <apu.h>
#include <apr_dbd.h>

static void print_apr_error(apr_status_t status, const char *message);
static void error_exit(apr_status_t status);

int
main(int argc, char *argv[])
{   
    apr_status_t r;
    apr_pool_t *pool;
    const apr_dbd_driver_t *driver;
    apr_dbd_t *dbh;
    apr_dbd_transaction_t *trans;
    int nrows;

    apr_pool_initialize();
    apr_pool_create(&pool, NULL);   /* root pool */

    r = apr_dbd_init(pool);
    if (r != APR_SUCCESS)
        error_exit(r);

    r = apr_dbd_get_driver(pool, "sqlite3", &driver);
    if (r != APR_SUCCESS)
        error_exit(r);

    r = apr_dbd_open(driver, pool, "test.db", &dbh);
    if (r != APR_SUCCESS)
        error_exit(r);

    trans = NULL;
    apr_dbd_transaction_start(driver, pool, dbh, &trans);

    apr_dbd_query(driver, dbh, &nrows,
                  apr_psprintf(pool, "INSERT INTO t_user (id, name)"
                                     "VALUES (\"%s\", \"%s\")",
                               apr_dbd_escape(driver, pool, "123", dbh),
                               apr_dbd_escape(driver, pool, "foo", dbh))
                 );

    fprintf(stdout, "%d rows accected.\n", nrows);

    apr_dbd_transaction_end(driver, pool, trans);

    r = apr_dbd_close(driver, dbh);

    return 0;
}

無駄にトランザクション処理も行っています。まぁトランザクションといっても,エラーが発生したらロールバックし(というよりその後に発行される SQL を無視する)発生しなかったらコミットするというだけのプリミティブなものですが*2

手順としては,

  1. [http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#gb5806cd6535aaeafe8e9f79ef2cc90c9:title=apr_dbd_escape()]エスケープ処理を行って
  2. [http://apr.apache.org/docs/apr-util/1.3/group___a_p_r___util___d_b_d.html#g40dbb6bb3f3f171f3443d21f3594a66a:title=apr_dbd_query()] で選択系以外の SQL 文を発行

ということになります。

プリペアードステートメントは使えないの?と思いますが,一応 API として用意はされています。しかし,APR-util 1.3 と SQLite の組み合わせでは使えなかった*3ので,今回のプログラムでは上記のように自力でエスケープ処理を行っています。プリペアードステートメントを使う場合,

    apr_dbd_prepared_t *ps;
    const char *values[2];

    ps = NULL;
    apr_dbd_prepare(driver, pool, dbh, "INSERT INTO t_user (id, name)"
                                       "VALUES (?, ?)",
                    NULL, &ps);

    values[0] = "12345";
    values[1] = "foo";
    apr_dbd_pquery(driver, pool, dbh, &nrows, ps, 2, values);

のような構文になります。

さて,実行しますと,

$ ./insert

1 rows accected.

無事1行挿入できたようです。

CLI をつかってたしかめてもいいのですが,せっかくなので先ほど作成した SELECT コマンドを実行してみます。

$ ./select

num_cols = 2
num_tuples = 2
record #0
[0]: 1
[1]: dayflower
record #1
[0]: 123
[1]: foo

うまくいっていますね。

メモリ使用量について

「ハンドル」を取得するための API は,だいたい次のような形になります。

int apr_dbd_get_row(const apr_dbd_driver_t *driver, apr_pool_t *pool,
                    apr_dbd_results_t *res, apr_dbd_row_t **row, int rownum)

このように,ポインタへのポインタをとります。ので,ハンドルのメモリアロケーションは API 側でやってくれるようです。そしてリリースのための API は定義されていません。

ということは,一度データベースのハンドルを取得したのちに,複数の SQL 文を発行する(=apr_dbd_results_t ハンドルが生成される)と,どんどんメモリが確保されていくことになります(ソースを追っていないので詳細は不明ですが,おそらくそうでしょう)。今回のように CLI を書くだけなら問題にはなりませんが,Apache モジュールを書いている場合,(プロセスが落ちるまで)どんどんメモリの使用量がふえていきます。

今回はそこまでのコーディングはやっていませんが,このような場合,各リクエストごとなどの単位でサブメモリプールを生成して(あるいは request_recpool を利用して),リクエスト終了時にメモリプールを破棄するようにすればいいでしょう。

そうやって考えてみれば,APR-util DBD API の多くの関数で,つどつど apr_pool_t を要求している理由がわかりました。

*1:漢(オトコ)のコンピュータ道: Apache mod_dbd設定編 参照

*2:APR-util 1.4 だとトランザクションのモードを指定できるようになっています。といってもやはりプリミティブな機能であることにはかわりありませんが。

*3:どちらが悪いのかは不明。