はじめに
O/Rマッパを勉強せず,いまだにDBIのオレオレラッピングなモジュールでデータベースをさわっているissmです.
現状では,DBIモジュールのfetchall_arrayrefやfetchall_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/;
$listをdumpしてみると次のようになります.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_hashrefとfetchall_hmvとの差分
fetchall_hashrefとfetchall_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; } - +
