Web Database を Deferrerd でラップして扱う ORM, jsdeferred-webdatabase
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 の抽象化をしたかったので一から書いてみた。
参考:
- http://d.hatena.ne.jp/Constellation/20090208/1234114965
- Constellation さんのエントリー。openDatabase のと非同期周りのことがよくまとまってます
- http://code.google.com/p/alexframework/wiki/AlexRecord_ja
- AlexRecord のページ
- http://dev.w3.org/html5/webdatabase/
- Web Database W3C Working Draft
*1:ドラフトでは openDatabaseSync があるが、現時点で実装しているブラウザはない