Firefox 4 の Indexed Database API を先取り紹介

訳注: 本文において IndexedDB と書かれている技術仕様の正式名称は Indexed Database API です。名前にスペースを含めることが議論により決定しています。

localStorage では単純なクライアントサイドのキーと値のペアを保存することができますが、これだけではまだウェブアプリケーションのための構造的でインデックスされたデータストレージの需要を叶えるものではありません。Mozilla では構造的ストレージとインデックスのための API である IndexedDB をサポートし、数週間以内にテストビルドをリリースする予定です。IndexedDB は、WebDatabase API という、SQLite のサブセット言語を用いる、いくつかのブラウザに実装されている API と比較することができます。Mozilla はこちらのポストに書かれているいくつかの理由により WebDatabase をサポートしないことを決定しています。

訳注: この経緯は「ブラウザ上のデータベースに関して」という記事に詳しいです。

IndexedDB と WebDatabase を比較するために、これらの仕様の非同期 API をほぼ網羅する4つの例を紹介します。これを読むとテーブルのある SQL ストレージ (WebDatabase) とインデックス付き JavaScript オブジェクトストレージ (IndexedDB) の違いがとてもよく分かるでしょう。同期バージョンの API は Worker スレッドからしか使うことができません。Worker スレッドはまだ全てのブラウザでサポートされているわけではないため、ここでは同期 API のことは話しません。IndexedDB のコードは、Mozilla が W3C WebApps ワーキンググループ (WG) に提出し、今のところ良いフィードバックを貰えている提案をベースとしています。両 API のコードは簡単のためにエラー処理などはしていませんが、もちろんみなさんが実用で使うときは忘れないようにしましょう。

訳注: Mozilla の提出した API 改正案はまだ2010年6月1日現在の Indexed Database API 仕様草案には反映されていませんが、Google Chrome チームの数人からの同意もあり、ほぼそのまま採用されると思われます。

キャンディー屋さんとお客さん (kids ということにします) の例を考えてみましょう。candySales という欄は、一人のお客さんへのあるキャンディーの売り上げを表しています。

例 1 – データベースを開いてセットアップする

最初の例は、データベースを開く方法を示します。データベースを開くときにバージョンをチェックし、必要なテーブルやオブジェクトストアを作り、正しい バージョン番号をセットします。WebDatabase はバージョンを厳しく処理し、開こうとしているデータベースのバージョンと違うものだった場合はエラーを出します。IndexedDB は単に呼び出す人がバージョン管理できるようにするだけです。バージョン管理に関してはまだ WG でも議論中なので注意してください。

WebDatabase

var db = window.openDatabase("CandyDB", "",
                             "My candy store database",
                             1024);
if (db.version != "1") {
  db.changeVersion(db.version, "1", function(tx) {
    // はじめてのユーザー。データベースを初期化。
    var tables = [
      { name: "kids", columns: ["id INTEGER PRIMARY KEY",
                                "name TEXT"]},
      { name: "candy", columns: ["id INTEGER PRIMARY KEY",
                                 "name TEXT"]},
      { name: "candySales", columns: ["kidId INTEGER",
                                      "candyId INTEGER",
                                      "date TEXT"]}
    ];
    for (var index = 0; index < tables.length; index++) {
      var table = tables[index];
      tx.executeSql("CREATE TABLE " + table.name + "(" +
                    table.columns.join(", ") + ");");
    }
  }, null, function() { loadData(db); });
}
else {
  // はじめてのユーザーではない。初期化しなくていい。
  loadData(db);
}

IndexedDB

var request = window.indexedDB.open("CandyDB",
                                    "My candy store database");
request.onsuccess = function(event) {
  var db = event.result;
  if (db.version != "1") {
    // はじめてのユーザー。データベースを初期化。
    var createdObjectStoreCount = 0;
    var objectStores = [
      { name: "kids", keyPath: "id", autoIncrement: true },
      { name: "candy", keyPath: "id", autoIncrement: true },
      { name: "candySales", keyPath: "", autoIncrement: true }
    ];

    function objectStoreCreated(event) {
      if (++createdObjectStoreCount == objectStores.length) {
        db.setVersion("1").onsuccess = function(event) {
          loadData(db);
        };
      }
    }

    for (var index = 0; index < objectStores.length; index++) {
      var params = objectStores[index];
      request = db.createObjectStore(params.name, params.keyPath,
                                     params.autoIncrement);
      request.onsuccess = objectStoreCreated;
    }
  }
  else {
    // はじめてのユーザーではない。初期化しなくていい。
    loadData(db);
  }
};

例 2 – お客さんをデータベースに保存する

この例では何人かのお客さんをテーブルやオブジェクトストアに保存します。また、WebDatabase では SQL injection のリスクを処理しなければいけないことを示します。WebDatabase では明示的にトランザクションを使わなければいけませんが、IndexedDB ではトランザクションは、たった一つのオブジェクトストアがアクセスされた場合に自動的に作られます。IndexedDB ではトランザクションのロックはオブジェクトストア毎です。さらに、IndexedDB では JavaScript のオブジェクトを挿入できますが、WebDatabase では特別にコラムを作らなければいけません。どちらの場合もコールバックで挿入 ID を得ることができます。

WebDatabase

var kids = [
  { name: "Anna" },
  { name: "Betty" },
  { name: "Christine" }
];

var db = window.openDatabase("CandyDB", "1",
                             "My candy store database",
                             1024);
db.transaction(function(tx) {
  for (var index = 0; index < kids.length; index++) {
    var kid = kids[index];
    tx.executeSql("INSERT INTO kids (name) VALUES (:name);", [kid],
                  function(tx, results) {
      document.getElementById("display").textContent =
          "Saved record for " + kid.name +
          " with id " + results.insertId;
    });
  }
});

IndexedDB

var kids = [
  { name: "Anna" },
  { name: "Betty" },
  { name: "Christine" }
];

var request = window.indexedDB.open("CandyDB",
                                    "My candy store database");
request.onsuccess = function(event) {
  var objectStore = event.result.objectStore("kids");
  for (var index = 0; index < kids.length; index++) {
    var kid = kids[index];
    objectStore.add(kid).onsuccess = function(event) {
      document.getElementById("display").textContent =
        "Saved record for " + kid.name + " with id " + event.result;
    };
  }
};

例 3 – お客さんの一覧を表示する

この例では kids テーブルと kids オブジェクトストアに保存されたお客さんを表示します。WebDatabase は全部の列を取得した後でコールバックメソッドに result set オブジェクトを渡します。一方、IndexedDB は結果を取得するごとに cursor handler を渡すので、結果が速く返ってくることになります。この例では示されていませんが、cursor.continue() を呼ばないことで繰り返しを止めることができます。

WebDatabase

var db = window.openDatabase("CandyDB", "1",
                             "My candy store database",
                             1024);
db.readTransaction(function(tx) {
  // テーブル全体を検索
  tx.executeSql("SELECT * FROM kids", function(tx, results) {
    var rows = results.rows;
    for (var index = 0; index < rows.length; index++) {
      var item = rows.item(index);
      var element = document.createElement("div");
      element.textContent = item.name;
      document.getElementById("kidList").appendChild(element);
    }
  });
});

IndexedDB

var request = window.indexedDB.open("CandyDB",
                                    "My candy store database");
request.onsuccess = function(event) {
  // オブジェクトストア全体をまわる
  request = event.result.objectStore("kids").openCursor();
  request.onsuccess = function(event) {
    var cursor = event.result;
    // cursor が null なら終了
    if (!cursor) {
      return;
    }
    var element = document.createElement("div");
    element.textContent = cursor.value.name;
    document.getElementById("kidList").appendChild(element);
    cursor.continue();
  };
};

例 4 – キャンディーを買ったお客さんを列挙する

この例ではキャンディーを買ったお客さんと、どれだけのキャンディーを買ったのかを表示します。WebDatabase では単に LEFT JOIN したクエリを使えば簡単にできます。IndexedDB は今のところ別々のオブジェクトストアをまたいだ JOIN をする API がありません。そのため、この例では kids オブジェクトへの cursor と candySales オブジェクトストアの kidId インデックスへのオブジェクト cursor を開き、手動で JOIN をしています。

WebDatabase

var db = window.openDatabase("CandyDB", "1",
                             "My candy store database",
                             1024);
db.readTransaction(function(tx) {
  tx.executeSql("SELECT name, COUNT(candySales.kidId) " +
                "FROM kids " +
                "LEFT JOIN candySales " +
                "ON kids.id = candySales.kidId " +
                "GROUP BY kids.id;",
                function(tx, results) {
    var display = document.getElementById("purchaseList");
    var rows = results.rows;
    for (var index = 0; index < rows.length; index++) {
      var item = rows.item(index);
      display.textContent += ", " + item.name + "bought " +
                             item.count + "pieces";
    }
  });
});

IndexedDB

candyEaters = [];
function displayCandyEaters(event) {
  var display = document.getElementById("purchaseList");
  for (var i in candyEaters) {
    display.textContent += ", " + candyEaters[i].name + "bought " +
                           candyEaters[i].count + "pieces";
  }
};

var request = window.indexedDB.open("CandyDB",
                                    "My candy store database");
request.onsuccess = function(event) {
  var db = event.result;
  var transaction = db.transaction(["kids", "candySales"]);
  transaction.oncomplete = displayCandyEaters;

  var kidCursor;
  var saleCursor;
  var salesLoaded = false;
  var count;

  var kidsStore = transaction.objectStore("kids");
  kidsStore.openCursor().onsuccess = function(event) {
    kidCursor = event.result;
    count = 0;
    attemptWalk();
  }
  var salesStore = transaction.objectStore("candySales");
  var kidIndex = salesStore.index("kidId");
  kidIndex.openObjectCursor().onsuccess = function(event) {
    saleCursor = event.result;
    salesLoaded = true;
    attemptWalk();
  }
  function attemptWalk() {
    if (!kidCursor || !salesLoaded)
      return;

    if (saleCursor && kidCursor.value.id == saleCursor.kidId) {
      count++;
      saleCursor.continue();
    }
    else {
      candyEaters.push({ name: kidCursor.value.name, count: count });
      kidCursor.continue();
    }
  }
}

IndexedDB は基本的にデータベースのプログラミングモデルを簡単にし、幅広いユースケースに使えます。WG はこの API を、ライブラリでラップできるようにデザインしました。例えば CouchDB のようなスタイルの API を上に重ねたりする余地は十分にあります。WebDatabase のような SQL ベースの API を IndexedDB 上に作ることも十分に可能です。仕様がまだ凍結していないこともあり、Mozilla は IndexedDB へのフィードバックを熱心に集めています。遠慮なくコメントしたり Rypple で匿名投稿してください。

2 件のコメント

  1. Seta :

    私の勘違いでしたら恐縮ですが、例3の原文”you can also stop iterating data with IndexedDB by simply not calling cursor.continue().”のところは、「cursor.continue() を呼ばないことで繰り返しを止めることができます」と言っているような気がします。
    ※ Indexed Database API 周辺の解説が、私にはちんぷんかんぷんだったので訳していただいて助かりました。

  2. edvakf :

    訳者です。ご指摘どうもありがとうございます。直しました。