Perl Hackers Hub

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

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回はカヤックの村瀬大輔さんで、テーマはDBIx::Classです。

DBIx::Classとは

DBIx::ClassはPerlのO/Rマッピングモジュールです。O/RマッピングObject/Relational Mapping以下ORM)とは、オブジェクト指向言語におけるオブジェクトとリレーショナルデータベースを紐づけるしくみのことで、ORMを使用するとユーザは直感的なオブジェクト操作によってデータベースを操作できるようになります。

DBIx::ClassはPerlのORMとしては現在世界で一番使われているモジュールです。日本では最近データベース操作モジュールとしてより軽量なDBIx::SkinnyやData::Modelなどの注目が高まってきていますが、機能的に枯れている点や豊富にテストされている点でDBIx::Classが現時点では最も信頼できるデータベース操作モジュールと言えるでしょう。

DBIx::Classも登場時にはいろいろな記事になりましたが、最近ではあまり取り上げられることもなくなりました。しかし登場時と比べると機能はずいぶん強化され、基本的な使い方での推奨方法も若干変更されていたりします。本稿では基本的な使い方を一通りおさらいしたあと、日本ではあまり記事にされていない発展的な使い方をいくつか紹介します。

サンプルDBスキーマ

本稿ではリスト1の定義のデータベースをサンプルとして使用します。このサンプルデータベースはTwitterのデータ構造を模したものになっていて、次のテーブルを持ちます。

  • user:ユーザ
  • tweet:つぶやき
  • following_map:フォロー関係
  • user_profile:ユーザ詳細プロフィール
リスト1 サンプルDBスキーマ
CREATE TABLE user (
    id INTEGER NOT NULL PRIMARY KEY,
    username VARCHAR(255) NOT NULL
);

CREATE TABLE user_profile (
    id INTEGER NOT NULL PRIMARY KEY, -- user.id
    full_name VARCHAR(255) NOT NULL,
    bio VARCHAR(255)
);

CREATE TABLE following_map (
    id INTEGER NOT NULL PRIMARY KEY,
    user INTEGER NOT NULL, -- user.id
    target INTEGER NOT NULL -- user.id
);

CREATE TABLE tweet (
    id INTEGER NOT NULL PRIMARY KEY,
    user INTEGER NOT NULL, -- user.id
    body VARCHAR(255) NOT NULL,
    created_date DATETIME NOT NULL,
    modified_date DATETIME NOT NULL
);

基本的な使い方

まずはDBIx::Classの基本的な使い方を見てみましょう。

データベースクラス定義

DBIx::Classを使うには、まずそれを継承した自前のクラスを作成し、使用するデータベース情報を定義します。作成するクラスは次の3つがあります。

  • Schemaクラス
  • Resultクラス
  • ResultSetクラス

●Schemaクラス

SchemaクラスはDBIx::Classを使ううえでベースとなるクラスで、次のように定義します。

package My::Schema;
use strict;
use warnings;
use base 'DBIx::Class::Schema';

__PACKAGE__->load_namespaces;
1;

この例ではMy::Schemaというクラス名でSchemaクラスを作成しています。

SchemaクラスはDBIx::Class::Schemaを継承して作成し、その中ではどのようにコンポーネントクラスを読み込むかなどを定義します。後方互換的にいろいろな定義方法があるのですが、現在は例のようにload_namespacesを使用するのが一般的です。

load_namespacesは、

  • My::Schema::Result::*にあるResultクラス
  • My::Schema::ResultSet::*にあるResultSetクラス

を自動的にロードするメソッドです。

●Resultクラス

Resultクラスはデータベースのテーブルを表すクラスで、操作する必要のあるテーブルの数だけ定義する必要があります。

Resultクラスは次のようにDBIx::Class::Coreを継承して作成します。

package My::Schema::Result::User;
use strict;
use warnings;
use base 'DBIx::Class::Core';

そしてこのResultクラスがデータベースのどのテーブルに紐付いているかを定義します。

__PACKAGE__->table('user');

続いてそのテーブルがどのようなカラムを持つのかも定義します。

__PACKAGE__->add_columns(qw/id username/);

カラム名だけでなく、より詳細な情報を登録することもできます。

__PACKAGE__->add_columns(
    id => {
        data_type         => 'INTEGER',
        is_nullable       => 0,
        is_auto_increment => 1,
    },
    username => {
        data_type   => 'VARCHAR',
        size        => 255,
        is_nullable => 0,
    },
);

DBIx::Classのコアではこの詳細情報は使われないので必ずしも詳細に定義する必要はありませんが、詳細に定義しておくとこの情報からデータベースにCREATETABLEを発行したり、DBIx::Class::WebFormなどのコンポーネントが使用できるようになったりします。

そして、プライマリキーの設定をします。

__PACKAGE__->set_primary_key('id');

ここまでが、テーブル定義に最低限必要な記述です。

●ResultSetクラス

ResultSetクラスはResultクラスの集合を表すクラスです。このクラスの定義は必須ではありません。定義しなかった場合はデフォルトのDBIx::Class::ResultSetがそのまま使用されます。

定義したクラスを使用する

●データベース接続情報定義

定義したスキーマクラスを使用するには、まずconnectメソッドを使用してスキーマオブジェクトを作ります。

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

connectメソッドに渡す引数はDBIのそれとほとんど同じで、次のようになっています。

My::Schema->connect( $dsn, $user, $password, $attrs );

●テーブルの操作

データベーステーブルを操作するにはそのテーブルに対するResultSet オブジェクトを取得します。ResultSet オブジェクトはSchema オブジェクトのresultsetメソッドで取得できます。

my $user_rs = $schema->resultset('User');

このResultSetオブジェクトが持つ各種メソッドを使用することで、ユーザテーブルに対する操作を行うことができますリスト2⁠。

リスト2 ResultSetオブジェクトによるテーブル操作
# プライマリキーによる取得
my $user = $user_rs->find(14);

# 検索
my $rs = $user_rs->search({
    username => { -like => 'daisuke%' },
});
while (my $user = $rs->next) {
    print $user->username, "\n";
}

# ユーザ作成
my $new_user = $user_rs->create({ username => 'daisuke' });

●レコードの操作

特定のレコードに対する操作はResultオブジェクトを使用して行いますリスト3⁠。

リスト3 Resultオブジェクトによるレコード操作
# 値の取得
print $user->username, "\n";

# データの更新
$user->username('daisuke');
$user->update;

# データの削除
$user->delete;

このようにレコードに対する操作はResultオブジェクトで、レコードの集合であるテーブルに対する操作はResultSetオブジェクトでそれぞれ操作します。

リレーションの定義

DBIx::Classでは複数テーブルの関係性をResultクラスに定義することで、その関係を使って楽にデータを扱うことができます。

●1:多─⁠─belongs_toとhas_many

ユーザ(User)とつぶやき(Tweet)との関係性に注目した場合、つぶやきは1つのユーザに属しbelongs_to⁠、ユーザは複数のつぶやきを持つhas_many関係にあります。これを1:多のリレーションと言い、データベースモデルの中で最も使われるリレーションモデルです。

DBIx::Class で1:多を定義するには、それぞれbelongs_toメソッドhas_manyメソッドを用います。

# in My::Schema::Result::User
__PACKAGE__->has_many(
    tweets => 'My::Schema::Result::Tweet', 'user',
);

# in My::Schema::Result::Tweet
__PACKAGE__->belongs_to(
    user => 'My::Schema::Result::User',
);

has_manyメソッドの第3引数ではTweet側のユーザIDを格納しているカラム名を指定します。このようにすることで、

my $tweet_rs = $user->tweets
my $user     = $tweet->user;

と関係しているオブジェクトを直感的に取得できるようになります。

また、データ取得だけでなく検索やデータ追加なども次のようにより直感的に行えます。

# 特定のユーザのつぶやきを検索
my $tweet_rs =
    $user->tweets({ body => { -like => 'Hello %' }});

# 特定のユーザにつぶやきを追加
$user->add_to_tweets({ body => 'Hello World!' });

●1:1─⁠─has_oneとmight_have

1:1のリレーションとは、複数のテーブルが共通の(プライマリ)キーを持つというようなリレーションモデルです。

DBIx::Classで1:1のリレーションを定義するには、has_oneメソッド、might_haveメソッドの2つがあります。has_onemight_haveの違いは、has_oneはリレーション先が必ず存在する場合にしか使えないのに対し、might_haveはリレーション先が存在しても存在しなくても定義できるということと、has_oneINNERJOINを使用するのに対し、might_haveLEFT JOINを使用するということです。

今回のサンプルではUserとUserProfileの関係が1:1にあたります。

# in My::Schema::Result::User
__PACKAGE__->has_one(
    profile => 'My::Schema::Result::UserProfile',
);

●多:多─⁠─many_to_many

多:多のリレーションとは今回の例で言うと、ユーザテーブルとそれらのフォロー関係を定義するfollowing_mapテーブルがあった場合の、マッピングテーブルを介したユーザテーブル同士の関係性を言います図1⁠。

図1 多:多のリレーション
図1 多:多のリレーション

DBIx::Classで多:多を定義にするには、many_to_manyメソッドを用います。

まず、1:多のリレーションを定義します。

# Result::User
__PACKAGE__->has_many(
    following_maps =>
        'My::Schema::Result::FollowingMap', 'user'
);
__PACKAGE__->has_many(
    follower_maps =>
        'My::Schema::Result::FollowingMap', 'target'
);

# Result::FollowingMap
__PACKAGE__->belongs_to(
    user => 'My::Schema::Result::User',
);
__PACKAGE__->belongs_to(
    target => 'My::Schema::Result::User',
);

そしてこの1:多のリレーションを使用して多:多のリレーションを使用できるよう定義します。

# Result::User
__PACKAGE__->many_to_many(
    followings => following_maps => 'target' );
__PACKAGE__->many_to_many(
    followers => follower_maps => 'user' );

以上の定義をすることで$user->followersという直感的なコードでユーザのフォロワー一覧のオブジェクトを取得できるようになります。

●カスケーディングデリート

さて、複数のテーブルがリレーションしている場合、削除処理が面倒になることがあります。ユーザを削除しようとした場合にそのユーザのつぶやきデータも併せて削除しないと、データの整合性がとれなくなってしまいます。DBIx::Classではリレーション定義を行っておくと関連するすべてのデータを同時に削除してくれます。これをカスケーディングデリートと言います。

# これだけでユーザが持つつぶやきデータも同時に削除される
$user->delete;

基本事項はこのくらいにして、以降では発展的な使い方を見ていきましょう。記事の都合でDBIx::Classのすべてをカバーすることはできませんので、書ききれない分はドキュメントを参照してください。

おすすめ記事

記事・ニュース一覧