-> 09 { 10 / 08 }
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 があるが、現時点で実装しているブラウザはない