(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
でインストールします。
モジュールがロードできればインストールに成功しています。
$ 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::Loader
とTeng::Schema::Declare
の2つの方法でスキーマ情報を得ることができます。
Teng::Schema::Loader
を利用すると、Teng
はデータベースサーバから自動的にスキーマ情報を取得して利用します。細かいスキーマ情報の指定はできませんが、その代わり手軽に利用できるのが特徴です。短期的に運用する小規模なサービスを作るときやちょっと試してみるときなどに便利です。
Teng::Schema::Declare
を利用すると、内部DSL(Domain 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_info
にDBI
のconnect
メソッドに渡すべき値を渡すことにより、接続先のデータベースを指定します。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
データを更新する場合はinsert
、update
、delete
メソッドを利用します。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
DBI
のdo
メソッドと利用方法は同じですが、エラーをhandle_error
メソッドでハンドリングできます。
Rowクラスを拡張する
これまでに紹介したコードでは、単一行のデータをRowクラスのオブジェクトにするメリットがあいまいでした。Rowオブジェクトは独自に拡張することにより真価を発揮します。
独自のRowクラスを定義する
Teng
を継承したクラス以下のRow名前空間にテーブル名をCamelCaseで表現したクラスを作成すると、それが自動的に利用されます。たとえば、Teng
を継承したMyApp::DB
でfoo_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::Sunny
、DBIx::Class
など、ほかのモジュールについても調べてみてください。DBIx::Sunny
はDBI
に便利なメソッド群などを追加してくれるモジュールです。最近は筆者はDBI
の代わりにDBIx::Sunny
を利用することが多いです。また、DBIx::Class
はPerlで最もメジャーなO/Rマッパの一つです。ぜひ、利用して使い勝手や機能をTeng
と比較してみてください。
さて、次回の執筆者はhide_o_55さんで、テーマは「Perlで自然言語処理入門」です。お楽しみに。