Perl Hackers Hub

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

(1)こちら⁠2)こちらから。

Tengの使い方

(2)ではDBIを使ってどのようにデータベースを操作できるかを解説しました。DBIはそのままでも十分に高機能ですが、ハッシュリファレンスや配列リファレンスで表現された単一行のデータをそのまま利用して複雑なロジックを実装すると、コードが複雑になってしまう場合があります。ロジックが複雑化するケースは単一行のデータをクラスに紐づけてオブジェクト化すると扱いやすくなることがよくあります。

O/RマッパObject-Relational Mapperは、その名のとおり単一行のデータをクラスに紐づけてオブジェクト化することを主目的とした実装です。O/Rマッパを利用することで、データベースを扱う処理をよりシンプルに実装できます。多くのO/Rマッパは、O/Rマッピングのほかにもクエリビルダなどのアプリケーション開発に便利な機能を備えています。

(3)では、Perl製のO/Rマッパの一つであるTengの機能とその基本的な使い方を解説します。なお、本稿のコードは、執筆時点の最新であるTengのバージョン0.25を前提に記述しています。

Tengとは何か

Tengは非常にシンプルなO/R マッパです。DBIx::ClassなどのほかのO/Rマッパと比較して、シンプルで覚えやすく、トラブル時にコードが追いやすいという特徴を持ちます。また、余計な機能がないため、O/Rマッパの学習にも適しています。

Tengのインストール

cpanmでインストールします。

$ cpanm Teng

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

$ perl -MTeng -E 'say $Teng::VERSION'

基本的な使い方

本項では、Tengを使ってデータベースプログラミングを行う基本的な手順を解説します。

Tengを継承したクラスの用意

Tengを利用するためには、Tengを継承したクラスを用意する必要があります。今回はMyApp::DBに用意します。次のようにただ継承するだけです。

package MyApp::DB;
use parent qw/Teng/;
1;

スキーマ情報の定義

スキーマ情報とは、データベース構造の情報のことです。具体的には、どのようなテーブルがあり、どのようなカラムが存在し、どのような制約がかかっているのかなど、どのような構造のデータが挿入できるかがわかる情報を指すと思ってください。

O/Rマッパは、O/Rマッパがスキーマ情報を知っていることを前提にデータベースプログラミングをサポートしてくれます。Tengも例外ではなく、TengではTeng::Schema::LoaderTeng::Schema::Declareの2つの方法でスキーマ情報を得ることができます。

Teng::Schema::Loaderを利用すると、Tengはデータベースサーバから自動的にスキーマ情報を取得して利用します。細かいスキーマ情報の指定はできませんが、その代わり手軽に利用できるのが特徴です。短期的に運用する小規模なサービスを作るときやちょっと試してみるときなどに便利です。

Teng::Schema::Declareを利用すると、内部DSLDomain Specific Languageドメイン特化言語)で任意のスキーマ情報をクラスとして定義できます。今回は解説しないinflate/deflate機能[6]の利用や、データサイズの大きなカラムをあえてTengのスキーマ定義から削るなど、細かいスキーマ情報の指定も行えます。

また、Teng::Schema::Dumperを利用すると、データベースサーバからスキーマ情報を取得し、Teng::Schema::Declareを利用したスキーマクラスのソースコードを生成できます。基本的にはTeng::Schema::Dumperを利用してスキーマクラスを生成するとよいでしょう。

今回はTeng::Schema::Loaderを利用してみましょう。次のようにして利用できます。

use DBI;
use MyApp::DB;
use Teng::Schema::Loader;

my $dbh = DBI->connect(...);
my $teng = Teng::Schema::Loader->load(
    dbh => $dbh,
    namespace => 'MyApp::DB'
);

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

Teng::Schema::Loaderを利用する場合、事前にデータベースへの接続を行う必要があるため、スキーマ情報を取得するために利用したデータベースハンドラが再利用されます。そのため、DBIで一度接続してしまえば接続のためにそれ以上の処理は必要ありません。

Teng::Schema::Declareを利用する場合は次のようにして接続できます。

use MyApp::DB;
use MyApp::DB::Schema;

my $teng = MyApp::DB->new({
    connect_info => [$dsn, $user, $pass, $attr],
    schema_class => 'MyApp::DB::Schema',
});

connect_infoDBIconnectメソッドに渡すべき値を渡すことにより、接続先のデータベースを指定します。schema_classにはTeng::Schema::Declareで定義したスキーマクラスを指定します。

DBIのデータベースハンドラの取得――dbh

Tengにはトランザクションの状態管理も考慮した、暗黙的な再接続機能が実装されています[7]⁠。dbhメソッドにより、常に接続が確立されているデータベースハンドラを得ることができます。高速化のためにDBIのメソッドを直接利用したい場合などはdbhメソッドを利用するとよいでしょう。

$teng->dbh->prepare(...);

単一行の取得――single

単一の行を取得したい場合はsingleメソッドを利用します。Tengはクエリビルダとして標準でSQL::Makerを利用しているので、SQL::Makerのフォーマットで指定を行います。詳しくはSQL::Makerのドキュメントを参照してください。

my $row = $teng->single(chat => {
    # WHERE
    room => 'room1',
    user => 'karupanerura',
}, {
    # ORDER BY、LIMIT
    order_by => { created_at => 'DESC' },
    limit => 1,
});

# Row オブジェクトからカラムの値が得られる
say $row->room;

なお、singleメソッドでは暗黙的にLIMITが1であるものとしてSQLが生成されますが、可読性のために基本的にはLIMITを明示したほうがよいでしょう。

複数行の取得――search

複数の行を取得したい場合はsearchメソッドを利用します。

my $iter = $teng->search(chat => {
    room => 'room1',
}, {
    order_by => { created_at => 'DESC' },
});

my @rows = $iter->all;

戻り値としてTeng::Iteratorのオブジェクトが得られます。リストコンテキストで戻り値を評価した場合は、Teng::Iteratorのallメソッドが暗黙的に呼び出されRowオブジェクトの配列が返ります。

データの更新――insert、update、delete

データを更新する場合はinsertupdatedeleteメソッドを利用します。singleメソッドなどと同様にSQL::Makerのフォーマットで引数を渡します。また、Rowオブジェクトから更新を行うこともできます。

my $row = $teng->insert(chat => {
    room => 'room1',
    user => 'karupanerura',
    msg => 'Hello, Teng!'
});

$teng->update(chat => { msg => '<deleted>' }, {
    id => $room->id,
});
$row->update({ msg => '<deleted>' });

$teng->delete(chat => { id => $room->id });
$row->delete();

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

txn_scopeメソッドを利用してトランザクションを利用します。これはDBIx::TransactionManagerの同メソッドへの委譲となっており、戻り値としてガードオブジェクトが得られます。ガードオブジェクトからcommit/rollbackを呼び出すことによりトランザクションを反映できます。

また、意図しない例外やreturnなどでスコープを抜けてしまった場合は暗黙的にrollbackが実行されます。これにより、意図せずトランザクションが継続してしまうことを防げます。

さらに、トランザクションをネストした場合は最も大きな範囲の1つのトランザクションに自動的にまとめてくれるようになっています。

# トランザクションの開始
my $txn = $teng->txn_scope();

# ロックの獲得
$chat = $chat->refetch({ for_update => 1 });

# NG ワードが含まれていなければROLLBACKして終了
if ($chat->msg !~ /ngword/) {
    $txn->rollback;
    return;
}

# NG ワードが含まれる投稿に対する処理
$chat->update({ message => '<censored>' });
$user = $user->refetch({ for_update => 1 });
$user->update({ violations => $user->violations + 1 });

# トランザクションをCOMMIT
$txn->commit;

エラーハンドリング――handle_error

Tengでは、RaiseError属性に暗黙的に真値がセットされて接続されます。エラーをハンドルするためにはhandle_errorメソッドをオーバーライドし、エラーメッセージを例外オブジェクトに変換して処理する方法が賢いでしょう。以下はMySQLのDuplicate entryエラーを例外オブジェクトでthrowする例です。

use MyApp::DB::Exception::DuplicateEntry;

sub handle_error {
    my $self = shift;
    my ($stmt, $bind, $reason) = @_;
    if ($reason =~ /Duplicate entry/) {
        MyApp::DB::Exception::DuplicateEntry->throw(
            message => $reason,
            stmt => $stmt,
            bind => $bind,
        );
    }
    $self->SUPER::handle_error(@_);
}

呼び出しもとではevalなどを利用して例外を捕捉します。たとえば、Try::Liteを利用すると次のようになります。

use Try::Lite;

try {
    $teng->insert(user => { ... });
}
'MyApp::DB::Exception::DuplicateEntry' => sub {
    # エラー処理
    ...
};

直接SQLを指定する

これまでTengのクエリビルダを利用してSQLを実行する方法を解説してきましたが、Tengでは直接SQLを指定して実行することもできます。

名前ベースでのデータのバインド─⁠─ search_named

search_namedメソッドを利用することで、直接SQLを利用してSELECTし、データをRowオブジェクトに変換できます。また、名前ベースでプレースホルダに値を埋め込むことができます。また、同様にsingle_namedメソッドも利用できます。なお、Rowクラスを決定するために利用するテーブル名はSQLから自動的に抽出されます[10]⁠。

my $iter = $teng->search_named(
    'SELECT user, msg FROM chat WHERE room = :room', { room
=> 'room1' });

任意のSQLの実行――do

DBIdoメソッドと利用方法は同じですが、エラーをhandle_errorメソッドでハンドリングできます。

Rowクラスを拡張する

これまでに紹介したコードでは、単一行のデータをRowクラスのオブジェクトにするメリットがあいまいでした。Rowオブジェクトは独自に拡張することにより真価を発揮します。

独自のRowクラスを定義する

Tengを継承したクラス以下のRow名前空間にテーブル名をCamelCaseで表現したクラスを作成すると、それが自動的に利用されます。たとえば、Tengを継承したMyApp::DBfoo_barテーブルのRowクラスを独自定義する場合は、MyApp::DB::Row::FooBarを定義すればよいです。独自に定義したRowクラスではTeng::Rowを継承してください。

package MyApp::DB::Row::FooBar;
use parent qw/Teng::Row/;
1;

Rowクラス拡張の勘どころ

Rowクラスは自由に拡張できるため、ここにさまざまな処理を書いてしまいがちですが、ある程度目的を絞って利用したほうが保守しやすいです。筆者は、Rowクラスで行うべきことはデータの整形、変換、条件判定に絞るべきだと考えています。

たとえば、先ほどトランザクション処理のサンプルコードで$chat->msg !~ /ngword/というコードが登場しました。このコードに相当するものをRowクラスにメソッドとして定義することにより、!$chat->has_ng_word_in_msgと書けるようになります。説明的なコードになり、コメントがなくともわかりやすくなったと思います。このように、Rowクラスは可読性を向上させるために非常に便利に使うことができます。

また、msgメソッドを独自定義して、NGワードのフィルタ処理を挟むといったことも実現できます。このような方法で、既存のコードをできるだけ書き換えずにロジックを変更することもできます。これはワークアラウンドに最適です。ただし、この方法はRowクラスによって既存の挙動が変更されることになるため、挙動がわかりにくくなってしまいます。基本的にはfiltered_msgなど別名のメソッドとして定義してそれを利用するほうがよいでしょう。

まとめ

本稿では、Perlでデータベースプログラミングを行うために必要な知識を解説しました。トランザクションが重要であること、DBIが汎用的な低レベルAPIになっていること、TengなどのO/Rマッパを利用すると可読性と保守性の高い実装が書きやすくなることをご理解いただけたでしょうか。

さらなるステップアップを目指す人は、DBIx::SunnyDBIx::Classなど、ほかのモジュールについても調べてみてください。DBIx::SunnyDBIに便利なメソッド群などを追加してくれるモジュールです。最近は筆者はDBIの代わりにDBIx::Sunnyを利用することが多いです。また、DBIx::ClassはPerlで最もメジャーなO/Rマッパの一つです。ぜひ、利用して使い勝手や機能をTengと比較してみてください。

さて、次回の執筆者はhide_o_55さんで、テーマは「Perlで自然言語処理入門」です。お楽しみに。

おすすめ記事

記事・ニュース一覧