Perl Hackers Hub

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

この記事を読むのに必要な時間:およそ 4 分

前回の(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個ものモジュールが存在します。これらのモジュールのほとんどが,データベースの差異を意識せずに利用できるようになっています。

注2)
データベースによっては使い方が違う機能,使えない機能なども一部あります。

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に値を埋め込む際は必ずプレースホルダを使いましょう。

注3)
複数のSQL文の実行ができるかはドライバと属性により異なります。

著者プロフィール

佐藤健太(さとうけんた)

1990年,千葉県生まれ。2011年に株式会社モバイルファクトリーに入社。

現在はソーシャルアプリ事業部所属のリードエンジニアとして,主にPerlによるWebアプリの設計と開発に従事。

Androidアプリの開発,ミドルウェアの検証と導入,パフォーマンスチューニング等も行う。セキュリティ,データベース,ソフトウェアアーキテクチャに関心が高い。

2014年5月にGotanda.pmを発足。YAPC::Asia等でもスピーカーをするなど,Perl関連のコミュニティに積極的に参加している。

好きな言語はPerlとLisp。最近はXSを勉強中。日本酒と寿司とロックンロールが好物。

URL:http://karupas.org/

コメント

コメントの記入