Perl Hackers Hub

第30回データベースプログラミング入門―汎用インタフェースDBIと、O/RマッパTengの使い方(2)

前回の(1)こちらから。

DBIの使い方

(2)では、Perlのデータベースプログラミングにおける基本的なモジュールであるDBIについて解説します。

なお、本稿のコードは、執筆時点の最新であるDBIのバージョン1.632、DBD::mysqlのバージョン4.028を前提に記述しています。

DBIとは何か

DBIは、データベースへアクセスするための抽象的なインタフェースを提供するモジュールです。DBIを利用することにより、Perlではデータベースの種類をほとんど気にせずにコードを記述できます[2]⁠。Perlにおいてデータベースを扱うほとんどのモジュールは、DBIを利用することを前提にしています。

DBIは各データベース用のドライバモジュールであるDBD系モジュールと組み合わせて利用します。たとえば、DBIでMySQLを扱う場合はDBD::mysqlと組み合わせて利用します。同様にPostgreSQLではDBD::PgSQLiteではDBD::SQLiteを利用します。このようにDBIでは、データベースへの実際のアクセスをドライバモジュールに任せることによって、統一されたインタフェースであらゆるデータベースへアクセスすることを可能にしています。

この性質は、データベースを扱うモジュールの実装を容易にします。実際、CPANにはDBIxというDBIのAPIを利用したモジュールを登録するネームスペースが存在します。そして、DBIxネームスペースにはなんと、執筆時点(2014年11月2日)で920個ものモジュールが存在します。これらのモジュールのほとんどが、データベースの差異を意識せずに利用できるようになっています。

libmysqlclientのインストール

今回はMySQLを用いるため、DBD::mysqlを利用します。DBD::mysqlのインストールにはlibmysqlclientが必要です。次のようにインストールします。

Homebrewを使う場合(OS X)
$ brew install mysql
apt-getを使う場合(Ubuntu、Debian GNU/Linuxなど)
$ sudo -H apt-get install libmysqlclient-dev

※ OSによってはパッケージ名が異なる場合があります

DBI、DBD::mysqlのインストール

続いて、DBIDBD::mysqlcpanmでインストールします。

$ cpanm DBI DBD::mysql

モジュールがロードできればインストールに成功しています。

$ perl -MDBI -E 'say $DBI::VERSION'
$ perl -MDBD::mysql -E 'say $DBD::mysql::VERSION'

基本的な使い方

DBIは、データベースへの接続connectメソッド⁠⁠、SQLの準備prepareメソッド⁠⁠、SQLの実行executeメソッド⁠⁠、結果の取得fetchメソッド)というフローで利用します。以降でこれらのメソッドの具体的な使い方を解説します。

データベースへの接続――connect

データベースへ接続するためにはconnectメソッドを使います。connectメソッドは「データベースハンドラ」と呼ばれるデータベースとの接続を示すオブジェクトを返します。次のようなコードでデータベースに接続できます。

my $dsn = "dbi:mysql:database=myappdb
          ;host=myapp-mysql.local;port=3306"; # (1)DSN
my $user = "myapp";
my $pass = q{fQ$aH'dKd#YxC};
my $dbh = DBI->connect($dsn, $user, $pass, {
    # (2)DBI属性
    AutoCommit => 1,
    PrintError => 0,
    RaiseError => 1,
    ShowErrorStatement => 1,
    AutoInactiveDestroy => 1
});

接続にはDSNData Source NameというODBCOpen Database Connectivity由来の記法を使います。DSNは次のようなフォーマットの文字列です。各項目の名前は定義されていないため、ここでは筆者が独自の名前を定義しています。

"dbi:ドライバモジュール名: 属性名1= 属性値1; 属性名2= 属性値2"

ドライバモジュール名には、ドライバモジュールのネームスペースDBDを除いた名前を記述します。たとえばMySQLの場合はDBD::mysqlなので、ドライバモジュール名はmysqlになります。

属性には、サーバへの接続に必要な設定、細かい挙動を制御する設定などを記述します。ドライバモジュールごとにどのような属性を渡すかは異なります。たとえばDBD::mysqlの場合は、MySQLサーバのホスト名などを指定する必要があります。DBD::mysqlの代表的な属性は表1のとおりです。つまり(1)のDSNを指定すると、DBD::mysqlを利用し、myapp-mysql.local:3306のMySQLのmyappdbデータベースに接続します。

表1 DBD::mysqlで利用できる代表的な属性
属性名属性値属性値の例
database利用するMySQLのデータベースmyapp
host接続先のMySQLサーバのホスト名myapp-mysql.local
port接続先のMySQLサーバのポート番号3306
mysql_enable_utf8MySQL上のデータがUTF-8で保存されることを前提に、utf8フラグの管理を自動で行わせる1

ほかにもさまざまな属性を利用できる。詳しくはドキュメントを参照(表2も同様)

また、ドライバモジュールと同様に、DBIにも(2)のように細かい挙動を変更できる属性を記述できます。代表的な属性は表2のとおりです。特に重要な属性としてRaiseErrorを覚えておいてください。DBIの各メソッドの失敗時の挙動はRaiseErrorによって異なります。詳しくは後述します。

表2 DBIで利用できる代表的な属性
属性名属性値属性値の例
AutoCommitステートメント単位でコミットする。トランザクションはbegin_work、commit、rollbackにより明示する1
PrintErrorDBIのエラーを標準エラー出力にprintする1
RaiseErrorDBIのエラーが起きたときにdieする1
ShowErrorStatementDBIのエラーに原因となったSQL文を含める1
InactiveDestroyDBIにはデストラクタで接続を切断する機能があるが、それを無効にする0
AutoInactiveDestroyfork時に自動的に子プロセスでInactiveDestroyを有効にする1

なおDBIの属性については、理解が浅いうちは上記サンプルコードと同様の設定にすることをお勧めします。いろいろな設定をオン/オフして挙動を観察するのもおもしろいので、ぜひ試してみてください。

SQLの準備と実行――prepare、execute

prepareメソッドはSQLを準備し、ステートメントハンドラを生成します。そしてステートメントハンドラのexecuteメソッドを利用してSQLを実行します。executeメソッドはSQLの実行に成功すれば真値を返します。失敗時の挙動はRaiseError属性に準じます。

my $sth = $dbh->prepare(
    'INSERT chat (room, user, msg) VALUES (?, ?, ?)'
);
$sth->execute("room1", "karupanerura", "hello");
$sth->finish;

ステートメントハンドラは再利用できます。再利用しない場合は明示的にfinishメソッドを呼び出しましょう。

my $sth = $dbh->prepare(
    'INSERT chat (room, user, msg) VALUES (?, ?, ?)'
);
$sth->execute("room$_", "bot", "hello") for 1..10;
$sth->finish;

上記のように、DBIでSQLに値を埋め込むにはプレースホルダというsprintfとよく似たしくみを利用します。値を埋め込みたい個所を?としてSQLに記述すると、executeメソッドの引数の値が対応する順序で埋め込まれます。

sprintfと違うのは、自動的にサニタイズ処理が行われる点です。単純な文字列結合やsprintfでは、次のように攻撃者が任意のSQLを実行できてしまうことがあります[3]⁠。

# 悪意のある入力
my $room = "room1";
my $user = "evil";
my $msg = "');
UPDATE chat SET msg = 'What a weak system!'; --";

# プレースホルダを利用せずに文字列結合で値を埋め込む
my $sth = $dbh->prepare(
    "INSERT chat (room, user, msg)
       VALUES ('$room', '$user', '$msg')"
);
$sth->execute();

このケースでは次のようなSQLが実行されます。

INSERT chat (room, user, msg)
  VALUES ('room1', 'evil', '');
UPDATE chat SET msg = 'What a weak system!'; --')

意図していない悪意のあるUPDATEが出現しています。もとのSQLの一部はコメントアウトされ、構文エラーにはなりません。サニタイズ処理が適切に行われておらず、複数のSQLが同時に実行可能である場合、このような攻撃も可能になってしまいます。

プレースホルダを利用して値を埋め込めばサニタイズが行われるため、もとのSQLが破壊されることはありません。もちろん、DBIやドライバモジュールにバグがないことが前提になりますが、DBIDBD::mysqlも枯れているモジュールなので安全性は高いです。少なくとも、自前でサニタイズ処理を実装したり、安全な入力かどうかをチェックする処理を実装するよりははるかに安全でしょう。よって、SQLに値を埋め込む際は必ずプレースホルダを使いましょう。

結果の取得――fetch系

ステートメントハンドラを利用してSQLの結果を得るには、fetch系メソッドを利用します。ここではfetchall_arrayrefメソッドを例に紹介します。fetchall_arrayrefは結果のすべての値を配列リファレンスとして返します。

my $sth = $dbh->prepare(
    'SELECT user, msg FROM chat WHERE room = ?'
);
$sth->execute('room1');

my $rows = $sth->fetchall_arrayref();
# $rows => [
# ["karupanerura", "hello"]
# ["kfly8", "hello"]
# ]

第1引数にリファレンスを指定することにより、単一行をデータとしてどのように表現するかを変えることができます。たとえば単一行をハッシュリファレンスとして扱いたい場合は、次のようにします。

my $rows = $sth->fetchall_arrayref(+{});
# $rows => [
# { user => "karupanerura", msg => "hello" },
# { user => "kfly8", msg => "hello" },
# ]

ほかにも、TMTOWTDIThere's More Then One WayTo Do It注4をスローガンに掲げるPerlならではか、値を得るだけでも多くのメソッドが提供されています。詳しくはDBIのドキュメントを参照してください。

任意のSQLの実行――do

単純にSQLを実行するだけであればdoメソッドが便利です。これはprepare、executeのショートカットです。SQLの実行に成功すれば真値を返します。失敗時の挙動はRaiseError属性に準じます。第3引数以降にプレースホルダに埋め込む値を渡すことができます。

my $room = "room1";
my $user = "karupanerura";
my $msg = "hello";
$dbh->do(
    'INSERT chat (room, user, msg) VALUES (?, ?, ?)', {},
    $room, $user, $msg
);

トランザクション処理─⁠─ begin_work、commit、rollback

DBIでトランザクションを扱うにはbegin_work、commit、rollbackメソッドを利用します。基本的にSQLを書く代わりに同様の名前のメソッドを呼ぶイメージですが、AutoCommit属性の状態によって正しい使い方が異なります。

$dbh->begin_work;

$dbh->do(...);
$dbh->do(...);

$dbh->commit;

AutoCommit属性が偽値である場合は、常にbegin_workされているような状態になります。そのため、begin_workを明示的に呼び出すとAlready in atransactionエラーが発生します[5]⁠。また、暗黙的にトランザクション内にいるため、commitrollbackを明示的に呼び出す必要があります。

AutoCommit属性が真値である場合は、明示的にbegin_workを呼び出さない限りトランザクションとして処理されません。こちらのほうがコンソールから直接MySQLを操作する感覚に近いでしょう。

Webアプリケーションの場合など、大きなアプリケーションではトランザクションの状態管理が難しくなってしまうため、基本的にはAutoCommit属性は真値に設定しておくほうが無難です。

エラーハンドリング

エラーハンドリングの方法はRaiseErrorの値によって異なります。

RaiseErrorが偽値である場合は、失敗時にundefなどの偽値を返します。エラーは$DBI::errstrに格納されます。

my $ret = $dbh->do(...);
unless ($ret) {
    # エラー処理
    my $err = $DBI::errstr;
    ...
}

RaiseErrorが真値である場合は、失敗時に$DBI::errstrdieします。失敗時にリカバリ処理を行いたい場合、evalで例外を拾うとよいでしょう。

eval {
    $dbh->do(...);
};
if (my $err = $@) {
    # エラー処理
    ...
}

ちょっとしたスクリプトなどで失敗しても処理を継続したいことが多い場合は、RaiseErrorに偽値を設定したほうが見通しが良くなることがあるかもしれません。しかし、エラーのハンドリング漏れが起きても気づきにくいうえ、エラーが起きても続きの処理が実行できてしまうため、バグを作ってしまいがちです。基本的にはRaiseErrorは真値に設定しましょう。

<続きの(3)こちら。>

おすすめ記事

記事・ニュース一覧