自己参照の階層構造を持つテーブルをツリー形式にする DBIx::Tree

bayashi
2011-12-03

こんにちは!こんばんは!毎日フェッチしてますか!bayashi です!

DBIx 系を自分で書いたことがなく、果てしなくネタに困り、しょうがないから CPAN で DBIx 検索して見つけた、面白そうなモジュールを紹介します。もちろん、ニッチなのを攻めてみますよ!

その名も、DBIx::Tree。なんだこれ!

DBIx::Tree

このモジュールは、いわゆる Adjacency List Model のテーブルを、ツリー形式で見るためのモジュールです。

Adjacency List Model というのは、階層構造を持つデータです。RDBMSにおいては、通常、シンプルにフラットな複数のリスト同士を、関連するidでヒモ付けることで階層構造のようなものをつくることが多いと思いますが、Adjacency List Model においては、ひとつのテーブルのレコードそれぞれにユニークなIDと、同じテーブルのレコードにヒモ付く親IDを持ちます。

例えば、以下のようなテーブルを見てみましょう!

    food                food_id   parent_id
    ==================  =======   =========
    Food                001       NULL
    Beans and Nuts      002       001
    Beans               003       002
    Nuts                004       002
    Black Beans         005       003
    Pecans              006       004
    Kidney Beans        007       003
    Red Kidney Beans    008       007
    Black Kidney Beans  009       007
    Dairy               010       001
    Beverages           011       010
    Whole Milk          012       011
    Skim Milk           013       011
    Cheeses             014       010
    Cheddar             015       014
    Stilton             016       014
    Swiss               017       014
    Gouda               018       014
    Muenster            019       014
    Coffee Milk         020       011

最初のレコードの "Food" が、ルート要素(レコード)で、food_id がユニークキー、parent_id がレコードの親を指すIDになります。よくあるパターンなら、親IDは別テーブルに存在しますが、今回の場合は、この同じテーブル内のレコードを指しています。

とはいっても、こうしたデータを見慣れない僕にはビクンッビクンッときにくかったので、さっそく DBIx::Tree でごにょごにょしてみました!

Adjacency List Model の視覚化
#!/usr/bin/perl
use strict;
use warnings;
use DBIx::Tree;

my $result;

my $dbh = DBI->connect(
    "dbi:mysql:database=DBNAME;host=localhost",
    'USERNAME',
    'PASSWORD',
    +{ RaiseError => 1, },
) or die "Cannot connect to DB: $DBI::errstr";

my $tree = DBIx::Tree->new(
    connection => $dbh,
    table      => 'food',
    method     => sub { disp_tree(@_) },
    columns    => ['id', 'food', 'parent_id'],
    start_id   => '001',
);

$tree->traverse;

print "$result\n";

sub disp_tree {
    my (%param) = @_;

    my $item = $param{item};
    my $p_id = ${$param{parent_id}}[-1] ? ${$param{parent_id}}[-1] : 'ROOT';
    my $id   = $param{id};
    $result .= (' ' x ($param{level}*2) ). "$p_id: ". "$item($id)\n";
}

上記のコードを実行すると、以下のように結果が表示されます。

  ROOT: Food(001)
    001: Beans and Nuts(002)
      002: Beans(003)
        003: Black Beans(005)
        003: Kidney Beans(007)
          007: Black Kidney Beans(009)
          007: Red Kidney Beans(008)
      002: Nuts(004)
        004: Pecans(006)
    001: Dairy(010)
      010: Beverages(011)
        011: Coffee Milk(020)
        011: Skim Milk(013)
        011: Whole Milk(012)
      010: Cheeses(014)
        014: Cheddar(015)
        014: Gouda(018)
        014: Muenster(019)
        014: Stilton(016)
        014: Swiss(017)

なるほど、こういうことか!

モジュールの使い方は DBIx::Tree のドキュメントの通りで難しくないと思いますが、なかなかどうしてこうしたデータ構造は興味をそそりますよね。

例えば Food テーブルのデータを追加、更新、削除するときどうするか、検索するときどうするか。スタックするのが快感な、再帰脳にはたまらない分野なのではないでしょうか。

ただ、上記のようなテーブルは Adjacency List Model でしたが、例えば MySQL で扱うには Nested Set Model の方が都合が良く、DBIx::Tree::NestedSet というモジュールも存在します。さらには、各レコードに ツリー構造そのものを保持する Path Emuneration Model がもっとも都合が良いと言われています(上記コードの disp_tree 内で取得できる $param{parent_id} をカラムとして持っている状態かな)。

それぞれ興味がわいた人はModel名で検索してみると、詳細な解説記事がたくさん見つかるでしょう!

まとめ

3日目は DBIx::Tree を取り上げました。ある DBIx モジュールをきっかけに、新たなデータ構造について学ぶことができるなんて、とっても素敵なことですね!

明日は nihen さんです。お楽しみに!