Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

IE10で動くHTML5アプリ実装例
「Indexed Database APIを使用したフィード取得アプリ」

最新ブラウザ「Internet Explorer10」と「HTML5」のAPI情報(2)

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2012/04/18 14:00

 本連載は、「Developers Summit 2012」(デブサミ2012)において、2月16日に行われた日本マイクロソフト株式会社 春日井 良隆氏によるセッション「次期Internet Explorer、IE10とHTML5 API」(セッション資料はこちら)をもとに、Internet Explorer 10の新機能やHTML5 APIとの関連について、3回に分けて特集します。第2回目の今回は、Indexed Database APIを使用し、フィードリーダーアプリを作成してみましょう。

はじめに

 Internet Explorer 10(以下、IE10)ではIndexed Database API(以下、IndexedDB)が初めて実装されました。IndexedDBは現在W3CのWorking Draft(英語)として公開されています。

 IndexedDBとはブラウザ内で動作するNoSQLデータベースで、JavaScriptのオブジェクトをそのまま保存、または読みだすことができます。IndexedDBの最大の特徴は、保存されたJavaScriptのプロパティ値に対するインデックスを持つことができ、オブジェクトの高速な検索が可能となっています。また、オフラインの状態でもデータの読み書きを行うことができ、ブラウザを終了してもデータは消去されないことから、デスクトップアプリと比べても遜色が無いようなオフラインWebアプリケーションを作成することも可能です。

 今回の記事では、RSSフィードリーダー「IDB Feed Reader」を題材として、IndexedDBの機能をおおまかに見ていくことにします(このデモは、Developers Summit 2012のセッション「次期Internet Explorer、IE10とHTML5 API」で使用したものと同じです)。

IndexedDBを使用したRSSフィードリーダーのデモのスクリーンショット
IndexedDBを使用したRSSフィードリーダーのデモのスクリーンショット
記事をクリックすると内容が表示されます
記事をクリックすると内容が表示されます

 このサンプルにアクセスすると、登録されているURLからフィードを読み込んで、すべてのフィードをIndexedDBに保存します。以降は既読や未読の管理、スターの付け外しなどの処理は全てIndexedDBを対象として行われるため、同じ処理をサーバに対して行うアプリケーションに比べて、非常に高速に動作します。

 では、以降はサンプルからコードを引用しながら、IndexedDBプログラミングについて全体的な解説を行います。このデモンストレーションのソースコードは、こちらからダウンロードすることができます。

IndexedDBを初期化する

 IndexedDBは、「indexedDB」というグローバル変数を起点としてプログラミングを開始します。また、IndexedDBを用いたプログラミングには、他にもIndexedDBにおけるトランザクションを表す「IDBTransaction」、データを順次フェッチするためのカーソルを表す「IDBCursor」、検索インデックスの範囲を絞り込むための「IDBKeyRange」などのインターフェースを必要とします。

 しかしこの記事の執筆時点では、これらのグローバル変数/インターフェースは、多くのブラウザにおいて異なる「ベンダープレフィックス」付きで提供されています。そのため、出来る限り多くのブラウザでこのデモが動作するように、ベンダープレフィックスを総当りで試して、見つかったものを使用するという処理を行っています。

var indexedDB = window.indexedDB || window.webkitIndexedDB
        || window.mozIndexedDB || window.moz_indexedDB
        || window.msIndexedDB;
var IDBCursor = window.IDBCursor || window.webkitIDBCursor
        || window.mozIDBCursor || window.moz_IDBCursor
        || window.msIDBCursor;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction
        || window.mozIDBTransaction || window.moz_IDBTransaction
        || window.msIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange
        || window.mozIDBKeyRange || window.moz_IDBKeyRange
        || window.msIDBKeyRange;

 IndexedDBを使用するには、まずデータベースをオープンしなくてはなりません。データベースをオープンするためのメソッドはopenですが、このメソッドはすぐに処理が呼び出し元に戻り、実際のオープン処理はバックグラウンドで実行されます。

var req = indexedDB.open(DB_NAME, latestVersion);

 IndexedDBには、どんな環境でも使用できる非同期型のAPIと、Web Workers内でのみ使用可能な同期型のAPIがありますが、このデモでは非同期型のAPIを使用しています。IndexedDBでは、非同期で呼び出されるメソッドの戻り値に対して「onsuccess」「onerror」と言ったイベントハンドラを設定することで、非同期処理の終了時に処理を行うことができます。また、非同期処理の結果は、非同期メソッドの戻り値が持つ「result」プロパティに格納されています。

var db;
req.onsuccess = function(event) {
    // オープンしたデータベースはresultプロパティに格納されている
    db = req.result;
    ...
};

 上のように、オープンに成功するとresultプロパティにデータベースオブジェクト(正確にはIDBDatabaseオブジェクト)が格納されています。

データベースのアップグレード

 IndexedDBがわかりにくい理由の一つに、データベーススキーマの「アップグレード」処理をプログラム中に書かなければならない点が挙げられます。IndexedDBはJavaScriptオブジェクトを読み書きすることができ、「NoSQL」を標榜しているDBのほとんどがそうであるように、静的なスキーマを必要としません。スキーマを持たないのに、なぜ「スキーマのアップグレード」という処理が必要なのでしょうか?

 IndexedDBにおけるJavaScriptオブジェクトの「入れ物」は「オブジェクトストア」と呼ばれ、RDBで言うところの「テーブル」に近い概念です。1つのオブジェクトストアには、どんな種類のJavaScriptオブジェクトでも格納するができますが、多くの場合は「構造がほぼ同一の」オブジェクトを読み書きすることになります。それらのオブジェクトを識別するための値は「キー」と呼ばれます。

 また、オブジェクトストアにはJavaScriptのプロパティ値に対して「インデックス」を生成することができ、プロパティ値で検索結果を絞り込むことが可能です。

 このように、IndexedDBは「オブジェクトストア」「インデックス」と言った特別な内部構造を持つため、時にデータベースを「アップグレード」して、それらを追加/削除する必要が生じるのです。

 前置きが長くなりましたが、アップグレードに関する処理を解説していきます。

 アップグレード処理は、IndexedDBのオープン直後に行わなくてはなりません。openメソッドの引数には「名前」と「バージョン番号」を指定しますが、バージョン番号が現在のDBと異なる場合、openの結果はupgradeneeded(DBのアップグレードが必要)というイベントを発生させることになります。このイベントハンドラが終了するまでの間が「アップグレード可能な」期間です。このタイミングを逃すと、データベースのスキーマ変更を行うことはできなくなります。

 オブジェクトストアを作る、または削除するためのメソッドは、データベースオブジェクトが保持しています。

db.createObjectStore(dbName, options)
db.deleteObjectStore(dbName)

 createObjectStoreの第二引数には、キーとなるプロパティの名前や「IDを整数値の自動採番によって生成するか」を指定できます。例えば、キープロパティの名前を"id"、IDを自動採番する場合は以下のようなコードになります。

var store = db.createObjectStore("TestStore", { keyPath: "id", autoIncrement: true});

 createObjectStoreの戻り値は、オブジェクトストアを表すオブジェクトです。オブジェクトストアは以下のようなメソッドを持ち、インデックスの生成と削除を行うことができます。

store.createIndex(name, keyPath, options)
store.deleteIndex(name)

 createIndexの引数は「インデックスの名前」「インデックスを設定するプロパティのパス」「unique(インデックス中の値がユニークならばtrue)、multientry(インデックス中の値が配列ならばtrue)と言った設定を行うためのオプション」となります。

 今回のデモにおいては、これらのメソッドを以下のように用いています。

 まず、データベースのスキーマが変更された時は、以前のオブジェクトストアを削除して作りなおすために、以下のようなメソッドを用意しました。

function recreateObjectStore(db, storeName, createOpts) {
    var storeNames = db.objectStoreNames;
    // 既ににオブジェクトストアが存在すれば削除
    for ( var i = 0, n = storeNames.length; i < n; i++) {
        if (storeNames[i] === storeName) {
            db.deleteObjectStore(storeName);
        }
    }
    // オブジェクトストアの作成
    var store = db.createObjectStore(storeName, createOpts);
    return store;
}

 データベースをオープンする際に指定したバージョン番号が現在のDBと食い違い、アップグレードが必要な場合は、`upgradeneeded`イベントが発生します。

var req = indexedDB.open(DB_NAME, latestVersion);

req.onupgradeneeded = function(event) {
    db = req.result;
    var currentVersion = event.oldVersion;
    // 現在のバージョンと最新バージョンの間に差異があったら、
    // アップグレード処理を実行
    if (currentVersion != latestVersion) {
        // 順に呼び出し
        upgrade(currentVersion, latestVersion, callback);
    } else {
        typeof callback == "function" && callback();
    }
};

 アップグレード時の処理は以下のようになります。フィードの情報と、フィードに含まれるエントリ情報は、それぞれ異なるオブジェクトストアを定義します。また、それぞれに対して検索やソートに用いるプロパティにインデックスを設定しています。

// URLを主キーとしてエントリを保存するオブジェクトストアを作成
var feedStore = recreateObjectStore(db, FEED_STORE, { keyPath : "feedUrl" });
// フィードの更新日時に対してインデックスを作成
feedStore.createIndex("date", "date");

// フィードエントリを保存するオブジェクトストア
var feedEntryStore = recreateObjectStore(db, FEED_ENTRY_STORE, { keyPath : link" });
// エントリの更新日時と、フィードのURLに対してインデックスを作成
// 更新日時は一覧のソートに、フィードURLはフィードごとの絞り込みの際に用いられる
feedEntryStore.createIndex("date", "date");
feedEntryStore.createIndex("feedUrl", "feedUrl");

 実際のコードでは、スキーマの今後のバージョンアップに備えて、バージョンごとのアップグレード処理をカプセル化した関数の配列を用意し、その配列をループすることでアップグレードを実現しています。興味がある方はコードを参照してください。

オブジェクトストアの操作

 データベースのオープン(またはアップグレード)に成功したあとは、一般的なデータ処理(RDBであればSELECT/INSERT/UPDATE/DELETE)を行なってアプリケーションを実現していくことになります。

 データ処理の起点となるのは、トランザクションの開始です。トランザクションを開始するには、データベースオブジェクトが持つtransactionメソッドを使用します。

tx = db.transaction(storeNames, mode)

 このメソッドの第一引数は、トランザクション中に操作対象となるオブジェクトストアの名称(配列により複数指定可能)です。また、第二引数のmodeは、IDBTransaction.READ_ONLY(読み取り専用)かIDBTransaction.READ_WRITE(読み書き可能)のいずれかを指定します。

 戻り値は、トランザクションを表すIDBTransactionオブジェクトです。このオブジェクトが持つobjectStoreメソッドを呼び出すと、オブジェクトストアを取得できます。

store = tx.objectStore(storeName)

 オブジェクトストアに対してJavaScriptオブジェクトを読み書きするために使用できる、主なメソッドを以下に示します。

  • get(value, key):キーを指定して値を取得
  • put(value, key):キーを指定して値を挿入または置換
  • delete(key):キーを指定して削除

 これらのメソッドは全て非同期で動作するため、結果を取得するには、DBのオープン時と同様successerrorといったイベントを処理する必要があります。以下に、getputを使用しているコードをサンプル中から抜粋します。

// スターの付け外しを行うために、フィードエントリをDBから読みだして状態を変更する
// トランザクションの開始とオブジェクトストアの取得
var objectStore = db.transaction([FEED_ENTRY_STORE], IDBTransaction.READ_WRITE).objectStore(FEED_ENTRY_STORE);
// URLをキーとして、エントリを取得する
var req = objectStore.get(url);
// オブジェクトの取得に成功
req.onsuccess = function(e) {
    // フィードエントリを取得
var entry = req.result;
// スターの状態を反転
entry.star = !entry.star;
// エントリを保存
var request = objectStore.put(entry);
request.onsuccess = function() {
    document.getElementById('star_'+entry.url).src = entry.star ? MARK_ON : MARK_OFF;
    };
request.onerror = console.log;
};

カーソルを使用したオブジェクトの読み出し

 オブジェクトストアからデータを取得するには、キーを指定してオブジェクトを取得する(getメソッド)ほかにも、キーやインデックスから範囲検索を行うことで、複数のオブジェクトを一気に取得することもできます。検索結果は「カーソル」という形で表現され、結果の先頭から末尾までを順に走査していくことができます。

 検索をおこなってカーソルを取得するには、openCursorというメソッドを使用します。

  • openCursor(range, direction):指定された範囲(range)のデータを取得します。また、directionで取得順序を指定したり、両方指定しないことで全データの取得ができます。実際の値を取り出すには、openCursorで取り出したオブジェクトをonsuccessコールバック内でcontinue()して全列回して取得します。

 サンプル内でopenCursorを使用しているコードを抜粋します。ポイントはcurReq.onsuccessが複数回呼ばれること、カーソルの終了条件を変数cursorがnullかどうかで判定していることです。

// カーソルをオープンする
var curReq = feedEntryStore.openCursor();
// カーソルのオープン、もしくはcontinueに成功
curReq.onsuccess = function() {
    var cursor = curReq.result;
    // カーソルが終了した
    if (!cursor) {
        typeof callback == "function" && callback(results);
        return;
    }
    // カーソルから値を取り出す
    var entry = cursor.value;
    results.push(entry);
    // カーソルを次に進める
    cursor.continue();

};

検索の範囲を指定する

 範囲を絞って検索を行うには、openCursor()の第1引数にIDBKeyRangeのオブジェクトを渡します。IDBKeyRangeは、以下のようなファクトリメソッドを用いてオブジェクトを生成します。

  • upperBound(bound, open):boundで指定した範囲よりも「小さい」範囲を表す。範囲にbound自身を含むかどうかをopenで指定
  • lowerBound(bound, open):boundで指定した範囲よりも「大きい」範囲を表す。範囲にbound自身を含むかどうかをopenで指定
  • bound(lower, upper, lowerOpen, upperOpen):下限と上限を同時に指定
  • only(value):範囲ではなく、固定値を指定

 サンプル中では、エントリ一覧のページング処理(スクロール位置が下に達すると、次のデータを読み込みます)を行う際に、範囲を指定した検索を行なっています。エントリ一覧は常に更新日時の降順でソートされています。次のページを読み込む際には、前回の検索結果の中で「一番古い更新日時」を上限とした範囲指定を行い、検索を行います。

// baseTimeは、前回の検索結果の中で一番古い更新日時
var range = IDBKeyRange.upperBound(baseTime, false);

 この範囲オブジェクトを指定して、インデックスから値を読み出します。

var curReq = feedEntryStore.index("date").openCursor(range, IDBCursor.PREV);

 また、openCursorの第二引数(取得順序)に指定する値は、IDBCursorインターフェースに宣言されている定数です。

  • NEXT:昇順
  • NEXT_NO_DUPLICATE:昇順(重複なし)
  • PREV:降順
  • PREV_NO_DUPLICATE:降順(重複なし)

 上のコードでは、降順でデータを取得するためにIDBCursor.PREVを指定しています。

 その他、IDBCursorオブジェクト使用できるメソッドやプロパティはこちら(英語)をご覧ください。

まとめ

 IndexedDBは非同期のAPIが多少扱いづらく、また、現時点ではブラウザ間の差異も大きいため、利用するには一定の慣れを必要とします。

 しかし、IndexdDBを使用することで、ネイティブアプリにも負けない機能性と操作感を持つWebアプリケーションを実装することが可能になります。ぜひサンプルをお試しになり、IndexedDBアプリケーションのきびきびとした動作を体感してください。

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 石川 将行(イシカワ マサユキ)

    株式会社シーエー・モバイルにてスマートフォン向けWebアプリケーションの開発を担当、現在はグリー株式会社でエンジニアとしてソーシャルアプリの開発を行なっている。

All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5