はじめに

O/Rマッパを勉強せず,いまだにDBIのオレオレラッピングなモジュールでデータベースをさわっているissmです.

現状では,DBIモジュールのfetchall_arrayreffetchall_hashrefを(ラップしたものを)使ってデータを取得したりしています.で,その取得したデータを他で使いやすいように加工して云々...ということをしています.その「加工」が毎回メンドイんですね.(日々効率化を心がけているつもりではありますが><)

そんなことから今回,最近かじったHash::MultiValueを使ってアドホックな対策をしてみたので,以下,その記録です.

なお,DBIモジュールのバージョンは1.609,Hash::Multivalueのバージョンは0.07(0.08出てるけど)です.

現状

SQLなテキストをまとめたものをYAMLファイルとして外部化し(「SQLライブラリ」と呼んでます.参考:「モダンPerl入門」p.83),DBIモジュールをラップしたオレオレモジュール的なものを使って読み込み,データベースに投げています.

SQLライブラリ

SQLライブラリは次のような感じです.

# shop.yml - SQLライブラリ的なYAMLファイル
---
get__select: |
  SELECT shop_id id, shop_No
       , code, name, flg_suspend
    FROM %%shop  /* 「%%」は適当なテーブル接頭辞に置換 */
    WHERE flg_del = 0
    ORDER BY shop_No ASC
 
add__insert: |
  INSERT INTO %%shop ...
  ...

fetchall_arrayrefをラップ

先のSQLを読み込んでデータベースに投げ,間接的にfetchall_arrayrefメソッドを呼ぶことで,結果をリストリファレンスで取得しています.

my $list = $_dsh->q(     # $_dsh: DBIのラッパ的なもの
    'shop::get__select', # shop.ymlの「get__select」をロード
    [],                  # バインド値はなし
    [],                  # リストリファレンスで取得
);
 
print dump $list;  # use Data::Dump qw/dump/;

$listの内容は次のような感じになります.

[
  ["roisxgdpraisxCeb", 1, "test1", "\x{30C6}\x{30B9}\x{30C8}1", 0],
  ["rpisxlbrryisxqAs", 2, "test2", "\x{30C6}\x{30B9}\x{30C8}2", 0],
  ["rpisxDgqrsisxqas", 3, "test3", "\x{30C6}\x{30B9}\x{30C8}3", 0],
  ["shisxwrpssisxKwD", 4, "test4", "\x{30C6}\x{30B9}\x{30C8}4", 0],
  ["rxisxvDlrJisxAey", 5, "test5", "\x{30C6}\x{30B9}\x{30C8}5", 0],
]

fetchall_hashrefをラップ

間接的にfetchall_hashrefメソッドを呼ぶ場合は次のような感じです.

my $list = $_dsh->q(     # $_dsh: DBIのラッパ的なもの
    'shop::get__select', # shop.ymlの「get__select」をロード
    [],                  # バインド値はなし
    {},                  # ハッシュリファレンスで取得
    'id',                # キーを「id」にセット
);
 
print dump $list;  # use Data::Dump qw/dump/;

$listは次のような感じのものが得られます.

{
  roisxgdpraisxCeb => {
                        code => "test1",
                        flg_suspend => 0,
                        id => "roisxgdpraisxCeb",
                        name => "\x{30C6}\x{30B9}\x{30C8}1",
                        shop_No => 1,
                      },
  rpisxDgqrsisxqas => {
                        code => "test3",
                        flg_suspend => 0,
                        id => "rpisxDgqrsisxqas",
                        name => "\x{30C6}\x{30B9}\x{30C8}3",
                        shop_No => 3,
                      },
  rpisxlbrryisxqAs => {
                        code => "test2",
                        flg_suspend => 0,
                        id => "rpisxlbrryisxqAs",
                        name => "\x{30C6}\x{30B9}\x{30C8}2",
                        shop_No => 2,
                      },
  rxisxvDlrJisxAey => {
                        code => "test5",
                        flg_suspend => 0,
                        id => "rxisxvDlrJisxAey",
                        name => "\x{30C6}\x{30B9}\x{30C8}5",
                        shop_No => 5,
                      },
  shisxwrpssisxKwD => {
                        code => "test4",
                        flg_suspend => 0,
                        id => "shisxwrpssisxKwD",
                        name => "\x{30C6}\x{30B9}\x{30C8}4",
                        shop_No => 4,
                      },
}

加工がメンドイ

得られたデータをテンプレートでも利用しやすいように,もう少し加工してあげたいところです.個人的には,「ハッシュリファレンスのリスト(リファレンス)」がいちばん扱いやすいんでないかな,と考えてます.

前者(arrayref)の場合,SQLでの「ORDER」どおりの順序ではありますが,それぞれの要素において,どのカラムがどの値なのか,ということを,リストの「位置」で把握する必要があります.データベースの構成に変更があれば,それに伴って「位置」も把握し直さなければなりません.各要素をハッシュリファレンスに置き換えるには,何らかの方法で,「カラム名の集合」を準備し,対応づける処理が必要になります.

後者(hashref)の場合,各カラムをハッシュのキーとして参照することができますが,ハッシュの特性上,keysとかvaluesとかして得られるリストの順序が,SQLで指定したとおりになる保証がありません.sortすればOKではありますが,まぁ二度手間ですね.SQLで指定したORDERはなんだったの?

...とまぁ,そんなメンドイ加工を毎回しているわけです><

Hash::MultiValueをかじって

で,先日のエントリmiyagawaさんにご指摘をいただいてから,Hash::MultiValueをかじってみているわけですが,そこでのひとりごと:

Hash::MultiValueって,「順序のあるハッシュ」としても使えるのかな.
Twitter / IWATA, Susumu : Hash::MultiValueって,「順序のあるハ …

に対して,次のようなレスポンスをいただけました.

@issm つかえます>Hash::MultiValue
Twitter / Tatsuhiko Miyagawa: @issm つかえます>Hash::MultiValue

...そんなわけで,Hash::Valueのこの特徴を利用して,先の不満を解決できるか試してみました.

fetchall_hmv(仮)を追加してみた

DBIモジュールの内部にHash::MultiValueオブジェクトを返すためのfetchall_hmvというメソッドを新たに追加し(ソースコードは後ほど),先のfetchall_hashrefを呼び出す部分をこれに置き換えてみました.

my $list = $_dsh->q(     # $_dsh: DBIのラッパ的なもの
    'shop::get__select', # shop.ymlの「get__select」をロード
    [],                  # バインド値はなし
    {},                  # Hash::MultiValueオブジェクトとして取得
    'id',                # キーを「id」にセット
);
 
print dump $list;  # use Data::Dump qw/dump/;

$listdumpしてみると次のようになります.Hash::MultiValueオブジェクトですね.

bless({
  roisxgdpraisxCeb => {
                        code => "test1",
                        flg_suspend => 0,
                        id => "roisxgdpraisxCeb",
                        name => "\x{30C6}\x{30B9}\x{30C8}1",
                        shop_No => 1,
                      },
  rpisxDgqrsisxqas => {
                        code => "test3",
                        flg_suspend => 0,
                        id => "rpisxDgqrsisxqas",
                        name => "\x{30C6}\x{30B9}\x{30C8}3",
                        shop_No => 3,
                      },
  rpisxlbrryisxqAs => {
                        code => "test2",
                        flg_suspend => 0,
                        id => "rpisxlbrryisxqAs",
                        name => "\x{30C6}\x{30B9}\x{30C8}2",
                        shop_No => 2,
                      },
  rxisxvDlrJisxAey => {
                        code => "test5",
                        flg_suspend => 0,
                        id => "rxisxvDlrJisxAey",
                        name => "\x{30C6}\x{30B9}\x{30C8}5",
                        shop_No => 5,
                      },
  shisxwrpssisxKwD => {
                        code => "test4",
                        flg_suspend => 0,
                        id => "shisxwrpssisxKwD",
                        name => "\x{30C6}\x{30B9}\x{30C8}4",
                        shop_No => 4,
                      },
}, "Hash::MultiValue")

これだけでは,fetchall_hashrefとほとんど変わりませんが,そこで同オブジェクトのメソッドが活躍します.

例えば,valuesメソッド.

print dump [$list->values];  # use Data::Dump qw/dump/;

すると,次のようになります.

[
  {
    code => "test1",
    flg_suspend => 0,
    id => "roisxgdpraisxCeb",
    name => "\x{30C6}\x{30B9}\x{30C8}1",
    shop_No => 1,
  },
  {
    code => "test2",
    flg_suspend => 0,
    id => "rpisxlbrryisxqAs",
    name => "\x{30C6}\x{30B9}\x{30C8}2",
    shop_No => 2,
  },
  {
    code => "test3",
    flg_suspend => 0,
    id => "rpisxDgqrsisxqas",
    name => "\x{30C6}\x{30B9}\x{30C8}3",
    shop_No => 3,
  },
  {
    code => "test4",
    flg_suspend => 0,
    id => "shisxwrpssisxKwD",
    name => "\x{30C6}\x{30B9}\x{30C8}4",
    shop_No => 4,
  },
  {
    code => "test5",
    flg_suspend => 0,
    id => "rxisxvDlrJisxAey",
    name => "\x{30C6}\x{30B9}\x{30C8}5",
    shop_No => 5,
  },
]

SQLで指定したとおりの順序での「ハッシュリファレンスのリスト(リファレンス)」が簡単に得られました.

おわりに

以上,データベースから取得したデータを,有用なデータ構造へ手間をかけずに準備するために,DBIモジュールへ,結果データをHash::MultiValueオブジェクトとして返すfetchall_hmvメソッドを追加してみた,というお話でした.

勢いで書いたので,文体の乱れはスルーの方向でお願いしますw

コードが間に合わせだったり,fetchall_hashrefメソッドのパラメータ体系に完全互換でなかったり,パフォーマンスとか未計測だったりと,いろいろな点でアドホックなので,どなたか検証してみていただければ幸いです.

では,以降にコードの差分を載せておしまいとします.

オリジナルとの差分

オリジナルとの差分(diff -u)です.

--- DBI.pm.orig	2010-02-18 15:21:00.000000000 +0900
+++ DBI.pm.mod	2010-02-18 16:53:33.000000000 +0900
@@ -156,6 +156,7 @@
 use Carp();
 use DynaLoader ();
 use Exporter ();
+use Hash::MultiValue;
 
 BEGIN {
 @ISA = qw(Exporter DynaLoader);
@@ -465,6 +466,7 @@
 
 	fetchall_arrayref => { U =>[1,3, '[ $slice [, $max_rows]]'] },
 	fetchall_hashref  => { U =>[2,2,'$key_field'] },
+	fetchall_hmv      => { U =>[2,2,'$key_field'] },
 
 	blob_read  =>	{ U =>[4,5,'$field, $offset, $len [, ¥¥$buf [, $bufoffset]]'] },
 	blob_copy_to_file => { U =>[3,3,'$field, $filename_or_handleref'] },
@@ -2057,6 +2059,35 @@
         return $rows;
     }
 
+    sub fetchall_hmv {
+	my ($sth, $key_field) = @_;
+
+        my $hash_key_name = $sth->{FetchHashKeyName} || 'NAME';
+        my $names_hash = $sth->FETCH("${hash_key_name}_hash");
+        my @key_fields = (ref $key_field) ? @$key_field : ($key_field);
+        my @key_indexes;
+        my $num_of_fields = $sth->FETCH('NUM_OF_FIELDS');
+        foreach (@key_fields) {
+           my $index = $names_hash->{$_};  # perl index not column
+           $index = $_ - 1 if !defined $index && DBI::looks_like_number($_) && $_>=1 && $_ <= $num_of_fields;
+           return $sth->set_err($DBI::stderr, "Field '$_' does not exist (not one of @{[keys %$names_hash]})")
+                unless defined $index;
+           push @key_indexes, $index;
+        }
+        my $hmv = Hash::MultiValue->new;
+        my $rows = {};
+        my $NAME = $sth->FETCH($hash_key_name);
+        my @row = (undef) x $num_of_fields;
+        $sth->bind_columns(¥(@row));
+        while ($sth->fetch) {
+            my $ref = $rows;
+            $ref = $ref->{$row[$_]} ||= {} for @key_indexes;
+            @{$ref}{@$NAME} = @row;
+            $hmv->add($row[$key_indexes[0]], $ref);
+        }
+        return $hmv;
+    }
+
     *dump_results = ¥&DBI::dump_results;
 
     sub blob_copy_to_file {	# returns length or undef on error

fetchall_hashreffetchall_hmvとの差分

fetchall_hashreffetchall_hmvを直接差分をとってみたものが次です.(同じくdiff -u

--- fetchall_hashref 2010-02-18 16:59:14.000000000 +0900
+++ fetchall_hmv	 2010-02-18 16:59:52.000000000 +0900
@@ -1,4 +1,4 @@
-    sub fetchall_hashref {
+    sub fetchall_hmv {
     my ($sth, $key_field) = @_;
 
         my $hash_key_name = $sth->{FetchHashKeyName} || 'NAME';
@@ -13,6 +13,7 @@
                 unless defined $index;
            push @key_indexes, $index;
         }
+        my $hmv = Hash::MultiValue->new;
         my $rows = {};
         my $NAME = $sth->FETCH($hash_key_name);
         my @row = (undef) x $num_of_fields;
@@ -21,7 +22,8 @@
             my $ref = $rows;
             $ref = $ref->{$row[$_]} ||= {} for @key_indexes;
             @{$ref}{@$NAME} = @row;
+            $hmv->add($row[$key_indexes[0]], $ref);
         }
-        return $rows;
+        return $hmv;
     }
-  
+

こちらもあわせてどうぞ

コメントをどうぞ