SQLでincrementした値を表示する方法を考える

例えば、アクセスするたびにそのページのPVをインクリメントしつつ表示するWebアプリを作るとする。
hogeというDBで

CREATE TABLE `fuga` (
  `path` varchar(256) NOT NULL,
  `pageview` int(10) unsigned default NULL,
  PRIMARY KEY  (`path`)
);

といったテーブルを作り、ここにURLをkeyとしたpageviewの情報を蓄積するようにする。
DBIx::Classを使ったwebアプリは

package Hoge::Schema;
use parent 'DBIx::Class::Schema::Loader';

__PACKAGE__->naming('current');


package main;

my $schema  = Hoge::Schema->connect('dbi:mysql:hoge', 'root', '');
my $fuga_rs = $schema->resultset('Fuga');

my $app = sub {
    my ($env) = @_;

    my $fuga = $fuga_rs->find_or_create({
        path     => $env->{PATH_INFO},
        pageview => 0,
    });
    $fuga->pageview(\'pageview + 1');
    $fuga->update_or_insert;

    $body = join ':', ($fuga->path, $fuga->pageview);
    return [200, [ 'Content-Type' => 'text/plain' ], [ $body ]];
};

といったかんじで書ける(※色々間違ってました。追記しました)。レコードが無ければ作って、あとは

UPDATE fuga SET pageview = pageview + 1 WHERE ( path = '/' );

を発行させて、そのpageviewの値をレスポンスで返すだけ。
…と思ったのだけど、実際アクセスしてみると返ってくるレスポンスは

/:SCALAR(0x1009de480)

のような形になってしまう。updateした際にpageviewに \'pageview + 1' というのを渡しているので、このスカラーリファレンスがそのまま返ってきてしまう。
のでこれを防ぐために

    $fuga->pageview(\'pageview + 1');
    $fuga->update_or_insert;
    $fuga->discard_changes;

のようにupdateしたあとに再びDBから取得しなおすなどの操作が必要になる。


最近リリースされたTengを使った場合だとこんなかんじかな?

package Hoge::DB;
use parent 'Teng';

__PACKAGE__->load_plugin('FindOrCreate');

1;


package main;
use Teng::Schema::Loader;

my $dbh = DBI->connect('dbi:mysql:hoge', 'root', '');
my $schema = Teng::Schema::Loader->load(
    dbh       => $dbh,
    namespace => 'Hoge::DB',
);
my $teng = Hoge::DB->new(
    dbh    => $dbh,
    schema => $schema,
);

my $app = sub {
    my ($env) = @_;

    my $fuga = $teng->find_or_create('fuga', {
        path => $env->{PATH_INFO},
    });

    $fuga->update({ pageview => \'pageview + 1' });
    $fuga = $fuga->refetch;

    $body = join ':', ($fuga->path, $fuga->pageview);
    return [200, [ 'Content-Type' => 'text/plain' ], [ $body ]];
};

これもやっぱりupdateの後にrefetchしないとSCALAR refが出てきてしまう。


まぁ考えてみれば当たり前で

UPDATE fuga SET pageview = pageview + 1 WHERE ( path = '/' );

というクエリはpageviewの値をインクリメントさせるだけでその値が何になったかは分からないのでもう一度selectし直すしかないわけで。


それだけのためにselect文を発行するのはアレだなーというときは

    $fuga->update({ pageview => $fuga->pageview + 1 });

とやるか、もう表示に使う値は更新前に取っておいて、

    my $pageview = $fuga->pageview;
    $fuga->update({ pageview => \'pageview + 1' });

    $body = join ':', ($fuga->path, $pageview + 1);

のように使うか、でしょうか。前者は$fugaを取得してからupdateする前に外部で更新されまくっていたときに上書いてしまう危険があるので良くなさそう。
ちゃんとやるならfind or createする前のところからtransaction張って、ということになるのだと思うけど そこまで厳密な値が必要ない場合だったら後者のようなやり方でいいかな。

追記

kazuhoさん、nekokakさん、tokuhiromさんからアドバイスとご指摘いただきました!記事を書き直す余裕がないのでつぶやきまとめだけ載せておきます >< ありがとうございました!
http://d.hatena.ne.jp/sugyan/20110120/1295481689 のフィードバック - Togetterまとめ