POD2::JA::KiokuDB::Tutorial - KiokuDBを始めよう
(インストール)
KiokuDBとバックエンドと一緒にインストールするには、Task::KiokuDBをインストールするのが一番簡単です。
KiokuDBはMooseと、いくつかのすぐに使えるモジュールに依存していますが、
特定のストレージモジュールには依存していません。
KiokuDBは複数のバックエンドのフロントエンドです。
DBIが実際のデータベースへの接続にDBDを使っているのに似ています。
開発用やテストとして、メモリに保存するKiokuDB::Backend::Hashバックエンドを使うことができます。
プロダクションには、KiokuDB::Backend::DBDかKiokuDB::Backend::DBIかKiokuDB::Backend::BDB
をバックエンドとして推奨します。
KiokuDB::Backend::DBDをインストールして、以下のインストラクションを見てください。
(ディレクトリハンドルの作成)
KiokuDBディレクトリは、すべての仕事がされるメインのオブジェクトです。
すぐに使えるもっとも単純なディレクトリは次のように作れます:
my $dir = KiokuDB->new(
backend => KiokuDB::Backend::Hash->new
);
このドキュメントの最後に、他のもっと面白いバックエンドの設定を紹介しますが、
とりあえず、やってみます。
いろいろなバックエンドに接続するためのDSN文字列を使うこともできます。
KiokuDB->connect("hash");
KiokuDB->connect("dbi:SQLite:dbname=foo", create => 1);
KiokuDB->connect("bdb:dir=foo", create => 1);
設定ファイルを使うこともできます。
KiokuDB->connect("/path/to/my_db.yml");
設定YAMLファイルです:
---
# these are basically the arguments for 'new'
backend:
class: KiokuDB::Backend::DBI
dsn: dbi:SQLite:dbname=/tmp/test.db
create: 1
(DBIバックエンドを使う)
2つの理由で、このチュートリアルではDBIバックエンドを使います。
1つ目の理由は、DBIがどこにでもあるからです。
2つ目の理由は、簡単に裏舞台を見ることが出来るからです。
KiokuDBが何をしているかをよりわかりやすくデモンストレーションできるからです。
この例ですべてのバックエンドがまったく同じように動きます。
以下で使う$dir変数は下記のように作られます:
my $dir = KiokuDB->connect(
"dbi:SQLite:dbname=kiokudb_tutorial.db",
create => 1, # this causes the tables to be created
);
ユーザー名とパスワードで接続する場合、名前付きの引数を指定しないといけません:
my $dir = KiokuDB->connect(
$dsn,
user => $user,
password => $password,
);
(オブジェクトのインサート)
Mooseを使った簡単なクラスを定義してみましょう:
package Person;
use Moose;
has name => (
isa => "Str",
is => "rw",
);
それをインスタント化します:
my $obj = Person->new( name => "Homer Simpson" );
下記のようにオブジェクトをデータベースに入れます:
my $scope = $dir->new_scope;
my $homer_id = $dir->store($obj);
これは、KiokuDBのとても普通の使い方です。ですが、いくつか重要なことを示しています。
1番目に、スキーマは必要ありません。KiokuDBはテーブルのような何かを事前に定義する必要はありません。
オブジェクトの情報を取り出すために、Mooseを使うことができます。
2番目に、データベースに入っているすべてのオブジェクトにはIDがあります。
オブジェクトにIDを選ばなけれあば、KiokuDBが代わりにUUIDを割り当てます。
IDはリレーショナルデータベースのプライマリーキーのようなものです。
自分でオブジェクトにIDを振りたければ、次のようにすることができます:
$dir->store( homer => $obj );
3番目に、すべてのKiokuDB操作は
scope内で行う必要があります。
スコープは上のような簡単な例では大して重要ではありませんが、
循環参照やweakリファレンスが使われるようになると、必要になります。
後でより詳細に見ていきます。
(オブジェクトの読み出し)
さて、データベースにHomerが入りました。"store"から得たIDで取り出せます。
my $homer = $dir->lookup($homer_id);
$scopeと$objは、スコープ内にあるとします。$homerと$objは実際に、同じオブジェクトになります。
# this is true:
refaddr($homer) == refaddr($obj)
生存しているオブジェクトセット
(KiokuDB::LiveObjects)内のオブジェクトが
"生存"しているかをKiokuDBが追跡しているからです。
オブジェクト既にメモリにあるなら、KiokuDBはインスタンスを
バックエンドから取得します。
(何が保存されたか)
データベースを覗いてみましょう:
% sqlite3 kiokudb_tutorial.db
SQLite version 3.4.0
Enter ".help" for instructions
sqlite>
データベースのスキーマには2つのテーブルがあります。"entries"と"gin_index"です:
sqlite> .tables
entries gin_index
"gin_index"はより複雑なクエリに使われます。チュートリアルの最後に扱います。
さて、"entries"に近付いてよく見ましょう:
sqlite> .schema entries
CREATE TABLE entries (
id varchar NOT NULL,
data blob NOT NULL,
class varchar,
root boolean NOT NULL,
tied char(1),
PRIMARY KEY (id)
);
メインのカラムは"id"と"data"です。KiokuDBにある、すべてのオブジェクトにはIDがあり、
プライマリキーとBLOBデータが関連付けられています。
DBIバックエンドのデフォルトのシリアライザーはKiokuDB::Serializer::JSONですので、
データを調査できます。
最初に、"sqlite"の出力モードを"line"にセットしましょう。大きいカラムでも
見やすくなります:
sqlite> .mode line
テーブルからデータを取得します:
sqlite> select id, data from entries;
id = 201C5B55-E759-492F-8F20-A529C7C02C8B
data = {"__CLASS__":"Person","data":{"name":"Homer Simpson"},"id":"201C5B55-E759-492F-8F20-A529C7C02C8B","root":true}
上記のように、"name"属性はblob内の"data"キーにオブジェクトのクラスとして保存されています。
"data"カラムはオブジェクトを再作成するのに必要なすべてのデータを含んでいます。
他のすべてのカラムは検索のためだけに使われます。後で、どのようにユーザー定義のカラムを
作るのかを見せます。
KiokuDB::Backend::DBDを使った場合は、ディスク上のフォーマットは、"id"から"data"のハッシュになり、
他の追加のカラムはありません。
(オブジェクトのリレーションシップ)
"Person"クラスに"name"よりも、もっと面白いデータを追加してみましょう:
package Person;
has spouse => (
isa => "Person",
is => "rw",
weak_ref => 1,
);
"spouse"属性は他のPersonオブジェクトのリファレンスを持ちます。
まずは、他のオブジェクトを作りましょう:
my $marge_id = $dir->store(
Person->new( name => "Marge Simpson" ),
);
データベースに両方のオブジェクトを持たせます。2つを一緒にリンクしましょう:
{
my $scope = $dir->new_scope;
my ( $marge, $homer ) = $dir->lookup( $marge_id, $homer_id );
$marge->spouse($homer);
$homer->spouse($marge);
$dir->store( $marge, $homer );
}
今、永続的な
オブジェクトグラフを作りました。これは、複数のオブジェクトが
お互いに参照しています。
"spouse"には"weak_ref"オプションがありましたので、この循環構造はリークしません。
データベースでオブジェクトが更新されたら、LinkDBは"spouse"属性を含むリファレンスを見て、
この関係はストレージ内でユニークなIDを使ってエンコードされます。
このグラフをロードするために、次のようにできます:
{
my $scope = $dir->new_scope;
my $homer = $dir->lookup($homer_id);
print $homer->spouse->name; # Marge Simpson
}
{
my $scope = $dir->new_scope;
my $marge = $dir->lookup($marge_id);
print $marge->spouse->name; # Homer Simpson
refaddr($marge) == refaddr($marge->spouse->spouse); # true
}
KiokuDBが最初のオブジェクトをロードしたら、そのオブジェクトが依存している
すべてのオブジェクトがロードされます。"spouse"属性は他のオブジェクトを(IDで)
持っているので、インフレーション時にそのリンクを解決します。
("new_scope"の目的)
"new_scope"が重要になるところです。オブジェクトはデータベースからインフレートされ、
リファレンスカウントを増やすために、生存しているオブジェクトスコープに追加されます。
これがされていなければ、"lookup"から$homerが戻ってくる時までに、
"spouse"属性がクリアされます。マージする他のリファレンスがないからです。
次のコードが理由をデモンストレートします:
sub get_homer {
my $homer = Person->new( name => "Homer Simpson" );
my $marge = Person->new( name => "Marge Simpson" );
$homer->spouse($marge);
$marge->spouse($homer);
return $homer;
# at this point $homer and $marge go out of scope
# $homer has a refcount of 1 because it's the return value
# $marge has a refcount of 0, and gets destroyed
# the weak reference in $homer->spouse is cleared
}
my $homer = get_homer();
$homer->spouse; # this returns undef
次のイディオムを使って:
{
my $scope = $dir->new_scope;
# do all KiokuDB work in here
}
少なくとも必要である時間はオブジェクトが生きていることを確保できます。
Webアプリケーションのコンテキストでは、普通リクエストごとに新しいスコープを作ります。
実際、Catalyst::Model::KiokuDBは、自動的にそうしています。
(データベース内のリファレンス)
さて、データベースにオブジェクトグラフがあります。内部がどうなっているか見てみましょう。
sqlite> select id, data from entries;
id = 201C5B55-E759-492F-8F20-A529C7C02C8B
data = {"__CLASS__":"Person","data":{"name":"Homer Simpson","spouse":{"$ref":"05A8D61C-6139-4F51-A748-101010CC8B02.data"}},"id":"201C5B55-E759-492F-8F20-A529C7C02C8B","root":true}
id = 05A8D61C-6139-4F51-A748-101010CC8B02
data = {"__CLASS__":"Person","data":{"name":"Marge Simpson","spouse":{"$ref":"201C5B55-E759-492F-8F20-A529C7C02C8B.data"}},"id":"05A8D61C-6139-4F51-A748-101010CC8B02","root":true}
"spouse"フィールドがJSONオブジェクトということに気づくでしょう。
そして、その内部の$refフィールドには、対象のオブジェクトのUUIDがあります。
データがロードされると、KiokuDBはロードさえていないオブジェクトへのリファレンスを
キューに入れて、オブジェクトグラフをメモリに常駐させるために、それらをロードします。
データがこのような方法で表現されている理由について知りたければ、
このフォーマットは、"JPSON"か
JavaScript Persistent Object
notation(<
http://www.jpson.org>)と呼ばれています。
KiokuDB::Backend::Storableを使うと、KiokuDB::EntryとKiokuDB::Referenceオブジェクトは、
代わりに、storableフックでシリアライズされます。
(オブジェクトセット)
より複雑なリレーションシップ(1対1に限らない)は、Set::Objectでふつう簡単にモデル化できます。
"Person"クラスを拡張してそのようなリレーションシップを足してみましょう:
package Person;
has children => (
does => "KiokuDB::Set",
is => "rw",
);
KiokuDB::Setオブジェクトは、Set::ObjectのKiokuDB用のラッパーです。
my @kids = map { Person->new( name => $_ ) } qw(maggie lisa bart);
use KiokuDB::Util qw(set);
my $set = set(@kids);
$homer->children($set);
$dir->store($homer);
"set"という便利な関数は新しいKiokuDB::Set::Transientオブジェクトを作ります。
一時的なセットはメモリスペースに存在するものです
(データベースからロードされたセットとは反対に)。
"weak_set"という便利な関数もあります。
循環構造(例えば、今の例に"parent"属性を追加する)を避けるために内部で使われている、
Set::Object::Weakで一時的なセットを作ります。
このオブジェクトは普通のSet::Objectとほとんど同じように振る舞います。
my @kids = $dir->lookup($homer_id)->children->members;
主な違いは、セットがデータベースから来るのがデフォルトで遅延されていることです。
@kidsにあるオブジェクトは、実際に必要になるときまでロードされません。
このことにより、ユーザーのオブジェクトのカプセル化を壊すこと無しに、
部分的にロードされるので、データベースに巨大なオブジェクトグラフがあっても問題になりません。
この振る舞いはKiokuDB::Set::DefferedとKiokuDB::Set::Loadedで実装されています。
このセットオブジェクトは、遅延ロードの操作に最適化されています。
例えば、2つの遅延セットを横断するなら、横断するセットのみがロードされる必要があります。
KiokuDBにオブジェクトが保存される際に、KiokuDB::Collapserを通過します。
エントリーがバックエンドにインサートされる前に、KiokuDB::Entryに、
"平たく"されたオブジェクトを入れます。
collapserには、KiokuDB::TypeMapオブジェクトを使います。このオブジェクトは、
それぞれのタイプのオブジェクトがどのように破壊するかを教えます。
オブジェクトを取ってくる間、オブジェクトを再インフレートして、
ワーキングオブジェクトにするのに、同じtypemapが使われます。
typemapにないオブジェクトを保存しようとするとエラーになります。その理由は
すべてのタイプのオブジェクトを保存できるか分からないからです。(例えば、
"DBI"はソケット、オブジェクト。XSベースのモジュールは数値のような内部的な
ポインタを持ちます。そのアドレスは次回のロード時には正しくなくなっています)。
大半のオブジェクトは安全にシリアライズできるにもかかわらず、
わずかな報告されないもろさが、大きなデバッグの難しい問題を作るのはありがちなことです。
このルールの例外は、Mooseベースのオブジェクトです。Mooseの強大な
リフレクションサポートを通して、十分なメタ情報が利用できるので、
安全にシリアライズ出来ます。
加えて、標準のバックエンドは共通のオブジェクト(DateTime,
Path::Classなど>)用に
デフォルトのtypemapを提供しています。KiokuDBにどんなカスタムのtypemapが渡されても、
デフォルトとマージされます。
それで、実際にKiokuDBにClass::Accessorベースのオブジェクトのようなものを保存させるには、
次のようにします:
KiokuDB->new(
backend => $backend,
allow_classes => [qw(My::Object)],
);
これは次の省略形です:
my $dir = KiokuDB->new(
backend => $backend,
typemap => KiokuDB::TypeMap->new(
entries => {
"My::Object" => KiokuDB::TypeMap::Entry::Naive->new,
},
),
);
KiokuDB::TypeMap::Entry::Naiveは単純に再帰的にたどることで、
オブジェクトのナイーブな破壊を行います。
collapser
は、オブジェクトを見つけると、KiokuDB::TypeMap::Resolverに、
オブジェクトのクラスに応じた、破壊ルーチンを尋ねます。
この検索は、典型的には、"ref
$object"で行われ、継承を使いません。
スーパークラスで安全に使われているtypemapエントリーは、
必ずしもサブクラスで安全に使えるとは限らないからです。
継承されたエントリーに
したいなら、"isa_entries"を指定してください。
KiokuDB::TypeMap->new(
isa_entries => {
"My::Object" => KiokuDB::TypeMap::Entry::Naive->new,
},
);
オブジェクトに通常の("ref"
keyed)エントリーが見つからなければ、
isaエントリーがオブジェクトスーパークラスのために探されます。
サブクラスエントリーはスーパークラスエントリーより前に試されます。
この検索の結果はキャッシュされるので、クラスごとに一回しか起こりません。
カスタムのシリアライズのフックが欲しければ、自分のオブジェクトを破壊するための
フックを指定できます。
KiokuDB::TypeMap::Entry::Callback->new(
collapse => sub {
my $object = shift;
...
return @some_args;
},
expand => sub {
my ( $class, @some_args ) = @_;
...
return $object;
},
);
これらのフックはオブジェクトを破壊するときに、メソッドとして呼ばれます。
例えば、typemapのISAに関連するPath::Classは:
'Path::Class::Entity' => KiokuDB::TypeMap::Entry::Callback->new(
intrinsic => 1,
collapse => "stringify",
expand => "new",
);
"intrinsic"フラグは次のセクションで述べます。
typemapエントリのもう一つの選択はKiokuDB::Typemap::Entry::Passthroughです。
バックエンドのシリアライズがネイティブにデータタイプを扱うことができると分かっていれば、
これは適切です。
例えば、オブジェクトに適切なStorableフックがあり(破壊する必要のあるサブオブジェクトを含まない)、
バックエンドには、KiokuDB::Backend::Serialize::Storableを使う場合です。
DateTimeはそのようにstorableが望むクラスの例です:
'DateTime' => KiokuDB::Backend::Entry::Passthrough->new( intrinsic => 1 )
KiokuDBでは、すべてのオブジェクトに、通常、IDが割り当てられます。
オブジェクトが複数のオブジェクトに共有されている場合、このリレーションは維持されます。
しかし、いくつかのオブジェクトは望ましい振る舞いをしません。
それらは、DateTimeや、Path::Classエントリ、URIオブジェクトのようなもので、
値を表現します。
KiokuDBは
intrinsiclyに、そのようなオブジェクトを、
そのオブジェクトにそれ自身のIDと新しいKiokuDB::Entryを作る代わりに、
破壊するよう要求できます。オブジェクトが直接破壊できれば、親の構造の中に入ります。
破壊され、共有されたリファレンスは、もともと2つの区別されたコピーとして
データーベースからロードされます。ですので、一つをアップデートしても、
もう一方には影響がありません。
例えば、下記のようなコードを動かしたとして:
use Path::Class;
my $path = file(qw(path to foo));
$obj_1->file($path);
$obj_2->file($path);
$dir->store( $obj_1, $obj_2 );
データがインサートされるときには、下記は真ですが、
$obj_1と$obj_2がデーターベースからロードされると、もはや真ではありません:
refaddr($obj_1->file) == refaddr($obj_2->file)
$obj_1と$obj_2の両方が$pathのコピーだからです。
この現象は、通常、変異されず、複製されたり置き換えられたりするオブジェクトに適しています。
そのようなオブジェクトのためには、最初のクラスエントリが独自のIDでバックエンドに作られるのは、
望まれていないからです。
それぞれのバックエンドには、デフォルトのtypemapがついています。
それには、共通のCPANモジュールオブジェクトのために、いくつか共通のビルトインのエントリもあります。
KiokuDB::TypeMap::Defaultにより詳細があります。
(単純な検索)
ほとんどのバックエンドが効率的ではないものの、便利な単純な検索があります。
これは、エントリをスキャンして、フィールドにマッチさせます。
このAPIを使いたいなら、KiokuDB::Backend::DBIを使うことをおすすめします。
単純亜検索はSQLのwhere節を使って実装でき、より効率的だからです。
(ただし、手でカラムをセットアップしないといけませんが)
"search"メソッドに引数としてハッシュリファレンスのみを渡して呼びます。
単純な検索機能が呼び出され、Data::Stream::Bulkが結果と一緒に戻ってきます:
my $stream = $dir->search({ name => "Homer Simpson" });
while ( my $block = $stream->next ) {
foreach my $object ( @$block ) {
# $object->name eq "Homer Simpson"
}
}
正確なAPIはまだ決められていません。将来的に、DBIx::Class
0.09のシンタックスと
互換にするつもりです。
この簡単な検索APIを使うには、DBIバックエンドにカラムを設定しなければいけません。
検索するために、'name'カラムを作りましょう:
my $dir = KiokuDB->connect(
"dbi:SQLite:dbname=foo",
columns => [
# specify extra columns for the 'entries' table
# in the same format you pass to DBIC's add_columns
name => {
data_type => "varchar",
is_nullable => 1, # probably important
},
],
);
スキーマを手で変更することもできますし、また、データをバックアップするのに、"kioku
dump"を使い、
データベースを削除し、"create
=> 1"で接続し、"kioku
load"を使うことも出来ます。
このカラムを埋め込むために、Homerをロードして、更新する必要があります:
{
my $s = $dir->new_scope;
$dir->update( $dir->lookup( $homer_id ) );
}
データベースでは次のようになります:
id = 201C5B55-E759-492F-8F20-A529C7C02C8B
name = Homer Simpson
(BDBを始めよう)
KiokuDBでもっとも成熟したバックエンドは、KiokuDB::Backend::DBDです(訳注:DBIのほうが安定しているとYAPC::Asia
2009で聞きました)。
十分に動きますし、多くの機能をサポートします。
オブジェクトのインデックスのカスタマイズやトランザクションを提供する
Search::GINのようなインテグレーションもあります。
KiokuDB::Backend::DBIはより新しいですが、そこまでテストされていません。
ですが、トランザクションもサポートしますし、クエリベースのSearch::GINもあります。
これも、なかなかよく動きます。ですが、KiokuDB::Backend::BDBと同じくらい速くはありません
(訳注:YAPC::Asia
2009では、ほぼ変わらないと聞きました)
KiokuDB::Backend::BDBは、BerkeleyDBモジュールが必要です。
また、最近のバージョンのBerkeley
DB自身も必要です。Berkeley
DBは、以下のURLにあります。
<
http://www.oracle.com/technology/software/products/berkeley-db/db/index.html>.
BerkeleyDB(ライブラリ)は通常、"/usr/local/BerkeleyDB.4.7"にインストールされます。
ですが、BerkeleyDB(モジュール)は、"/usr/local/BerkeleyDB"を見ようとします。
ですので、シンボリックリンクを作っておけば、インストールが簡単になります。
BerkeleyDBがインストールできれば、KiokuDB::Backend::BDBは問題なくインストールできるはずです。
KiokuDBと一緒に使うことができます。
BDBバックエンドを使うために、ストレージを作らなければいけません。
このために、"create"フラグを渡さなければいけません。
my $backend = KiokuDB::Backend::BDB->new(
manager => {
home => Path::Class::Dir->new(qw(path to storage)),
create => 1,
},
);
BDBバックエンドは、BerkeleyDB::Managerを使って、たくさんのBerkeleyDBの下働きを行います。
BerkeleyDB::Managerオブジェクトは"manager"属性で提供される引数を使って、インスタンス化されます。
これで、ストレージがつくられました。このバックエンドを、以前と同様に使います。
my $dir = KiokuDB->new( backend => $backend );
その後のオープンには、"create"属性が真である必要はありませんが、真であっても特に害はありません。
この"connect"は上記のものと同じです:
my $dir = KiokuDB->connect( "bdb:dir=path/to/storage", create => 1 );
(トランザクション)
いくつかのバックエンド(KiokuDB::Backend::Role::TXNロールをするもの)は、トランザクションが使えるものがあります。
DBIx::Classに慣れているなら、すぐわかるでしょう:
$dir->txn_do(sub {
$dir->store($obj);
});
BerkeleyDBレベルのトランザクションを作ります。データベースへのすべての変更は
ブロックが綺麗に実行されたら、コミットされます。
何らかのエラーが起きれば、トランザクションはロールバックされます。
変更は次の読み込みでは、見えません。
KiokuDB生きているインスタンスには触れません。ですので、次のようにすると
$dir->txn_do(sub {
my $scope = $dir->new_scope;
$obj->name("Dancing Hippy");
$dir->store($obj);
die "an error";
});
"name"属性はロールバック
されません。"store"オペレーションだけが、元に戻ります。
トランザクションは適切にネストできます。また、ほとんどのバックエンドで、一般的に
書き込みのパフォーマンスが良くなります。
(クエリ)
KiokuDB::Backend::BDB::GINはKiokuDB::Backend::BDBのサブクラスで、
Serach::GINインテグレーションを提供しています。
Search::GINはインデックスとクエリーオブジェクトのフレームワークです。
Postgresの内部GIN
apiにインスパイアされました。
GINは、Generalized Inverted
Indexes(訳注:汎用転置索引)の略です。
Search::GINを使うと、任意の検索キーをオブジェクトにタイしてインデックスできます。
そして、それらのオブジェクトをクエリで検索できます。
例えば、Search::GINがサポートする、すぐに使える、予めある検索の一つに、クラスインデックスがあります。
Search::GIN::Extract::Callback
を使って、オブジェクトにカスタムのインデックスを作りましょう:
my $dir = KiokuDB->new(
backend => KiokuDB::Backend::BDB::GIN->new(
extract => Search::GIN::Extract::Callback->new(
extract => sub {
my ( $obj, $extractor, @args ) = @_;
if ( $obj->isa("Person") ) {
return {
type => "user",
name => $obj->name,
};
}
return;
},
),
),
);
$dir->store( @random_objects );
オブジェクトを検索するために、マニュアルキー検索クエリを使います:
my $query = Search::GIN::Query::Manual->new(
values => {
type => "person",
},
);
my $stream = $dir->search($query);
結果として、検索結果を表すData::Stream::Bulkオブジェクトが返ります。
次のようにイテレートできます。
while ( my $block = $stream->next ) {
foreach my $person ( @$block ) {
print "found a person: ", $person->name;
}
}
また、より単純に、メモリに全結果をロードしてもかまわないなら:
my @people = $stream->all;
Search::GINはまだ未成熟です。ドキュメントも書いているところです。
ですが、このような単純な検索は動きますし、Search::GIN::Extract::Classのような
予めある解決を含んでいます。
つまり、現在は動きますが、新しく開発をするときには、これに注意してください。
翻訳者:加藤敦 (
[email protected])
Perlドキュメント日本語訳
Project にて、
Perlモジュール、ドキュメントの翻訳を行っております。
http://perldocjp.sourceforge.jp/
http://sourceforge.jp/projects/perldocjp/
http://www.freeml.com/ctrl/html/MLInfoForm/[email protected]
http://www.perldoc.jp/