日向夏特殊応援部隊

俺様向けメモ

Web テクノロジーセミナー in Hokkaido と Hokkaido.pm のスライドを公開します

ちょっと忙しいので感想とかは後で><

というわけで行って参りました北海道。前日頑張ってスライド書いては呑んでスライド書いては呑んでの繰り返しでホテルの朝食を二度とも逃すという体たらくぶりでしたw

Web テクノロジーセミナー in Hokkaido

一応補足的に言っておくと、設計指針辺りで前提として今回作っているのは Web Application ではなくて Web API であるという事で、可能な限り速くレスポンス返したいので余計なことはしたくないってのがあります。
という訳で WAF とか要らんよねーって言う話につながる訳です。一般的な Web Application を書く際はカスタムで書かなくても WAF から書いても良いし、好きにおれおれ WAF 作って書いても良いし、いずれにせよ WAF 使った方が多くの人にとって便利だとは思います。

DBIC みたいなのはもう覚えるコストも考えるときついなーと最近考えていたりします。

DBIx::Connector + SQL::Abstract 辺りでだいぶコードもすっきりしてるので余り不満が無いのが現状です。

後は Object::Container をヘビーに使い始めました。現在は逆にテストがちょっと難しくなる部分があるんだけども、これを使った事の良さは後からどんどん効いてくるだろうなーと思っています。

memcached の代わりに handlersocket 使う話だとか Gearman だとかの話は追々、実運用を通してフィードバックしていけたらなーと思っております。

Hokkaido.pm

DB 処理における Fixture テストの勧め

当日書いたのでgdgd感満載ですけど、こちら実演でやってまいりました。伝えたかった点としては、

  • SQL::SplitStatement が地味に便利
    • 外部化した複数の SQL 文を安全に一個ずつ実行とかで使える。mysqldump の結果も読めます。*1
  • DBIx::Connector の利用例
    • txn() の時に commit は書かなきゃいけないのが罠過ぎるけど他は便利
  • Test::Fixture::DBI の利用例
    • 今度改めてきちんと書きたいなと

って感じです。

所感とか

Web テクノロジーセミナーの方は来場者のターゲティングが難しく、ご来場頂いた方の半分くらいには随分と縁遠い話をしてしまったやもなーと思ってます。もうちょっとライトの話にすれば良かったですね><

とはいえその日のWeb テクノロジーセミナーの来場者の一部とか、翌日の Hokkaido.pm では東京と変わらず技術ネタを深く議論出来ました。実感として東京との技術的な格差みたいなものはあまり無いなーと思いました。
一方で身近で触れられる感覚が違うという点も手伝ってかアウトプットする事というのはまだまだみたいだなぁと思いました。一歩踏み出す勇気だとか、あるいはアウトプットした内容を伝えていく媒体とか必要かもしれません。

僕が、Planet Hokkaido.pm 作ってよーと Hokkaido.pm のリーダーである id:onagatani さんに言ったら、すぐに作りますとの事でしたので程なく出来るんじゃないかな?出来たらぜひ皆さんウォッチして下さいませ。

他にも地方ならではの問題やらを直に聞けたのは良かったなーと思いました。地方 pm にも出来る限り巡業に行きたいなーと思いました。lestrrat さんばかりに地方巡業をお任せしっぱなしだったので、今後は少しでもお手伝いしなきゃなーと思ってる次第であります。

最後に

ご来場頂いた皆様及び関係者の皆様ありがとうございました!そしてお疲れ様でした。

*1:mysql_multi_statements オプションを有効にしても出来ますが、SQL Injection の温床になるので個人的にはお勧めしません

生 DBI ユーザーのための DBI Cookbook (5)

まさかの続編ですよwww

HandleError を使ってより詳しいエラーを得る

今日、帰りに @myfinder さんと話していて、syslog-ng に吐かれるエラーで Too many connection とかをちゃんと監視しつつも、エラーメールボムによって大事な思い出が消えたりしないようにしたいねー的なことを話していて、その場合はエラーナンバーをきちんと記録するだの、エラーが起こった DB の host 名だとかで良しなにエラー通知間隔を制御したいよねと。
そういう際にやっぱり DB のホスト名だとか追加情報がエラー文字列に入ってると便利だろうなということでこんなソリューションはどうかと。

#!/usr/bin/perl

use strict;
use warnings;

use Test::More;
use Test::Exception;

use Carp;
use Data::Dump qw(dump);
use DBI;
use Try::Tiny;

sub create_dbh {
    my ( $dsn, $user, $credential, $attrs ) = @_;

    $attrs ||= +{
        RaiseError         => 1,
        PrintError         => 0,
        PrintWarn          => 0,
        ShowErrorStatement => 1,
        AutoCommit         => 0,
    };

    $attrs->{HandleError} = sub {
        my $e     = shift;
        my $lasth = $DBI::lasth;
        unless ( ref $lasth ) {
            croak $e;
        }
        elsif ( $lasth->isa('DBI::dr') ) {
            croak sprintf( '%s (errno: %d)', $DBI::errstr, $DBI::err );
        }
        else {
            my $dbh =
                $lasth->isa('DBI::db')
              ? $lasth
              : $lasth->{Database};
            my %dsn = map { split '=' => $_ } split( ';', $dbh->{Name} );
            my %err_report = (
                errno => $dbh->err,
                user  => $dbh->{Username},
            );
            for (qw/host db dbname/) {
                $err_report{$_} = $dsn{$_} if ( exists $dsn{$_} );
            }

            croak sprintf(
                '%s (%s)',
                $dbh->errstr,
                join( ", ",
                    map  { $_ . ": " . $err_report{$_} }
                    sort { $a cmp $b } keys %err_report )
            );
        }
    };
    DBI->connect( $dsn, $user, $credential, $attrs );
}

lives_ok {
    create_dbh( "dbi:mysql:dbname=test;host=localhost", "root", "" );
}
'database test is exists';

throws_ok {
    try {
        create_dbh( "dbi:mysql:dbname=hidek;host=localhost", "root", "" );
    }
    catch {
        note $_;
        croak $_;
    };
}
qr/Unknown database 'hidek' \(errno: 1049\)/ => 'database hidek is not exists';

throws_ok {
    try {
        my $dbh =
          create_dbh( "dbi:mysql:dbname=test;host=localhost", "root", "" );
        $dbh->selectall_arrayref("SELECT * FROM hidek");
    }
    catch {
        note $_;
        croak $_;
    }
}
qr/Table 'test\.hidek' doesn't exist/ => 'table hidek is not exists';

throws_ok {
    try {
        my $dbh =
          create_dbh( "dbi:mysql:dbname=test;host=localhost", "root", "" );
        my $sth = $dbh->prepare('SHOW TABLES');
        $sth->execute( 1, 2, 3 );
    }
    catch {
        note $_;
        croak $_;
    }
}
qr/called with 3 bind variables when 0 are needed/ => 'invalid bind params';

done_testing;

これを実行すると次のようになります。

ok 1 - database test is exists
# Unknown database 'hidek' (errno: 1049) at /usr/lib/perl5/site_perl/5.8.8/i386-linux-thread-multi/DBI.pm line 667
ok 2 - database hidek is not exists
# Table 'test.hidek' doesn't exist (dbname: test, errno: 1146, host: localhost, user: root) at handle_error.pl line 86
ok 3 - table hidek is not exists
# called with 3 bind variables when 0 are needed (dbname: test, errno: -1, host: localhost, user: root) at handle_error.pl line 100
ok 4 - invalid bind params
1..4

という訳で、dbname だとか host がめでたく取れましたとさ。ひょっとしたら HandleSetError とかでやった方が良いかもしれませぬ。

Webテクノロジーセミナー in Hokkaido と Hokkaido.pm

先日、Yahoo!モバゲーサンドボックス環境のリリースも何とかかんとかリリースいたしました。皆さん是非使って見て下さいね。*1

それはさておき8/6, 8/7は札幌で行われるイベントにお話しに行きます。

モバゲータウン」のディー・エヌ・エー社、Japan Perl Association(JPA)から講師陣をお迎えして、北海道札幌で、 WebとPerlのテクノロジーについてのセミナーイベントを開催致します。 モバゲータウンMovable Type で使用されているPerl言語についての講演や、オープンプラットフォームなどのWebテクノロジーについての講演を予定しております。 本セミナーは、WebやPerlといったテクノロジーを利用したビジネスやプロジェクト、コミュニティなどにご興味のあるエンジニアの方を対象としております。

Webテクノロジーセミナーではせっかくなので旬のネタであるヤバゲーの開発裏話とか OpenSocial の話、QUnit によるテストとかその辺りの事を話そうかなーとか思ってます。北海道在住の方や旅行でその時期にいらっしゃる方などふるってご参加頂けたらと思います。

次の日にあるHokkaido.pm #1 ではぐっと Perl よりの話をしようかなーと。多分テストの話になると思います。

話は変わりますが、fukuoka.pm の発起人である @sugmak さんこと杉山誠さんの訃報が突然伝えられ、ただただ驚いております。sugmak さんとは昨年の YAPC で初めてお会いしましたが、その際には YAPC のリアルタイムレポートを担当頂きました。きっとこのレポートを楽しみにされていた方も多かったのではないでしょうか。

sugmak さんのご冥福をお祈りすると同時にこの事で挫けず、fukuoka.pm がさらに盛り上がると良いなと思いました。
次の fukuoka.pm に予定が合えば参加しようかなとちょっと思ってます。

*1:とか言いつつ法人のみですが><

使ってると思しきモジュール一覧を列挙するワンライナー

Module::ExtractUse で何となく出来る。

perl -MModule::ExtractUse -le 'my $p = Module::ExtractUse->new; my @used; while (my $module = shift @ARGV ) { $p->extract_use($module); push(@used, $p->array); } my %seen; @used = sort { $a cmp $b } grep { !$seen{$_}++ } @used; local $,="\n"; print @used' `find ./lib -name "*.pm"`

こういう感じ?

ただ use parent とかで指定したモジュールとかまでは理解してくれないのであくまで補助的にかなー。もっと良い方法あったら教えてエロい人!

information_schema から trigger の定義を取得する

PROCEDURE/FUNCTION の場合は何も気にせず SHOW CREATE PROCEDURE とかで取得出来るんですが、TRIGGER の場合はそれ相当のステートメントが存在しません。

発火するタイミング (BEFORE/AFTER) と発火するイベント (INSERT/UPDATE/DELETE) とかあるからシンプルな構文にはならなさそうですが。

で、やっぱり information_schema があればどうにかなりますね。

SELECT CONCAT('CREATE DEFINER=`', SUBSTRING_INDEX(DEFINER, '@', 1), '`@`', SUBSTRING_INDEX(DEFINER, '@', -1), '` TRIGGER ', TRIGGER_NAME, ' ', ACTION_TIMING, ' ', EVENT_MANIPULATION, ' ON ', EVENT_OBJECT_TABLE, '\nFOR EACH ROW ', ACTION_STATEMENT) AS Create_Trigger FROM TRIGGERS WHERE TRIGGER_SCHEMA = 'test' AND EVENT_OBJECT_TABLE = 'hidek' \G

とかやると、

*************************** 1. row ***************************
Create_Trigger: CREATE DEFINER=`zigorou`@`%` TRIGGER hidek_on_after_insert AFTER INSERT ON hidek
BEGIN
  CALL create_hidek_gallary( NEW.id );
  CALL update_hidek_money( NEW.amount, 0, NEW.status );
END
*************************** 2. row ***************************
Create_Trigger: CREATE DEFINER=`root`@`%` TRIGGER hidek_on_after_update AFTER UPDATE ON hidek
BEGIN
  CALL update_hidek_gallary( NEW.id );
  CALL update_hidek_money( NEW.amount, OLD.amount, NEW.status );
END
2 rows in set (0.00 sec)

こんな感じで出て来ます。

当たり前ですが、mysqldump でも --triggers オプションを使えば定義を取得出来ますが、DBI 経由でその定義を設定したい場合は内容を整形しないと怒られてしまうので、こんな感じのやり方で定義を一個ずつ取り出せた方が良いかなーとか思った次第でした。

DeNA の TechStuDIG 2010 始まったよ!

開発環境の下りが誤っていたので訂正しました。

学生の皆さんに会社の方から PR ぜよ!

TechStuDIG ってなんだよ!

もの凄く簡単に言うと 2012/4 に入社可能な学生さんが モバゲーオープンプラットフォーム DeNA で用意する開発環境 を使って1.5ヶ月使って新しいソーシャルゲームを開発出来るよと言うコンテストです。
その際に DeNA の会社に来て開発して頂きます。

採択されると1.5ヶ月で10万円も貰えますよ!是非チャレンジしてみて下さい。

良いゲームの芽が出て来たら、1.5ヶ月で開発しきれなかった分も引き続き DeNA のスタッフと共に開発出来たりします。
優勝するとシリコンバレーツアーもついてきますよー。

と言う訳で

我こそはと言う方は下記にアクセスしちゃって下さいませ。

http://www.dena.jp/recruit/sp/newgraduate/studig02.html

twitter の公式アカウントは、@studig_pr です。こちらも是非フォローして下さいませ。

MySQL の複合 DELETE 構文

1ヶ月半ぶりのエントリです。皆さんお元気ですか?
何故か最近 Eclipse ばっかり使ってる zigorou でございます。

12.2.1 DELETE 構文 を見ていたら複合 DELETE 構文ってのが有ったので試してみました。

前提としてレコードがうんざりする程多いテーブル、、、と言う背景があります。

解説

とりあえず次のようなテーブルがあるとしましょう。

CREATE TABLE `diary` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `guid` int(11) NOT NULL,
  `subject` varchar(32) DEFAULT NULL,
  `body` text,
  `created_on` datetime DEFAULT NULL,
  `updated_on` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

comment
Create Table: CREATE TABLE `comment` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `diary_id` int(11) NOT NULL,
  `guid` int(11) NOT NULL,
  `body` varchar(256) DEFAULT NULL,
  `created_on` datetime DEFAULT NULL,
  `updated_on` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

comment.diary_id には diary.id が入っていて、FOREIGN KEY とかは貼ってない、もっと言うと ON DELETE CASCADE みたいな定義が無いとして、diary.id 値が 1, 3 の diary のレコードと、comment.diary_id が 1, 3 のレコード全部消したい、みたいなシーンがあるとしましょう。

大人しく書けば両方で、

DELETE FROM diary WHERE id IN (1, 3);
DELETE FROM comment WHRRE diary_id IN (1, 3);

みたいな事をやると思うんですが、IN 文の id 値がたくさんある何て場合にこれらの DELETE 句を実行するのはちょっとはばかられますね。

これは実は、複合 DELETE 構文ってのを使うと、

DELETE c, d FROM comment c, diary d WHERE c.diary_id IN (1, 3) AND d.id = c.diary_id;

と言う風に書くと上記の条件を満たした diary, comment のレコードを1つの SQL で書く事が出来ます。ちょうど、

SELECT c.*, d.* FROM comment c, diary d WHERE c.diary_id IN (1, 3) AND d.id = c.diary_id\G

で取り出される comment, diary 全部が対象になります。親子関係に注意しないとダメです!

サンプルコード

#!/usr/bin/perl

use strict;
use warnings;

use Data::Dump qw(dump);
use DBI qw(neat);
use SQL::Abstract;
use SQL::Abstract::Plugin::InsertMulti;

my @diaries = (
    # [ guid, subject, body, created_on, updated_on ]
    [ 100, 'aaa', 'aaaaaa', \'NOW()', \'NOW()' ],
    [ 101, 'bbb', 'bbbbbb', \'NOW()', \'NOW()' ],
    [ 102, 'ccc', 'cccccc', \'NOW()', \'NOW()' ],
    [ 102, 'ddd', 'dddddd', \'NOW()', \'NOW()' ],
);

my @comments = (
    # [ diary_id, guid, body, created_on, updated_on ],
    [ 1, 100, 'aaa', \'NOW()', \'NOW()', ],
    [ 1, 101, 'bbb', \'NOW()', \'NOW()', ],
    [ 2, 102, 'ccc', \'NOW()', \'NOW()', ],
    [ 2, 103, 'ddd', \'NOW()', \'NOW()', ],
    [ 3, 100, 'eee', \'NOW()', \'NOW()', ],
    [ 3, 101, 'fff', \'NOW()', \'NOW()', ],
    [ 4, 102, 'ggg', \'NOW()', \'NOW()', ],
    [ 4, 103, 'hhh', \'NOW()', \'NOW()', ],
);

my $dbh = DBI->connect( 'dbi:mysql:dbname=test', 'root', '', +{ AutoCommit => 0, RaiseError => 1, ShowErrorStatement => 1 } );

$dbh->do( 'ALTER TABLE diary auto_increment = 0' );
$dbh->do( 'ALTER TABLE comment auto_increment = 0' );
$dbh->do( 'TRUNCATE TABLE diary' );
$dbh->do( 'TRUNCATE TABLE comment' );

my ( $stmt, @bind );
my $sql = SQL::Abstract->new;
my $rv;

( $stmt, @bind ) = $sql->insert_multi(
    'diary',
    [qw/guid subject body created_on updated_on/],
    \@diaries,
);

$rv = $dbh->do( $stmt, undef, @bind );
printf( "affected rows %s at diary table\n", neat($rv) );

( $stmt, @bind ) = $sql->insert_multi(
    'comment',
    [qw/diary_id guid body created_on updated_on/],
    \@comments,
);

$rv = $dbh->do( $stmt, undef, @bind );
printf( "affected rows %s at comment table\n", neat($rv) );

$dbh->commit;

$dbh->do( 'CREATE TEMPORARY TABLE IF NOT EXISTS diary_delete ( id INT PRIMARY KEY NOT NULL ) ENGINE=Memory' );
$dbh->do( 'INSERT INTO diary_delete (id) VALUES (?), (?)', undef, 1, 3 );
$dbh->do( 'DELETE c, d FROM comment c, diary d, diary_delete dd WHERE c.diary_id = dd.id AND d.id = c.diary_id' );

print dump( $dbh->selectall_arrayref( 'SELECT * FROM diary', +{ Slice => +{} } ) );
print dump( $dbh->selectall_arrayref( 'SELECT * FROM comment', +{ Slice => +{} } ) );

$dbh->disconnect;

で試しに実行結果です。

affected rows 4 at diary table
affected rows 8 at comment table
[
  {
    body => "bbbbbb",
    created_on => "2010-05-18 15:50:31",
    guid => 101,
    id => 2,
    subject => "bbb",
    updated_on => "2010-05-18 15:50:31",
  },
  {
    body => "dddddd",
    created_on => "2010-05-18 15:50:31",
    guid => 102,
    id => 4,
    subject => "ddd",
    updated_on => "2010-05-18 15:50:31",
  },
][
  {
    body => "ccc",
    created_on => "2010-05-18 15:50:31",
    diary_id => 2,
    guid => 102,
    id => 3,
    updated_on => "2010-05-18 15:50:31",
  },
  {
    body => "ddd",
    created_on => "2010-05-18 15:50:31",
    diary_id => 2,
    guid => 103,
    id => 4,
    updated_on => "2010-05-18 15:50:31",
  },
  {
    body => "ggg",
    created_on => "2010-05-18 15:50:31",
    diary_id => 4,
    guid => 102,
    id => 7,
    updated_on => "2010-05-18 15:50:31",
  },
  {
    body => "hhh",
    created_on => "2010-05-18 15:50:31",
    diary_id => 4,
    guid => 103,
    id => 8,
    updated_on => "2010-05-18 15:50:31",
  },
]

実際に実行してみると、comment.diary_id が 1, 3 の comment のレコード及び diary.id が 1, 3 の diary のレコードが一括で削除されているのが分かるかと思います。

まとめ

そんな大量のデータを扱う必要の無い人はまったく要らない知識だと思います。

削除候補の id を一旦 temporary table に格納しておいてそれと join しつつ一括で複数のテーブルの条件に一致するレコードを削除する、、、なんてのに使えそうだなーと。
例えば削除候補が 100 万件とかあったら、、、って考えると temporary table にちまちま入れつつ繰り返し DELETE LOW_PRIORITY とかでやるとかは使えそうだなと。

ただ先ほども述べましたが親子関係に注意しないと思わぬレコードを消してしまって再帰不能なんて事もあると思うのでよくよく注意して使った方が良さそうです。