IT戦記

ただただがむしゃらにソフト開発をしていたい

Safari 3.1 に実装された「Client-side database storage (SQL API)」とは何か?

はじめに

Safari 3.1 には Client-side database storage (SQL API とも呼ばれています。)という新しい仕様が実装されました。
というわけで、この新しい API について色々調べたことを簡単にまとめておきます。

Client-side database storage が使えるブラウザ

2008 年 03 月 27 日現在では、 Safari 系のブラウザのみです。

Client-side database storage とは

Selectors API とは HTML5 で定義された仕様です。詳細に関してはこちらをどうぞ。
簡単に説明すると JavaScript 内でリレーショナルデータベースを使えるということです。
もっと簡単にイメージするために、実際のコードを示すとこんな感じです。

var db = openDatabase('mydb', '1.0');

// トランザクションの開始
db.transaction(function(tx) {
  // テーブルを作る
  tx.executeSql('CREATE TABLE IF NOT EXIST link (id INTEGER PRIMARY KEY, href TEXT, title TEXT)');

  // リンクを抽出
  var xresult = document.evaluate('//a', document, null, 7, null);
  for (var i = 0; i < xresult.snapshotLength; i ++) {
    var elmLink = xresult.snapshotItem(i);

    // テーブルに挿入
    tx.executeSql('INSERT INTO link VALUES(NULL, ?, ?)', [elmLink.href, elmLink.textContent])
  }
});

このように JavaScript から SQL を実行してデータベースを使うことができるのです。
簡単ですね!

データベースの保存場所と保存期間と共有範囲

保存場所

データは、クライアントサイドに保存されます。
サーバにあるデータベースとは一切関係ないので誤解しないように

保存期間

このデータベース内の情報はリロードしてもブラウザを再起動しても失われません。
永続 Cookie のような感じです。

共有範囲

このデータベースは、同じドメイン*1で有効です。
つまり、

  • http://www.example.com/hoge/hoge
    • http://www.example.com/hoge/fuga は同じデータベースを見ています。
    • http://www.example.com/fuga/fuga は同じデータベースを見ています。
    • http://www.example.com:80/ は同じデータベースを見ています。
    • http://example.com/hoge/hoge は違うデータベースを見ています。
    • http://sub.example.com/hoge/hoge は違うデータベースを見ています。
    • https://www.example.com/hoge/hoge は違うデータベースを見ています。
    • http://www.example.org/hoge/hoge は違うデータベースを見ています。

という感じになります。
いわゆる「Same origin policy」というやつですね。

関数の具体的な使い方

Client-side database storage を使うには以下の三つの種類の関数を使います。

  • openDatabase
    • データベースを作る、開く
  • transaction, changeVersion
  • executeSql
    • SQL を実行する

では、ひとつひとつ見ていきましょう

openDatabase

openDatabase 関数はデータベースを開く関数で、データベースがない場合はデータベースを作ります。(あってもなくても開くってことです。)
この関数は、グローバル関数として使うことができます。

// 以下のように
//     第一引数にデータベースの名前、
//     第二引数にデータベースのバージョン名(自分でかってに決める、適当でいい)
// を与えると、データベースを開いて、
//     Database オブジェクト
// を返します
var db = openDatabase('testdb', '1.0');

// 既に testdb というデータベースが(同じドメイン内のデータベースとして)存在している場合で、
// バージョンが違う場合は例外(エラー)が投げられます。
db = openDatabase('testdb', '2.0'); // error!

また、データベースの容量はブラウザ側で制限されている可能性があります(実装依存)。
その場合は以下のように書きます。

// 第三引数に何かしらのメッセージ
// 第四引数に使いたい容量(バイト数)
var db = openDatabase('testdb', '1.0', '100MB 使わせてください><', 100000000);
transaction, changeVersion

transaction 関数は openDatabase 関数で取得した Database オブジェクトの関数(メソッド)です。
トランザクションを開くことができます。

// 第一引数に関数を渡します。
// すると、
//     トランザクションが開始され、
//     関数が実行され
//     関数が終了するとトランザクションが終了します。
db.transaction(function(tx) { // 関数の第一引数に SQLTransaction オブジェクトが渡される

  // この関数内で、
  // SQLTransaction オブジェクトを使ってデータベースを操作する
  tx.executeSql('INSERT INTO hoge VALUES(NULL, "hoge")');

});

ここで、注意しなければならないことがあります。
それは、 transaction 関数に渡された関数は非同期に呼び出されるということです。
たとえば、

var data = 'hoge';

db.transaction(function(tx) {
  // ここで data は 'fuga' になる
  tx.executeSql('INSERT INTO hoge VALUES(NULL, ?)', [data]);
});

// ここは transaction より前に実行される
data = 'fuga';

イメージ的には setTimeout に渡された関数の挙動と同じですね。
では、transaction が終わった後に処理をしたい場合はどうしたらいいでしょうか。
そんなときは、 transaction 関数に第二引数、第三引数として関数を渡します。
するとトランザクション終了時にそれらの関数を呼び出してくれます。

  • 第二引数は、エラーが発生して、ロールバックした場合に呼び出されます
  • 第三引数は、正常にトランザクションがコミットされた場合に呼び出されます。

以下のような感じです。

tx.transaction(
  function(tx) {/* ...略 */},
  function(error) {
    alert(error.message + 'という訳でロールバックしたよ')
  },
  function() {
    alert('正常に終了したよ'); 
  }
);

また、 transaction 関数の特殊版として changeVersion という関数があります。
これも、 Database オブジェクトの関数(メソッド)です。
データベースのバージョンを変更するために使います。
バージョンとは、 openDatabase の第二引数で指定したものです。

  • 第一引数に、古いバージョン(openDatabase の時に指定したもの)
  • 第二引数に、新しいバージョン

を指定します。
第三引数以降は、 transaction 関数の第一引数以降と同じです。

tx.changeVersion('1.0', '2.0',
  function(tx) {
    // テーブルを追加するなどする
  }, 
  function(error) {
    // エラーが発生した
  },
  function() {
    // 正常終了
  }
);

なぜ、バージョン変更関数とトランザクションが関係あるかというと、バージョンが変わるとその際にいろいろデータの形をかえたりする必要があるためです。
バージョンを変更すると openDatabase 関数の第二引数に与えるべき値も変わります。
ただ、僕が Safari 3.1 で、この関数を試したところ、バージョンを変更したデータベースがオープンできなくなってしまいました。ちょっと調べています(バグ?)

executeSql

executeSql 関数は transaction 関数に渡した関数に渡される SQLTransaction オブジェクトの関数(メソッド)です。
第一引数に SQL を渡すことで SQL を実行することができます。
こんな感じです。

db.transaction(function(tx) {
  tx.executeSql('CREATE TABLE IF NOT EXIST bbs (id INTEGER PRIMARY, title TEXT, content TEXT)');
});

トランザクションの外では実行できません。

var globalTx;
db.transaction(
  function(tx) {
    globalTx = tx;
  },
  function() {},
  function() {
    // ここは、トランザクション終了後に実行されるため
    // 以下は、エラー
    globalTx.executeSql('CREATE TABLE IF NOT EXIST bbs (id INTEGER PRIMARY, title TEXT, content TEXT)');
  }
);

また、プレースホルダ「?」を使うには、第二引数に配列を渡します。

db.transaction(function(tx) {
  tx.executeSql('INSERT INTO bbs VALUES(NULL, ?, ?)', ['hoge', 'hogehoge']);
});

また、結果を使うような場合は、第三引数に関数を渡します。
そうすると、 SQL 実行後に結果を表す SQLResultSet オブジェクトがその関数に渡されます。

db.transaction(function(tx) {
  tx.executeSql('SELECT * FROM bbs', [], function(tx, rs) { // rs が SQLResultSet オブジェクト。 tx は同じ。

    // rs.rows に行のデータが入っている
    for (var i = 0; i < rs.rows.length; i++) {
      var row = rs.rows.item(i);

      // row はカラム名でハッシュになっている
      var title = row.title; // または row['title']
      var content = row.content; // row['content']
    }
  });
});

ここで注意しなければならないのは、このコールバック関数は非同期に呼び出されるということです。
つまり、以下のようなことはできません。

db.transaction(function(tx) {
  var bbsid;
  tx.executeSql('INSERT INTO bbs VALUES(NULL, ?, ?)', ['hoge', 'hogehoge'], function(tx, rs) {
    bbsid = rs.insertId; // insertId には行に挿入したときに自動で決まった id が入っている
  });
  tx.executeSql('INSERT INTO bbs_user_map VALUES(NULL, ?, ?)', [bbsid, userid]); // この時点で bbsid は決まらない
});

なので、以下のようにすれば大丈夫です。

db.transaction(function(tx) {
  var bbsid;
  tx.executeSql('INSERT INTO bbs VALUES(NULL, ?, ?)', ['hoge', 'hogehoge'], function(tx, rs) {
    bbsid = rs.insertId;
    tx.executeSql('INSERT INTO bbs_user_map VALUES(NULL, ?, ?)', [bbsid, userid]); // この時点で bbsid は決まっている
  });
});

また、 SQL でエラーが発生した場合は自動でロールバックされるのですが、エラーを補足してロールバックさせないようにするには、第四引数に関数を渡します。

db.transaction(function(tx) {
  tx.executeSql('INSERT INTO bbs VALUES(NULL, ?, ?)', ['hoge', 'hogehoge'],
    function(tx, rs) {
    },
    function(tx, error) {
      alert(error.message + 'だけどロールバックしない');
    }
  );
});

こんな感じで SQL を実行します。
便利ですね!

Safari での実装

使い方がわかったところで Safari の実装について少し見てみましょう

データベースはどこに保存される?

Mac でしか確認していないのですがデータベースは ~/Library/Safari/Databases ディレクトリに以下のように保存されました。

Databases
|-- Databases.db
|-- http_amachang.art-code.org_0
|   `-- 0000000000000001.db
`-- http_hogehoge.com_0
    |-- 0000000000000006.db
    `-- 0000000000000006.db-journal
エンジンは?

Sqlite 3 のようです。sqlite3 コマンドで中身を見ることができました。

Web Inspector との連携

コンテンツでデータベースをオープンした状態で Web Inspector を開くと以下のようにデータベースを調べることができます。

この画面上からも、直接 SQL が打てるので便利です。

Google Gears

Client-side database storage は今は Safari からしか使うことができません
しかし、 Google Gears を使ってすべてのブラウザに実装することは可能だと思います。(すでにある?)
その辺りも少し調べてみたいです。

まとめ

という訳で、少し突っ込んで Client-side database storage というものを調べてみました。
Client-side database storage かなりおもしろかったです。
モバイルブラウザ上の JavaScript コンテンツ (iPhone 用のアプリ) や、 UserScripit などから利用するととても便利そうだなと思いました。

*1:ここでは単にドメインと書いたが、正確にはプロトコル(スキーム)、ホスト名、ポート番号が等しい範囲内。