Hatena::Groupsubtech

#生存戦略 、それは

-> 09 { 10 / 08 }

Web Database を Deferrerd でラップして扱う ORM, jsdeferred-webdatabase

21:12 | はてなブックマーク - Web Database を Deferrerd でラップして扱う ORM, jsdeferred-webdatabase - #生存戦略 、それは

現在 Web Database を実装しているブラウザは Safari4 / Chrome4 Dev などがある。これらのブラウザの実装では、WebDatabase は非同期で扱う openDatabase API のみ *1 なため、すべての SQL の結果は非同期で扱うことになる。で、DB を非同期で扱うというのがものすごくめんどくさくて、いろいろ書くのがめんどくさくなってきたので、めんどくささが味わいたい人は今すぐ直接 openDatabase を利用してみよう!

というわけで Transaction 部分を抽象化し、Model を ActiveRecord パターンでマッピングできるような ORマッパーの jsdeferred-webdatabase というのを作っています。

前述の通り SQL 発行の成功・エラー等のすべての結果はコールバック関数で受け取らなくてはならず、普通に非常に汚くなりがちになる。そこで JSDeferred を利用すると、成功は next チェイン、エラーは error チェインに繋げるだけなので、うまくラップできるように。

Database/Transaction 部分はこんな感じ。openDatabase の transaction は、transaction 内部で非同期の関数呼び出しをするとそこで終わってしまうため、WebDatabase.Transaction 内部では queue をもっており、deferred ぽい呼び出しなら繋げて、そうでなかったら executeSql で実際に発行、みたいなどろどろとしたことをやってる。

var Database = JSDeferred.WebDatabase;

db = new Database('dbname');
db.transaction(function(tx) {
  // tx は WebDatabase.Transaction のインスタンス
  tx.execute(sql).next(callback).error(errorback);
  tx.execute(sql2).next(callback).error(errorback);
}, true).next(finishCommitFunc).error(errorTransactionFunc);

// Transaction  は Deferrerd ぽく扱えるのでこんな風にも書ける
db.transaction(function(tx) {
  tx.
    execute(sql1).
    execute(sql2).
    execute(sql3).
    next(function(result) {
    });
});

// トランザクションを利用しないなら、Database に対して execute で発行もできる
db.execute('select * from tables').next(function(result) {
  // result は SQLResult オブジェクト
});

Model は ActiveRecord パターンで定義できる。getter/setter は __defineGetter__ 系のメソッドで直接代入・参照できるようになっている。

var User = Model({
    table: 'users',
    primaryKeys: ['uid'],
    fields: {
        'uid'        : 'INTEGER PRIMARY KEY',
        name         : 'TEXT UNIQUE NOT NULL',
        data         : 'TEXT',
        timestamp    : 'INTEGER'
    }
}, db);

// 基本的に SQL の操作が入るメソッドは Deferrerd オブジェクトを返すので、以下のように書ける
User.dropTable().next(User.createTable).next(function() {
  var u = new User({
    name: 'nadeko'
  });
  u.data = 'foobar';
  u.save().next(function(user) {
    user.uid; // 1
  }).next(User.count).next(function(c) {
    c; // 一件追加されたので1
  });
});

SQL文の生成には SQLAbstract ( http://subtech.g.hatena.ne.jp/secondlife/20091007/1254926133 ) を利用しているため、引数にそれっぽく書ける

User.findFirst({ name: 'yuno' }).next(function(user) { ... });

User.find({
  where: { uid: {'>', 10} },
  fields: 'name',
  limit: 10
}).next(function(result) {
  result[0]; // User のインスタンス
});

Model 自体に transaction を張り、最後に一斉にコミットもできる。SQLite は transaction 張らないと CRUD の CUD 操作がめちゃくちゃ遅いので、無いと遅くて死ねる。

こんな感じ(テストからコピペ)

var num = 0, afterSaveNum = 0, beforeSaveNum = 0;
var now = Date.now();
// afterSave/beforeSave でトリガれる
User.afterSave = function() {
    afterSaveNum++;
}
User.beforeSave = function() {
    beforeSaveNum++;
}

db.transaction(function() {
    for (var i = 0;  i < 5; i++) {
        var u = new User({num: i, name: 'name' + i});
        if (i == 3) {
            var u2 = new User({name: 'name' + 2});
            u2.save().error(function(e) {
                // すでに同名のnameのデータがあるはずなのでエラーになる
                ok(e, 'catch error2');
            });
        }
        u.save().next(function(res) {
            num++;
            return res;
        }).next(function(res) {
            ok(res.name, 'transaction save chain ok:' + res.name);
            num++;
        });
    }
    var u3 = new User({name: 'name' + 3});
    u3.save().next(function(n) {
        ok(false, 'don"t call this');
    }).error(function(e) {
        ok(e, 'catch error3');
        return 'okk';
    }).next(function(r) {
        equals('okk', r, 'get chainback success');
        num++;
    }) ;
}).next(function() {
    User.count().next(function(c) {
        equals(c, 5);
        equals(num, 11);
        equals(afterSaveNum, 5);
        equals(beforeSaveNum, 7);
        p(Date.now() - now);
        d.call();
    });
});

その他色々な機能は test 見てください。非同期周りのテストばっかりのためにひどいことに…。あと開発中なので、いろいろAPIは変わります。

当初は alex.record を使おうとしたけど、非同期周りとエラー周りの操作を自分の書き方だとスムーズに扱えなかったことと transaction の引き回しが大変だったので断念。JSDeferred と WebDatabase の連携はすでに Constellation さんがやっていたけど、Model と Transaction の抽象化をしたかったので一から書いてみた。

参考:

*1:ドラフトでは openDatabaseSync があるが、現時点で実装しているブラウザはない