Web Storageが単純なキー/バリュー型のストレージであるのに対し、SQLを用いた柔軟なデータアクセスを行える、リレーショナルデータベースを扱うための仕様がWeb Databaseだ。
原稿執筆時点ではSafari 4のみがAPIを部分的に実装しているという状況であり、汎用的に使用できるAPIとは言いがたい状況だが、オフラインWebアプリを実装する上で欠かせない仕様なのは間違いない。今から学んでおく価値は十分にあるだろう(現時点でローカルDBを試したければ、Gearsを使用するという手段もある)。
データベースのオープン
ローカルデータベースを使用するためには、まずopenDatabase()という関数を呼び出して、データベースをオープンする必要がある。openDatabase()のシグネチャは以下のようになっている。
Database openDatabase(name, version, displayName, estimatedSize)
第1引数のnameはデータベースの名称だ。空文字も含むあらゆる文字列が使用可能だ。
第2引数のversionは、データベーススキーマのバージョンを表す文字列だ。Webアプリのバージョンが上がり、ローカルDBのスキーマを変更する必要が生じたときに、バージョンによる管理が有効だ。空文字を使用すると、バージョンに関係なくデータベースをオープンすることができる。
第3引数のdisplayNameは、データベースをユーザに表示する際に使用される名称だ。Safariでは、環境設定ダイアログの「セキュリティ→データベース」をクリックして表示されるデータベース一覧で、displayNameの値が表示される。
第4引数のestimatedSizeは、データベースの見積りサイズをbyte単位で指定する。
SQLの実行
データベースのインスタンスを取得したら、SQLを用いてデータの操作を行うことができる。
すべてのSQLはトランザクション内で実行する必要がある。トランザクション処理を行うためには、Databaseオブジェクト(openDatabase()の戻り値)が持つtransaction()メソッドを使用する。
// dbはDatabaseオブジェクト
db.transaction(callback, onError, onSuccess)
引数は前から順番に「トランザクション内で行う処理」「トランザクションがエラー終了した際の処理」「トランザクションが正常終了した際の処理」となる。第1引数以外は省略可能だ。
transactionメソッドのコールバック関数には、引数としてSQLTransactionオブジェクトが渡される。同オブジェクトは、SQLを実行するためのexecuteSqlというメソッドを持つ。
// txはSQLTransactionオブジェクト
tx.executeSql(sql, args, onSuccess, onError)
第1引数のsqlは、SQL文を文字列で指定する。SQLにはプレースホルダとして「?」を含めることができ、これらは第2引数のargsによって置換される。onSuccess, onErrorはSQL文を実行した結果を受け取るためのコールバック関数だ。
executeSql()メソッドは、第2引数以降は省略可能となる。
これらを踏まえて、DBをオープンしてからSQLを実行するプログラムの疑似コードを以下に示す。
// データベースのオープン
var db = openDatabase(...);
// トランザクションの実行
db.transaction(
// トランザクション内の処理
function(tx) {
// SQLの実行
tx.executeSql(
"SQL", // SQL文
[arguments], // SQL文のパラメータ
// SQLの実行に成功した際の処理
function onSuccess(tx, resultSet) {
},
// SQLの実行に失敗した際の処理
function onError(tx, error) {
}
);
},
// トランザクション内でエラー発生時の処理
function onError(error) {
}
);
たった1つのSQLを実行するだけのコードなのに、コールバック関数だらけで非常に読みづらい。UI操作を妨げないようすべてのメソッドシグネチャが非同期呼出を前提としているので、致し方ないところではあるが…。
なお、ワーカの中でのみ使用できる同期型のAPIも実は存在するが、現時点で実装しているブラウザがないため説明は行わない。
SQLの実行結果
SQLTransaction.executeSql()メソッドは、DDLを含むあらゆる種類のSQLを実行可能だ。SQLの実行結果はコールバック関数の引数として渡されるSQLResultSetオブジェクトに格納される。
SQLResultSetオブジェクトは、以下のプロパティを持つ。
- insertId … INSERT文を実行した結果、自動的に生成されたIDを返す
- rowsAffected … SQLを実行した結果、変更が行われた行の数を返す
- rows … SELECT文を実行した結果。以下のプロパティ/メソッドを持つ
- length … 検索結果の総数
- item(index) … インデックスを指定して行データを取得する。戻り値は通常のJavaScriptオブジェクト(プロパティ名はカラム名に対応)
これらを踏まえて、SELECT文を実行して結果を取得するための疑似コードを以下に示す。
// txはSQLTransactionオブジェクト
tx.executeSql("select name,val from storage", [],
function(tx, rs) {
// 検索結果はrowsプロパティに格納されている
var rows = rs.rows;
// 検索の総数はlengthに格納されている。
// lengthプロパティへのアクセスは重たい処理になる可能性もあるので、
// 変数に格納しておいて利用する
var length = rows.length;
for (var i = 0; i < length; i++) {
// 指定したインデックスの行データを取得
var row = rows.item(i);
// プロパティにアクセスすればカラムのデータを取得できる
var name = row.name, value = row.val;
...
}
});
サンプル
以下に示すサンプルは、Web Storageでお見せしたサンプルをWeb Databaseを用いて書き直したものだ。APIに関する説明はすでに終えているので、サンプルコードに関する詳細な解説は行わない。コメントを参照しながら読み進めて欲しい。このサンプルは、Safari 4上での動作が確認済みだ。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript">
// データベースのオープン
var db = openDatabase("mycom_sample", "", "Sample for mycom", 1024*1024);
// テーブルの作成
db.transaction(function(tx) {
tx.executeSql("create table if not exists storage (name, val)");
});
function load() {
db.transaction(function(tx) {
tx.executeSql("select * from storage", [], function(tx, rs) {
var list = document.getElementById("list");
list.innerHTML = "";
var rows = rs.rows;
// storageテーブルに格納されている全ての値を列挙
for (var i = 0, n = rows.length; i < n; i++) {
var row = rows.item(i);
list.options[list.options.length] = new Option(row.val, row.name);
}
});
});
}
function remove() {
var list = document.getElementById("list");
if (list.selectedIndex < 0) {
return;
}
var selected = list.options[list.selectedIndex].value;
// 選択された項目を削除
db.transaction(function(tx) {
tx.executeSql("delete from storage where name = ?", [selected], function() {
load();
});
});
}
function add() {
var name = document.getElementById("name").value;
var value = document.getElementById("value").value;
// storageテーブルに値を格納
db.transaction(function(tx) {
tx.executeSql("insert into storage (name, val) values (?, ?)", [name, value], function() {
load();
});
});
}
</script>
</head>
<body onload="load()">
<h1>Databaseのサンプル</h1>
<select id="list" size="5" style="width: 100px"></select>
<button onclick="remove()">削除</button><br>
キー:<input type="text" id="name"><br>
値:<input type="text" id="value">
<button onclick="add()">追加</button><br>
</body>
</html>