DBD::SQLite 1.30_02

あと、先日リリースしたDBD::SQLite 1.30_02の変更点について。

DBD::SQLiteはこれまで

  • DBIが用意した枠組みの範囲内でのトランザクションを利用している(AutoCommit/begin_work)
  • fork()などを利用して並列性を高めている
  • 読み書きを平行して行っている

といった条件が重なったときにトランザクションデッドロックすることがある、という問題を抱えていました。最小限の再現コードはこんな感じになります。

use strict;
use warnings;
use DBI;

my $pid = fork();
if ($pid) {
    do_transaction();
}
else {
    sleep 1;
    do_transaction();
    exit;
}

unlink 'test.db';

sub do_transaction {
    my $dbh = DBI->connect('dbi:SQLite:test.db');
    $dbh->do('create table if not exists foo (id)');
    $dbh->begin_work;
    $dbh->do('select * from foo;');
    $dbh->do('insert into foo values(1)');
    sleep 2;
    $dbh->commit;
}

これはSQLiteのマニュアル等にも載っている既知の問題で、対策としてはトランザクションを開始するときに BEGIN IMMEDIATE ないし BEGIN EXCLUSIVE といったSQLを発行することがあげられています。

sub do_transaction {
    my $dbh = DBI->connect('dbi:SQLite:test.db');
    $dbh->do('create table if not exists foo (id)');
#    $dbh->begin_work;
    $dbh->do('begin immediate');
    $dbh->do('select * from foo;');
    $dbh->do('insert into foo values(1)');
    sleep 2;
    $dbh->commit;
}

もちろんこれでもいいんですが、これだとO/Rマッパ等では無力なので、1.30_02では接続時に(ないし任意のタイミングで)データベースハンドルにsqlite_use_immediate_transactionというアトリビュートを渡すことで内部的に発行しているbeginをbegin immediateにできるようにした、というのが今回の変更点。

sub do_transaction {
    my $dbh = DBI->connect('dbi:SQLite:test.db');
    $dbh->{sqlite_use_immediate_transaction} = 1;
    $dbh->do('create table if not exists foo (id)');
    $dbh->begin_work;
    $dbh->do('select * from foo;');
    $dbh->do('insert into foo values(1)');
    sleep 2;
    $dbh->commit;
}

DBD::SQLiteの用途的にはデフォルトで有効にしておいてもよさそうですが、この辺はもう少し余裕のあるときにO/Rマッパをつくっている人たちと相談してみるつもりです。