Perl Hackers Hub

第3回DBIx::Classでデータベース操作(2)

Schema::Loaderの利用

第3回(1)でResultクラスには各テーブルがどのようなカラムを持っているかを定義する必要があると書きましたが、⁠そのようなテーブル情報はデータベースから自動的に取得できるのでは?」と思った方もいるかもしれません。DBIx::Class::Schema::Loaderという別で配布されているモジュールを使用すると、Resultクラスでのテーブル情報の定義を省略できます。

Schema::Loaderを使うべきか

軽いデータベース操作であればSchema::Loaderがお手軽ですが、そうではない場合はResultクラスにテーブル定義をしっかりと書くのがお勧めです。Resultクラスにテーブル定義を書くべき理由は主に2つあります。

●DBIx::Classからデータベースを作成できる

Resultクラスにテーブル定義を書くと、DBIx::Classからデータベースを作成できます。しっかりテーブル定義が書かれている場合、

my $schema =
    My::Schema->connect('dbi:SQLite:/path/to/my.db');
$schema->deploy;

とdeployメソッドを使用してデータベースを作成できます。これは特定のデータベース実装に依存しないようにできていますので、上記の例ではSQLiteデータベースを作成していますし、次のようにすれば同じ定義からMy SQLに対してテーブルを作成することもできます。

my $schema = My::Schema->connect(
    'dbi:mysql:mydb', 'mysql_user', 'mysql_password',
);
$schema->deploy;

これで、Resultクラスに書かれているテーブル定義に沿ってCREATE TABLEが実行され、テーブルが作成されます。なお、My SQLの場合はあらかじめデータベースは作成しておく必要があります。

●Schemaバージョニング機能が利用できる

Resultクラスにテーブル定義を書くと、Schemaバージョニングというさらに便利な機能も利用できます。

開発を進めていくにつれ、もしくはアプリケーションのバージョンアップ時など、データベース定義を変更することがあると思います。このような場合にdeployメソッドは役に立たないでしょう。そのたびにテーブルを作りなおすわけにはいきません。

Schemaバージョニングは、DBIx::Classでのデータベース定義と実際のデータベース定義との差分を判別して適切なALTER TABLEを発行してくれたり、バージョンが一致しないデータベースに接続しようとすると警告を発してくれたりする便利な機能です。

この機能の詳細はDBIx::Class::Schema::Versionedにまとまっていますので参照してください。

ページャオブジェクト

データベースから取得するデータが多い場合には、LIMIT句などを使用してデータサイズを指定したページング処理を使うことが多いと思います。

DBIx::Classのsearchメソッドには検索結果と連動したページャオブジェクトを取得できる機能があります。ページャオブジェクトを使用するとWebサイトなどでよく使用するページネーション表示などを簡単に作成できます。

ページャオブジェクトを使用するにはsearchメソッドにrows属性とpage属性を渡します。

my $rs = $schema->resultset('Tweets')->search(
    {}, { rows => 10, page => 1, }
);

rows属性には1ページあたりの表示件数を、page属性には何ページめを表示するかをそれぞれ渡します。すると返されたResultSetオブジェクトのpagerメソッドで、それに対応するページャオブジェクトを取得できます。

my $pager = $rs->pager;

このページャオブジェクトはData::Pageのオブジェクトになっています。

トランザクション

トランザクションをサポートするRDBMSを使用している場合にはDBIx::Classはトランザクションをサポートします。現在DBIx::Classからトランザクションを扱うには次の3つの方法があります。

  • txn_do
  • txn_scope_guard
  • txn_begin/txn_commit/txn_rollback

これらはすべてSchemaオブジェクトのメソッドです。

txn_doを使ったトランザクション

DBIx::Classにおける一番確実なトランザクションの方法は、txn_doメソッドを使用することです。

use Try::Tiny;

my $res;
try {
    $res = $schema->txn_do(sub {
        # トランザクション処理したい一連の処理
    });
} catch {
    if ($_ =~ /Rollback failed/) {
        # ロールバックに失敗した場合
    }

    # 何らかのエラーによりロールバックした
}

txn_doにはトランザクションしたい処理をコードリファレンスにして渡します。txn_doはまずトランザクションを開始しその中でコードリファレンスを実行します。そしてその中で何らかのエラーが発生すると自動的にrollbackを発行してくれます。コードリファレンス内で起きた例外はrethrowされるので、それをキャッチするためにはevalでくくるかこの例のようにTry::Tinyなどを使用してあげる必要があります。

txn_doを使用する注意点として、コードリファレンス内にはデータベースを扱う処理以外も書くことができますが、もちろんそれらはデータベースのロールバックが呼ばれても元に戻ることはありません。また、処理中にデータベースの接続が切れた場合、DBIx::Classは自動的に初めから処理をやりなおします。そのときにはデータベースに関係のないコードは2回実行されてしまうことになります。データベース操作以外のコードをコードリファレンスに含める場合は、意図せずコードが実行されてしまう可能性があることに注意してください。

txn_scope_guardを使ったトランザクション

より簡単なトランザクションの方法は、txn_scope_guardを使用することです。

my $txn_guard = $schema->txn_scope_guard;

# トランザクションしたい一連の処理
$tweet->body("val1");

$tweet->update;

$txn_guard->commit;

これは変数のスコープを利用したトランザクション処理です。DBIx::Classはトランザクションガード変数が生成された場合トランザクション処理を開始し、そのガード変数のcommitメソッドが呼ばれるとトランザクションをコミットします。commitが呼ばれる前に何らかの理由(例外が発生した、commitを呼ぶ前にreturnしたなど)で変数がDESTROYすると、自動的にロールバック処理を行います。

txn_doを利用したトランザクションと比べると途中でデータベースとの接続が切れた場合のリトライ機能がないなどの欠点はありますが、よりシンプルにトランザクション処理が定義できるというメリットがあります。

txn_begin/txn_commit/
txn_rollback

DBIx::Class上でのトランザクションは先述したtxn_doもしくはtxn_scpe_guradの使用が推奨されていますが、BEGIN、COMMIT、ROLLBACK相当のメソッドも用意されており、これらを使うと通常のデータベース操作と同じ感覚でトランザクション処理を行うことができます。

my $err = '';
try {
    $schema->txn_begin;

    # トランザクション処理をしたい一連の処理

    $schema->txn_commit;
} catch {
    $err = $_;
    try { $schema->txn_rollback } catch { $err = $_ };
};
die $err if $err; # 必要があれば例外をrethrow

トランザクションの入れ子

現実のアプリケーションコードではまとまった処理ごとに関数などを作成し、その中でトランザクションを使用することが多いでしょう。そのトランザクションに何らかの処理を追加したいというような場合、通常のSQLではCOMMITを発行した時点でトランザクションが終わってしまうので、既存のトランザクション処理に追加で何か処理を入れたい場合めんどうです。

しかし、今までに紹介したDBIx::Classのtxn_*というメソッド群を使用したトランザクションはどれも入れ子にして使用できます。この機能により、トランザクションを内部的に使用した複数のメソッドを同時に1つのトランザクションとして実行できるようになります。なお、入れ子にして使用したトランザクションは外側のものが有効になります。

UTF-8の扱い

モダンなPerlアプリケーションのベストプラクティスの一つにアプリケーション内部の文字列はすべてUTF-8で扱うというものがあり、そのようなアプリケーションを書いている方が多いと思います。この場合、データベースへのデータの入出力時にUTF-8のエンコード・デコード処理が自動的に行われてほしいと思うかもしれません。

このような場合は、DBIx::Class自体ではなくDBD::SQLite、DBD::Pgなどが持つDBDレベルでのエンコーディングサポートを使用するのが現在推奨されている方法です。DBDへのオプションはconnectメソッドで渡すことができますリスト4⁠。リスト4のようにしてデータベースに接続すると、データベースから取得したデータは自動的にutf8フラグの付いたものになり、フラグの付いたデータを適切にエンコードしてデータベースに格納できるようになります。

リスト4 DBDレベルでのutf-8オプション
# SQLiteの場合
My::Schema->connect(
    'dbi:SQLite:/path/to/database.db', '', '', {
        sqlite_unicode => 1,
    },
);

# My SQLの場合
My::Schema->connect(
    'dbi:mysql:database', 'db_user', 'db_password', {
        on_connect_do => ['SET NAMES utf8'],
        mysql_enable_utf8 => 1,
    }
);

# Postgre SQLの場合
My::Schema->connect(
    'dbi:Pg:database', 'db_user', 'db_password', {
        pg_enable_utf8 => 1,
    }
);

デバッグ

DBIC_TRACE環境変数

DBIx::Classを使用していると、DBIx::Classが実際にどのようなSQLを生成しているのかを確認したくなることがあると思います。そのような場合はDBIC_TRACE環境変数を使用すると、実行されているSQLを標準エラーで確認できます。

$ DBIC_TRACE=1 perl your_app.pl

また次のようにファイル名を指定することで、標準出力の代わりにファイルへSQLをはき出すこともできます。

$ DBIC_TRACE="1=/path/to/trace.txt" perl your_app.pl

SQLクエリのプロファイリング

DBIC_TRACE環境変数は実行しているSQLをダンプするだけの機能しかありませんが、DBIx::Class::Storage::Statisticsのサブクラスを作成することで、このダンプする部分のコードを自分のものに差し替えることができます。

My::Schema->storage->debugobj(My::Profiler->new);

こうすることでDBIC_TRACE時の処理を自分のプロファイラに置き換えることができます。この機能の詳細はDBIx::Class::Manual::Cookbookに使用例が載っていますのでそちらを参照ください。

またこの機能を使用したSQLプロファイリングモジュールとしてDBIx::Class::QueryLogというモジュールもあります。

おすすめ記事

記事・ニュース一覧