tsucchi’s diary(元はてなダイアリー)

はてなダイアリー(d.hatena.ne.jp/tsucchi1022)から移行したものです

github pages で blog を立ち上げました

ちょっと前に、github pages をつかって blog を立ち上げました。
アドレスは http://localhost:4000/おっと、間違えた http://tsucchi.github.com/ です。

jekyll 使ってます。そのへんの顛末もそっちに書いてあります。ここと、新しい方と、どちらを本館とするかはかなり迷っているのですが、書きやすいのは emacs で書けるほうなので、メインが変わるかもしれないなー、と思っています。

ShipIt::Step::Copy というものを書いてみた

表記のとおりです。「既に誰かが作ってそうだなー」とか、「むしろコアの機能でできちゃったりしそうだな」とか思ったのですが、ざっと調べた限りはどちらもなさそうなので、試しに書いてみたら意外と簡単にできた。

.shipit に Copy というステップが書けるようになるプラグインです。copy.dir に書いたディレクトリにモジュールのディストリビューションがコピーされます。(なので、Copy は MakeDist の後に書いてね)

steps = ..., Copy
copy.dir = ~/Dropbox/Public/My-Module

何がしたいかというと、この例にもあるみたいに、Dropbox のフォルダにディストリビューションを置けたら便利かなー、と思ったのです。

CPAN に上げてるやつは普通に CPAN に上げちゃえばいいと思うのですが、上げる気がないやつ*1なんかは、そのまま github とかに置いてもいいのですが、github に置いてる状態だと*2、AuthorTest で使うモジュールとか入ってないとインストールできないとかあって、そういうの面倒くさいなー、と。

なので、最近はそういう微妙なやつは Dropbox に置いてたのですが、手動で置くのもめんどいので、ShipIt のプラグインを書いてみた次第です。よろしければお使いください。

https://github.com/tsucchi/p5-ShipIt-Step-Copy
https://www.dropbox.com/sh/2a8u7yq1w41z4fv/RB_2lwQ008/ShipIt-Step-Copy

*1:将来的には上げたいけど、まだそのクォリティーに達してないなー、と思うやつとか

*2:何を git で管理するかによって多少差異はありますが

SQL::Executor 0.11 をリリースしました

昨日放流したので、もうCPAN の各ミラーにアップされているでしょう。

たいした変更はしてないのですが、(2つの変更のうちの1つは)互換性の無い変更なので書いときます。

1. 名前付きプレースホルダで、プログラム側が渡してなくて、SQL 側がパラメータ定義してる場合、エラーになっていたのを、デフォルトではエラーではなくしました

ちょっと分かりにくいので、コードで説明すると、たとえば

my $ex = SQL::Executor->new($dbh);
$ex->select_named('SELECT * FROM SOME_TABLE WHERE value = :value', {});

みたいな呼び方は従来はエラーとしていましたが、これは正当なものとしました。従来どおりエラーとしたい場合は、

my $ex = SQL::Executor->new($dbh, { check_empty_bind => 1 });

とコンストラクタで check_empty_bind に 1(TRUE) を設定してください。(デフォルト 0(FALSE)です)

何がしたいかというと、

$ex->select_named('SELECT * FROM SOME_TABLE WHERE IF( :value IS NULL, 1, value = :value )', $params_href);

みたいな感じで、「パラメータを省略したら検索条件から外す」みたいな処理が結構あるので、そういうときに便利かなー、と思ってのことです。*1

2. select_id はデフォルトでは発行しないようにしました

今まで、select 文でコールバックを指定していた場合、当該 select 文毎に一意となる id(select_id)を発行していたのですが、これを止めました。必要な場合、SQL::Executor を継承して、select_id メソッドをオーバーライドしてください。これにより互換性がなくなったのですが、多分僕以外使ってないと思うので、問題無いんじゃないかなー、と思っています*2

もし欲しい人がいれば、コンストラクタで select_id メソッドを置き換えるコールバックを受けるとか、そんな感じのことを考えてるので、@tsucchi OR CPAN RT OR github issue プリーズ。

何がしたかったというと、Data::UUID への依存を切りたかっただけです。おかげでインストールしやすくなったのではないかなー、と思います。

ってか、ここまでやってから CPAN に上げればよかったわー、と思えるバージョンです。多分もう大きな変更はしないので、安心して使ってもらえばよいのではないかなー、と思います。

*1:ちなみに、デフォルトの挙動をかえず、デフォルトはエラーにする選択肢も考えたのですが、最初に書いた記事ふくめ、本モジュールに対する反響は僕の知るかぎりなかったので、今回は「こっちのほうが便利だよねー」と思える方にデフォルトを振りました。なので紹介記事に対する反響ってのは重要なんです。これは自戒も込めてだけど。

*2:そもそもこのモジュール使ってる人がいるかどうかも怪しいけど。。。

SQL::Executor を cpan にアップしました

前にだいたい書きましたが、SQL::Executor というモジュールを書いてます。

簡単な SQLSQL::Maker を使って SQL を生成して投げて、面倒くさいやつは生のSQLと名前付きプレースホルダを使って SQL を投げれる物体です。

use DBI;
use SQL::Executor;
my $dbh = DBI->connect($dsn, $id, $pass);
my $ex = SQL::Executor->new($dbh);
my @rows = $ex->select('SOME_TABLE', { id => 123 });
$ex->insert('SOME_TABLE', { id => 124, value => 'xxxx'} );
$ex->update('SOME_TABLE', { value => 'yyyy'}, { id => 124 } );
$ex->delete('SOME_TABLE', { id => 124 } );

# 名前付きプレースホルダ(select)
my @rows = $ex->select_named('SELECT id, value FROM SOME_TABLE WHERE id = :id', { id => 123 });
# 名前付きプレースホルダ(更新系)
$ex->execute_query_named('INSERT INTO SOME_TABLE SET id = :id, value = :value 
                            ON DUPLICATE KEY UPDATE value = :value', { id => 123, value => 'value dayo' });
# iterator
my $itr = $select_itr('SOME_TABLE', { value => 'aaa' });
while ( my $row = $itr->next() ) {
   ... #do something with row
}

ほかにもいっぱいメソッドあるのですが、あとのメソッドは使わなくても多分大丈夫じゃないかなー、と思います。*1

あと、select 時にコールバックを指定できるのですが、これは自作の ORM のエンジンとして使うためなのですが、CSV 出力とかしたい場合なんかも便利かもしれません。(やったこと無いので、やってみたらエントリ書くかも)

個人的には結構長く*2使っているので、まあまあ安定してるんじゃないかなー、と思ってます。よろしければ使って見てください。変なところとかあれば、CPAN の RT でも github の issue でも、twitter の @tsucchi でも何でもよいので連絡いただければ、と思います。

*1:僕個人的には select_by_sql とか、execute_query とかの名前付きなしのやつをたまに使うかも、くらいです

*2:連休中に書き始めたので、2ヶ月弱くらい

Class::Load を使う(try_load_class と load_optional_class)

Class::Load、動的にモジュールを読み込んでくれて、しかも require より柔軟で、便利です。

で、Class::Load::try_load_class を使っていたのですが、ロード先がコンパイルエラーとかになっている場合に、0 が返ってきてしまうんですね。die してほしいなー、と思ったのです。

try_load_class は、配列コンテキストだと、2番目の値にエラーメッセージを返すので、コレ見て何とかしよう、と思って、試しにこんなの書いた。

#!perl
use strict;
use warnings;
use Class::Load qw(try_load_class);

{
    my ($status, $message) = try_load_class('Net::FTP');#ある
    warn $status, $message;# $status => 1, $message => undef
}
{
    my ($status, $message) = try_load_class('Net::FCP');#そんなのない
    warn $status, $message;# $status => 0, $message => 'Can't locate Net/FCP.pm in @INC (@INC contains: ...'
}
{
    my ($status, $message) = try_load_class('a');#コンパイルエラー a.pm => "package a;0;#コンパイルエラー";
    warn $status, $message;# $status => 0, $message => 'a.pm did not return a true value at ...'
}

で、「Can't locate ...」ってオフィシャルのドキュメントにあるかなー?とか思って、perldiag みて見たりとかしてたんだけど、ふと我に返って Class::Load のドキュメント見てたら、

load_optional_class Class::Name, \%options -> 0|1
load_optional_class is a lot like try_load_class, but also a lot like load_class.
(中略)
If the class exists on disk, but loading from disk results in an error ( i.e.: a syntax error ), then it will croak with that error.


あ゛、あったw

#!perl
use strict;
use warnings;
use Class::Load qw(load_optional_class);

{
    my $status = load_optional_class('Net::FTP');#ある
    warn $status; # $status => 1
}
{
    my $status = load_optional_class('Net::FCP');#そんなのない
    warn $status; # $status => 0
}
{
    my $status = load_optional_class('a');#コンパイルエラー a.pm => "package a;0;#コンパイルエラー";
    warn $status; #ここに来れずに手前でしぬ。'a.pm did not return a true value at ...'
}

うん、意図どおりの挙動です。名前に騙されて、ちゃんとドキュメント読んでなかったわー orz

モジュールのドキュメントはちゃんと読まないとダメですねー。

Kappa という ORM を書いてみた話

このへんの続きみたいな話。

Kappaという ORM を書いています。欲しい機能は大体できて、まあドキュメントがアレなのですが、とりあえず自分で使う分には困らないし、頑張れば使いたい人も使えるかなー、くらいまではきたのでここに書いた次第です。

インストール

github から、SQL::ExecutorKappaを入れてください。SQL::Executor はもしかしたら CPAN に上げるかも。

使い方

コンストラクタで、dbh を渡します。あと、オプションで Row オブジェクトとテーブルクラス*1の namespace を渡せます。

use Kappa;
use DBI;
my $dbh = DBI->connect($dsn, $id, $pw);
my $db = Kappa->new($dbh, {
   row_namespace   => 'MyProj::DB::Row',
   table_namespace => 'MyProj::DB::Table',
});
my $row_obj = $db->select('SOME_TABLE', { id => 123 });
print $row_obj->id, $row_obj->value;

どちらのオプションも必須ではなくて、デフォルトでは Row オブジェクトは Kappa::Row, テーブルクラスは Kappa 自身を使うようになっています。Row オブジェクトもテーブルクラスも、テーブル毎に定義されたものがあればそれ、なければ、namespace 自身(この例では MyProj::DB::Row/Table)、それもなければデフォルト(Kappa::Row/Kappa)が返ります。

Row オブジェクト

select 系を叩いたときだけ返ります。select_row 系だと Row オブジェクトそのもの、select_all 系だと Row オブジェクトの配列、select_itr 系だと iterator 経由で、iterator#next を呼ぶと Row オブジェクトが返ります。

row_namespace を指定すると、テーブル単位で Row オブジェクトを定義できます。Row オブジェクトは基本的にカラムの情報を組み合わせてアレコレすること以上のことはできませんが。*2

あと、

my $db = Kappa->new($dbh, {
   row_namespace   => 'MyProj::DB::Row',
});
$db->row_object_enable(0);
my $row_href = $db->select('SOME_TABLE', { id => 123 });

みたいに、row_object_enable というメソッドを呼ぶと、Row オブジェクトの代わりに hashref (all系なら hashref の配列)が返るようになります。また、この row_object_enable は Guard オブジェクトを戻り値に返せるので、戻り値を受けると、スコープを抜けた際に、元の設定に戻します。

my $db = Kappa->new($dbh, {
   row_namespace   => 'MyProj::DB::Row',
});
{
    my $guard = $db->row_object_enable(0);
    my $row_href = $db->select('SOME_TABLE', { id => 123 });
    # ここまでは row_object_enable(0)が有効
}
# ここからは元通り Row オブジェクトが返る

あと、inflate したい人は、Row オブジェクトに inflate rule を書くことになると思います。

テーブルクラス

テーブル毎のロジックを置く場所です、ファクトリメソッドにテーブル名を指定して呼びます。

my $db = Kappa->new($dbh, {
   table_namespace => 'MyProj::DB::Table',
});
my $db_for_sometable = $db->create('SOME_TABLE');

なんでこんなものを用意したかというと、テーブルごとに面倒くさい SQL とか、マスタデータのコード/名称変換とか置けたら便利で見通しが良いかなー、と思ってのことです。deflate したい人はテーブルクラス毎に deflate rule を置くことになると思います。*3

やりたかったこと

最近の日記につらつら書いてましたが、大体こんな感じのことです

  • 簡単な SQL は簡単に実行したい
  • 面倒くさい SQL は手書き、その際に名前付きプレースホルダを使いたい
  • Row (だけ)ではなく、テーブルごとにある程度ロジックを置きたいな
  • スキーマクラスなしで動かしたい
ORM なんて世の中にいっぱいあるのに、どうして車輪の再発明をしたのか

ほしいものが無かったから、というのが超簡単な答え。

前にもちょっと書いたように、Teng を少し使っていて、上記の上半分は快適に要件満たせていたのだけど、下半分、とに「スキーマクラスなし」で動かせるほうがいいなー、と思いました。*4

で、DBIx::Simple だと要件だいたい満たせそうなのだけど、もう Teng 向けにかいちゃったコードが結構あるし、なんか DBIx::Simple のインターフェース(とくに ResultSet まわり)が見た感じ好きじゃないなー、と思ったし。。。みたいな感じです。

要は、DBIx::Simple みたいに使えて、Teng みたいなインターフェースを持ってる物体が欲しかったんですね。

インターフェースは変えない?

今のところ、変えるつもりはないのだけど、変えるかもしれません。「使ってます」って声があったら変更は慎重になると思います。あるいは、今のところ CPAN にあげる気は無いのだけど、気が変わって CPAN に上げたら、互換性を損なうような変更はしない(する場合はドキュメントに明記する)と思います。

なぜ Kappa?

DBIx::Lite とか、そんな感じだと良くわかんないし、ありそうだし(つーか今しらべたらあったw)。じゃあ SuperLight?Easy?なんかそういうの嫌だよね。

で、某アニメ*5で、「Kappa -> Parasite(パラサイトなのにパラシテと読まれる)->Tengu*6」という「絵しりとり」のネタをふと思い出して、「これだ!、ORM の名前は Kappa で、今 Teng 向けに書いちゃった部分の移植を容易にする為のブリッジをパラシテにしよう!」*7と思って決めた次第。ここ数週間は、超忙しかったのでどうやら少し頭がおかしかったようです。

まあそんな感じで、特に意味は無いんだけど、ウチの地域のマスコットが河童らしいし、ちょうどいいんじゃないですかね。CPAN のネームスペースはギリギリ空いてるっぽいし。

*1:多分これ私の造語

*2:この辺あんまりドキュメントに書いてないので、書かないと。。。

*3:僕自身はあんまり inflate/deflate いらないことが分かったし、それよりはスキーマクラスなしで動くことの方が重要だったため、deflate ルールを置く場所がここしかありません

*4:テーブル数が結構多いので、スキーマクラスの動的生成はそれなりに時間がかかって、特にテストコードが遅くなるのが嫌だった

*5:僕はアニメ見ないんだけど、コレは NHK でやってたので見てた。つーか娘が見てた

*6:ちなみになぜかTENGOと表記される

*7:パラシテは公開してません

ORM とかテーブルデータゲートウェイとか、その周辺の話

このへんとかこのへんの続きみたいな話。。。だと思う。

ORM がほしい

  • 簡単な SQL を簡単に Perl から投げれるといいな
  • 難しい SQL(とくに JOIN いっぱいするような SELECT)は、クエリビルダーで頑張るのではなく、SQL を書いて実行したい
  • その際に名前付きプレースホルダが使いたいなー
  • INSERT とか UPDATE は hashref が渡せればいい
  • スキーマクラスが無くてもうごくのがいいな

テーブルデーゲートウェイがほしい

  • テーブルごとにある程度ロジックを置きたいな
    • 1枚のスキーマクラスを持つのではなく、(必要に応じて)テーブルごとに「テーブルクラス*1」をおくような感じ
    • 親子関係のテーブルの INSERT とか一発でできるとうれしいな*2
    • めんどくさいけど良く使う SELECT をメソッドとして置けるとうれしいな
  • 今はできない*3けど、将来は SQL もそこに置いておくと管理しやすくていいだろうな

テーブルデーゲートウェイについて

  • PoEAA みると、テーブルクラスがあって、ベースクラスを継承してるつくりになっている。テーブル単位のユーティリティーがそこに置かれるイメージ。ただ、Perl だと引数によるオーバーロードができないのであんまりメリットないかもなー、とも思う。たとえば insert をオーバーロードできない(オーバーライドするわけにいかない)ので、insert_by_obj 作るとか、そんな感じ。ちょっとダサい。
  • Row オブジェクトは本来不要だが、世の実装には大抵ついてるようだ。そのほうが便利だからであろう。
  • PHP の Zend_Db とか(DBIx::Simple もかな?)テーブルデーゲートウェイの実装と呼ばれてるものは、なんか単に機能が少ないだけのような気がする。Teng とか Rails の Active Record とかだってテーブルデーゲートウェイとしての機能を持ってる。(行データゲートウェイも結局 Row オブジェクトを生成するための入り口としてテーブルデーゲートウェイ相当の機能が必要だから)。*4
  • 僕がほしいのは、そうではなくて、テーブルごとにカスタマイズしたメソッドを持つことのできる物体。普通はそういうものはスキーマクラスで実現するけど、スキーマクラスがなくても動くようにしたい。
  • Row オブジェクトはプアでもよくて、つーか hashref とかでもぜんぜんいい。
  • inflate/deflate はできなくても良くて、どうしてもやりたかったら inflate は Row オブジェクトのメソッドとして、deflate はテーブルクラスのメソッドとして Row オブジェクトを受けるメソッドを提供すれば良いと思う。

その他雑談

  • SELECT して結果が返らなかった場合は undef を返すか、NULL オブジェクトを返すかどっちでもいい。使う側としては、NULL オブジェクトを返す方が実装が楽で、undef を返す方がエラーを意識して注意深く書けるので一長一短あり、好きな方を選べるといいけど、アプリ内では統一しといたほうがいいだろう。
  • トランザクション管理とか、コネクションの管理って、ORM でやった方が便利には違いないけど、別にやらなくてもいい気がするのだけど、大抵やってるのは何故だろう?結構不思議。
  • 僕のところでは、テーブルの新規追加も変更も結構頻繁なので、スキーマクラス管理とかだるいし、かといって実行時に生成すると遅いし。mod_perl とかにキャッシュすれば実行速度は速いけど、テストが遅くなるのが嫌。スキーマクラスを使う ORM*5使ってる人たちは困って無いのかなー?不思議だ。

結論

結論なんて多分ない。だって今考えてること、悩んでることをつらつら書いただけだから。

*1:これ正式な用語かどうか分からん

*2:これは行データゲートウェイだと多分ちょっとめんどい

*3:mod_perl なので __DATA__ が使えない...

*4:なので、テーブルデーゲートウェイの正しい姿、みたいなのをまだ明確にイメージできてない。もうちょい修行が必要

*5:大抵のORMはそうだよね?