DBIx::Classメモ

本番のアプリケーションを動かすのはMySQLPostgreSQLだけど、テストはSQLiteでやりたい。

まずは、SQLiteでテストができる環境をDebian GNU/Linux Lenny上に用意。

% sudo aptitude install libdbix-class-perl sqlite3 libdbd-sqlite3-perl

次にNekokak's core dump:Perl/DBICにしたがい練習してみる。ただし、SQLiteで。PeroDoc和訳:DBIx::Class::Manual::Example - シンプルなCDデータベースの例にディレクトリを構成。

% mkdir -p app/db
% cd app/db

データベースのスキーマを以下のようにuser.sqlとして作成。

create table user(
       id    int(10)    NOT NULL autoincrement,
       name  varchar(256)   NOT NULL,
       PRIMARY KEY (id)
);

これを元にデータベース作成。

% sqlite3 example.db < user.sql 
SQL error near line 1: near "autoincrement": syntax error

さっそく挫折。Googleで「sqlite autoincrement」で検索した結果見つけたページによると、SQLiteではINTEGER PRIMARY KEYと指定するとその列をNULLでINSERTすると、その列中の最大数+1が入力され、INTEGER PRIMARY KEY AUTOINCREMENTとすると、その列をNULLでINSERTすると、テーブルのその列においてユニークな数字が挿入されるらしい出典)。SQLiteMySQLだとSQLの書き方からして違うのね。

user.sqlを以下のように変更。

create table user(
      id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
      name  TEXT   NOT NULL
);

これを元にデータベース作成。

% sqlite3 example.db < user.sql 

あとは、Nekokak's core dump:Perl/DBICにしたがい練習してみる。

% cd ..
% mkdir -p Neko/Schema

Neko/Schema.pmを作成する。

package Neko::Schema;

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

__PACKAGE__->load_classes(qw/User/);

1;

次にNeko/Schema/User.pmを作成する(SQLiteなのでPK::Auto、MySQLだとPK::Auto::MySQLらしい)。

package Neko::Schema::User;

use strict;
use warnings;
use base 'DBIx::Class';

__PACKAGE__->load_components(qw/ PK::Auto Core/);
__PACKAGE__->table('user');
__PACKAGE__->add_columns(qw/id name/);
__PACKAGE__->set_primary_key('id');

1;

次にapp/test.plを用意し、insert, select, update, deleteを試す

#! /usr/bin/perl

use strict;
use warnings;
use Neko::Schema;

my $schema = Neko::Schema->connect('dbi:SQLite:db/example.db');
#my $schema = Neko::Schema->connect('dbi:mysql:nekodb','nekokak','******');

# insert
$schema->resultset('User')->create({id => 1, name => 'nekokak'});

# select
my $user = $schema->resultset('User')->find(1);
print $user->id,"\n";
print $user->name,"\n";

# update
$user->name('nomaneko');
$user->update;

# select
$user = $schema->resultset('User')->find(1);
print $user->id,"\n";
print $user->name,"\n";

# delete
$user->delete;

読み進めて、1対多の関係の部分。まずは、テーブルを追加する。app/db/bookmark.sqlを以下の内容で作成する。

create table bookmark (
    id      INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL REFERENCES user(id),
    url     TEXT NOT NULL
);

これを元にテーブル追加。

% sqlite3 db/example.db < db/bookmark.sql 

app/Neko/Schema/Bookmark.pmを以下のように作成。

package Neko::Schema::Bookmark;

use strict;
use warnings;

use base 'DBIx::Class';

__PACKAGE__->load_components(qw/ PK::Auto Core/);
__PACKAGE__->table('bookmark');
__PACKAGE__->add_columns(qw/id user_id url/);
__PACKAGE__->set_primary_key('id');

__PACKAGE__->belongs_to(user_id => 'Neko::Schema::User');

1;

app/Neko/Schema/User.pmを以下のように変更。

package Neko::Schema::User;

use strict;
use warnings;
use base 'DBIx::Class';

__PACKAGE__->load_components(qw/ PK::Auto Core/);
__PACKAGE__->table('user');
__PACKAGE__->add_columns(qw/id name/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(bookmark => 'Neko::Schema::Bookmark', 'user_id');

1;

app/Neko/Schema.pmを以下のように変更。

package Neko::Schema;

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

__PACKAGE__->load_classes(qw/User Bookmark/);

1;

リレーションを試してみるapp/test2.plを以下のように作成

#! /usr/bin/perl

use strict;
use warnings;
use Neko::Schema;

my $schema = Neko::Schema->connect('dbi:SQLite:db/example.db');

# insert
$schema->resultset('User')->create({id => 1, name => 'nekokak'});
$schema->resultset('User')->create({id => 2, name => 'nomaneko'});

# select
my $userit = $schema->resultset('User');
while(my $user = $userit->next){
  print $user->id,',',$user->name,"\n";
}

# insert
$schema->resultset('Bookmark')->create({id => 1, user_id => 1, url => 'http://hogehoge.jp'});
$schema->resultset('Bookmark')->create({id => 2, user_id => 1, url => 'http://hugahuga.jp'});
$schema->resultset('Bookmark')->create({id => 3, user_id => 2, url => 'http://kurukuru.jp'});
$schema->resultset('Bookmark')->create({id => 4, user_id => 2, url => 'http://borabora.jp'});

# select
my $bookmarkit = $schema->resultset('Bookmark');
while(my $bookmark = $bookmarkit->next){
  print $bookmark->id,",",$bookmark->user_id,",",$bookmark->url,"\n";
}

$userit = $schema->resultset('User')->search({name => 'nekokak'});
while ( my $user = $userit->next ) {
    my $bkit = $user->bookmark;
    while ( my $bookmark = $bkit->next ) {
        print $bookmark->id,':',$user->id,':',$bookmark->url,"\n";
    }
}

my $bkit = $schema->resultset('Bookmark')->search();
while ( my $bookmark = $bkit->next ) {
    my $user = $bookmark->user_id;
    print $user->id,':',$bookmark->id,':',$user->name,':',$bookmark->url,"\n";
}

#delete
$userit = $schema->resultset('User');
while(my $user = $userit->next){
  $user->delete;
}

$bookmarkit = $schema->resultset('Bookmark');
while(my $bookmark = $bookmarkit->next){
  $bookmark->delete;
}

exit;

inflateとdeflateが何のことやらわからなかったのでGoogle様にお伺い。Walrus, Digit.:Perlメモ/DBIx::Classモジュール: データベース入出時にフィールド値を変換する(inflate、deflate)によるとINSERTやUPDATEをしたときの自動記録に便利そうな機能であるということがわかった。

DateTime::Format::SQLiteなんて無いようなので、DateTime::Format::MySQLを使う。

app/db/login.sqlを以下のように用意する。

create table login (
    id      INTEGER PRIMARY KEY REFERENCES user(id),
    login_time DATETIME
);

app/Neko/Schema/Login.pmを以下のように用意する。

package Neko::Schema::Login;

use strict;
use warnings;
use base 'DBIx::Class';
use DateTime::Format::MySQL;

__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('login');
__PACKAGE__->add_columns(qw/id login_time/);
__PACKAGE__->set_primary_key('id');

__PACKAGE__->might_have(id => 'Neko::Schema::User');
__PACKAGE__->inflate_column('login_time', {
    inflate => sub { DateTime::Format::MySQL->parse_datetime(shift); },
    deflate => sub { DateTime::Format::MySQL->format_datetime(shift); },
});

app/Neko/Schema/User.pmを以下のように変更。

package Neko::Schema::User;

use strict;
use warnings;
use base 'DBIx::Class';

__PACKAGE__->load_components(qw/ PK::Auto Core/);
__PACKAGE__->table('user');
__PACKAGE__->add_columns(qw/id name/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(bookmark => 'Neko::Schema::Bookmark', 'user_id');
__PACKAGE__->might_have(id => 'Neko::Schema::Login');
1;

app/Neko/Schema.pmを以下のように変更

package Neko::Schema;

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

__PACKAGE__->load_classes(qw/User Bookmark Login/);

1;

inflateとdeflateを試すスクリプトをapp/test3.plとして以下のように書く。

#! /usr/bin/perl

use strict;
use warnings;
use Neko::Schema;

my $schema = Neko::Schema->connect('dbi:SQLite:db/example.db');
$schema->storage->debug(1);
$schema->storage->debugfh(IO::File->new('/tmp/trace.out', 'a'));

my $i = $schema->resultset('User')->create({name => 'nekokak'});
$schema->resultset('Login')->create({id => $i->last_insert_id,
                                     login_time => '2006-02-03'});

my $it = $schema->resultset('Login')->search();
while ( my $login = $it->next ) {
    my $u = $login->user_id;
    print $login->id,':',$u->name,':',$login->login_time,"\n";
}

exit;

これで実行するとどうもうまくいかない。

% perl test3.pl
Can't locate object method "last_insert_id" via package "Neko::Schema::User" at test3.pl line 10.

検索しても原因不明。とりあえずここまで。

SQL文はMySQLSQLiteで共有できないけれども、ソースコードはconnect部分を除き共有できそう。