Perl Hackers Hub

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

Resultクラスの拡張

Resultクラス、ResultSetクラスは自分の好みに合わせて拡張できます。

カラムのinflate/deflate

$tweet->created_dateのようなつぶやきの日付を取りたい場合、カラムの値そのままではなくDateTimeオブジェクトを返してくれたらうれしいと思います。その機能を実現するのがカラムのinflate/deflate機能です。inflate(膨らませる)は文字どおりカラムのデータをPerlオブジェクトに変換する機能で、deflate(収縮させる)はPerlオブジェクトをカラムデータへ変換する機能です。

Resultクラス内で

__PACKAGE__->inflate_column('column_name', {
    inflate => sub {
        # カラムデータからオブジェクトを作って返す
    },
    deflate => sub {
        # オブジェクトからカラムデータを作って返す
    },
});

という定義をすると、このカラムのアクセサがオブジェクトのinflate/deflateを行ってくれるようになります。

# inflateされたPerlオブジェクトを返す
my $obj = $raw->column_name;

# Perlオブジェクトをカラムに入れる
$raw->column_name($obj);

このようにデータベースからPerlオブジェクトを取得したり、Perlオブジェクトからそのままデータを入れられるようになるため、便利に使える場面が多いでしょう。

たとえば、JSONデータを格納したjsonというカラムがあるとしましょう。これを自動的に入出力時にPerlオブジェクトと変換を行いたい場合は、次のように書きます。

use JSON;

__PACKAGE__->inflate_column('json', {
    inflate => sub { decode_json(shift) },
    deflate => sub { encode_json(shift) },
});

●日付のinflate/deflate

日付のような、よく使用するであろうinflate/deflateは、自前で定義しなくともDBIx::Class::InflateColumn::DateTimeというパッケージで定義されているため、それを利用すれば簡単に利用できます。

利用するのは簡単で、Resultクラスに

__PACKAGE__->load_components(qw/InflateColumn::DateTime/);

と1行書くだけです。これを書くとdata_typeがDATEもしくはDATETIMEなカラムは自動的にDateTimeへinflate/deflateされるようになります。また、DateTimeオブジェクトのタイムゾーンなどを指定したい場合は、カラム定義に指定するとそのタイムゾーンが使われます。

created_date => {
    data_type => 'DATETIME',
    is_nullable => 0,
    timezone => 'Asia/Tokyo',
},

●カラムの生データへのアクセス

inflate/deflateしている場合、アクセサ経由では常にPerlオブジェクトが返ってきてしまうため、カラムデータそのものにアクセスできないという問題があります。その場合でもget_column/set_columnメソッドを使用するとカラムデータに直接アクセスできます。併せて覚えておくとよいでしょう。

Result/ResultSetクラスに自分のメソッドを増やす

DBIx::Classが用意しているメソッドだけでなく、独自のメソッドをResultクラスやResultSetクラスに定義することもできます。ユーザオブジェクトにfollow関数を追加するには次のようにします。

# in My::Schema::Result::User
sub follow {
    my ($self, $target) = @_;

    $self->add_to_following_maps({
        target => $target->id
    });
}

このようにResultクラスやResultSetクラス上で独自のメソッドを定義すると、それをSchemaオブジェクトから取得したResultクラスなどでそのまま使用できます。

# daisukeというユーザをfollow
my $daisuke = $user_rs->find({ username => 'daisuke' });
$user->follow($daisuke);

組み込みのメソッドを拡張する

ResultクラスやResultSetクラス内では独自のメソッドを定義できるだけではなく、DBIx::Classが用意しているメソッドをオーバーロードすることもできます。たとえばResultクラスの場合INSERT時に呼ばれるinsertメソッド、UPDATE時に呼ばれるupdateメソッド、DELETE時に呼ばれるdeleteメソッドをオーバーロードすることでデフォルトの挙動を変更したり、処理を追加したりできるようになります。

たとえばTweetクラスのようにデータ作成時刻を記録するcreated_date カラムと修正時刻を記録するmodified_dateカラムを持つテーブルがあったとしましょう。これらは次のようにするとそれらの時刻フィールドを自動的に更新されるようプログラムすることができます。

sub insert {
    my $self = shift;

    my $now = DateTime->now;
    $self->created_date($now);
    $self->modified_date($now);

    $self->next::method(@_);
}

sub update {
    my $self = shift;
    $self->modified_date(DateTime->now);

    $self->next::method(@_);
}

next::methodというメソッド呼び出しは、この場所でDBIx::Class が定義しているメソッドを呼び出すという意味です。たいていの場合はこのようにnextメソッドを呼び、その前後に自分の処理を加えて、既存のメソッドに対し処理を追加するような使い方が多いと思います。

ただ、nextを呼ばずにDBIx::Classが定義している処理を完全に上書きすることもできます。

Resultクラス、ResultSetクラスで定義されているメソッドのすべてはこのようにオーバーロードすることができます。

DBIx::Classハックのためのヒント

1ページ目で少し触れたnextによるメソッドチェインなどからもわかるように、DBIx::Classは一般的なPerlモジュールと比べると少し変わった構造をしています。

C3 MRO

DBIx::Classが一般的なPerlモジュールの構造と一番異なるのが、C3というMROを採用している点です。MROMethod Resolution Orderとは、あるメソッドが呼ばれたときにそのクラスにそのメソッドが存在しなかった場合、どのような順番でメソッドを解決するかのアルゴリズムのことを指します。

DBIx::ClassではC3のメソッド解決のオーダー(順番)を利用したメソッドチェインを採用しているため、DBIx::Classの深いところを知ろうとする場合には、まずこの順番を頭に入れる必要があります。

またC3 MROは、Perl 5.9.5以上からはPerl本体に組み込まれ、より高速に使用できるようになりました(mroプラグマ⁠⁠。したがってDBIx::Classを使用する場合は安定版のPerl 5.10以上を使用すると、より高速に動作させることができます。C3 MROについての詳細も、mroプラグマのドキュメントを参照するのが一番わかりやすいでしょう。

DBIx::Classのクラス構造

すべてのDBIx::ClassコンポーネントはClass::C3::Componentisedというクラスを継承しています。Class::C3::Componentisedのload_componentsというメソッドが、DBIx::Classの構造の肝の部分です。

load_componentsは引数で与えられたクラスをそのクラスの親の階層に追加します。言葉ではわかりにくいので例を見てみましょう。

package My::Schema::Result::Tweet;
use base 'DBIx::Class::Core';

__PACKAGE__->load_components(qw/InflateColumn::DateTime/);

このResult::TweetクラスはDBIx::Class::Coreを継承していますが、load_componentsをすることで親にDBIx::Class::InflateColumn::DateTime が追加されます。つまり、図2という構造になるということです。

図2 Result::Tweetの継承図
図2 Result::Tweetの継承図

さらにDBIx::Class::Coreはその中で、

__PACKAGE__->load_components(qw/
  Relationship
  InflateColumn
  PK::Auto
  PK
  Row
  ResultSourceProxy::Table
/);

をしています。したがって最終的には図3のようなクラス構造になっていることになります。この状態でTweetテーブルからのC3メソッド解決の順番は、

  • ① My::Schema::Result::Album
  • ② DBIx::Class::InateColumn::DateTime
  • ③ DBIx::Class::Core
  • ④ DBIx::Class::Relationship
  • ⑤ DBIx::Class::InateColumn
  • ⑥ DBIx::Class::PK::Auto
  • ⑦ DBIx::Class::PK
  • ⑧ DBIx::Class::Row
  • ⑨ DBIx::Class::ResultSourceProxy::Table

となります。

図3 Result::Tweetの継承図詳細版
図3 Result::Tweetの継承図詳細版
クラス名の「DBIx::Class::」は省略している

前節でResultクラスのupdateメソッドを上書きしましたが、その中でnextというメソッドを呼び出しました。nextは上記の順番で次のクラスの同名のメソッドを探し、見つかるとそれを実行します。これが、DBIx::Classのメソッドチェインのしくみで、DBIx::Classのほとんどすべてのメソッドは、このしくみを利用してロードされたコンポーネントによって挙動を追加・変更したり、ユーザが自由に拡張できるような設計になっています。また、このような独特なしくみによって構成されているため、DBIx::Classのソースを理解するためにはまずこのメソッドチェインのしくみを理解する必要があります。

まとめ

DBIx::Classは機能が豊富な分、巨大なモジュールです。またC3によるメソッドチェインなど独特のしくみも合わさり、取っつきにくいモジュールかもしれません。しかし一度覚えてしまうと手放せないモジュールになることは保証します。そしてその拡張性の高さに驚くことになると思います。

本稿ではDBIx::Classのすべての機能はとても紹介しきれませんでした。興味の持った方はぜひドキュメントを参照してください。

次回の執筆者は和田裕介(ゆーすけべー)さんで、テーマはWeb APIです。お楽しみに。

おすすめ記事

記事・ニュース一覧