コンテンツにスキップするには Enter キーを押してください

IndexedDBに関する覚書 (2011年12月版)

僕の名刺には肩書きに「主任研究員」って書いてあるんですけど、実際のところは作成時のノリとシャレで決めた肩書きであって社内的な意味は全くありません。
でもまぁ、せめて名前負けしないように、ネタ的なサービス・アプリばかり作ってるわけじゃないことをアピールしようということで、たまにはマジメな技術記事でも書くことにします。

さて。

最近お仕事で、広義のHTML5と呼ばれるところのJavaScript API、特にIndexed Database API(以下、IndexedDB)を触っています。

このIndexedDBはクライアントサイド(ブラウザ)にNoSQLのDBを持てる仕組みで、現在のところFirefox(4~)とChrome(11~)で実装されており、気になるIEも次期バージョンの10から実装されることになっています。

ちなみにWeb SQL DatabaseというこちらはSQLiteベースの別の仕組みもあり、こちらはChromeやSafari、Opera(10.5~)などで実装されていたのですが、こちらは諸事情により2010年11月に仕様策定が断念されてしまいました。
そんなわけで今後、Web SQL DatabaseがIEやFirefoxで実装されることはありません。
とはいえ、モバイル環境ではMobileSafariやAndroidBrowser、MobileOperaといった主要ブラウザで実装されている一方、IndexedDBはどのブラウザでもまだ実装されていないため、モバイル環境に限って言えば今後しばらくはWeb SQL Databaseが活躍する機会はあるでしょう。

さて、クライアントサイドにDBを持てると何が嬉しいのかというと、例えばGmailのようなメールアプリケーションならば予めメールデータをダウンロードしてDB内に保存しておいて、以降はそこからデータを参照するようにすればオフライン環境になってもメールを読むことができる、といったような使い方が考えられます。

ところで、IE10で利用できるということは、Windows8のメトロスタイルアプリでも利用できるということでもあります。
その証拠に、MSDNメトロスタイルアプリのデベロッパーセンターの中にもIndexedDBのドキュメントが存在しています。
IndexedDBはWindowsアプリを作る上でも重要な技術になっていくことが期待できますね。

・・・が。

このIndexedDBはいまもまだワーキングドラフト(WD)の段階であり、仕様がちょくちょく変わっています。
最近も、つい先日12月6日に新しいWDが公開されたところです。
そのため、同じように「対応ブラウザ」とされていても、バージョンによって実装が違っていたりします。

Web上にもいくつかチュートリアル的な記事があるのですが、最近のバージョンのブラウザでは動作しなくなってしまったものがほとんどだったり…(-_-;)

そんなわけで、先日公開されたWDを基準として、今リリースされている最新版およびプレビュー版のブラウザで動くIndexedDBの基本的な使い方のサンプルコードとその説明をまとめました。以前の仕様からソースコードの書き方が変わった場合はその違いについても触れています。

はじめにおことわり

この記事は2011年12月12日から16日の間までに執筆しました。
記事閲覧の時期によっては既に仕様や実装が変更されていることも考えられますので、その点ご容赦ください。

対象読者

JavaScriptとRDBの基本的な知識があることを前提とします。
MongoDBやGoogleAppEngineのデータストアなど、他のNoSQLのDBの知識があるとなお良いです。

ライセンス

この記事自体はCC-BYライセンスとします。引用等される場合はクレジット表記だけお願いします。

クリエイティブ・コモンズ・ライセンス
IndexedDBに関する覚書 (2011年12月版) by mzsm is licensed under a Creative Commons 表示 3.0 非移植 License.

この記事に記述しているサンプルコードはNYSLライセンス(ver.0.9982)とします。
簡単に言うとご自由にどうぞお使いくださいというライセンスです。ソースコードの引用に関してはクレジット表記は必要ありません。

動作環境

サンプルコードの動作確認は執筆日現在リリースされているChromeとFirefoxの最新版(Chrome16、Firefox8)およびプレビュー版(Chrome18(canary)、Firefox Aurora 10a2、Firefox Nightly 11a1)で行いました。
Internet Explorer 10 Platform Preview 3については動作環境を持っていないため確認できませんでした。
なお、各ブラウザで付与されているベンダープリフィックスの差異を吸収するため、以下の共通コードをサンプルコードの直前に含んでいるものとします。

//共通コード
window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
window.IDBKeyRange = window.IDBKeyRange|| window.webkitIDBKeyRange || window.msIDBKeyRange;
window.IDBCursor = window.IDBCursor || window.webkitIDBCursor || window.msIDBCursor;

用語

IndexedDBというかNoSQLの概念についての説明は省略します(他のサイトにちゃんとした説明があると思いますので)。
ここではIndexedDBを説明していく上で出てくる単語について、他のDBでは何に相当するものかを表で示しておきます。

IndexedDB RDB MongoDB Datastore(GAE)
オブジェクトストア(object store) 表(テーブル/table) コレクション(collection) 種類(kind)
レコード(record) 行(row) ドキュメント(document) エンティティ(entity)
プロパティ(property) 列(column) フィールド(field) プロパティ(property)

厳密に言うと違う部分もありますが、こんなイメージだとざっくりと理解しておいてください。

DBへの接続

ではここから実際のコードを使って説明していきます。
まず、2011年4月のWDの仕様(以前の仕様)でDBへ接続するには次のように書きます。

//Chrome16, Chrome18(canary), Firefox 8 で動作します
var dbName = 'mzsmTest'; //データベース名
var idb;

var dbConnect = indexedDB.open(dbName);
console.log(dbConnect); //-> {IDBRequest}
dbConnect.onsuccess = function(e){
    idb = e.target.result; //IDBDatabaseオブジェクト
    console.log('DB接続完了');
};
dbConnect.onerror = function(e){
    console.log('DB接続失敗', e);
};

一方、2011年12月のWDの仕様(新しい仕様)ではこうなります。

//Firefox Aurora 10a2, Firefox Nightly 11a1で動作します
var dbName = 'mzsmTest'; //データベース名
var dbVersion = 20111212; //バージョン番号
var idb;

var dbConnect = indexedDB.open(dbName, dbVersion);
console.log(dbConnect); //-> {IDBOpenDBRequest}
dbConnect.onsuccess = function(e){
    idb = e.target.result; //IDBDatabaseオブジェクト
    console.log('DB接続完了');
};
dbConnect.onerror = function(e){
    console.log('DB接続失敗', e);
};

どちらのコードでもindexedDB.openメソッドを叩いて接続するのですが、引数が少し変わっています。

以前の仕様では引数は1つで、DBの名前を指定します。
2010年8月のWDの仕様(さらに古い仕様)では第2引数にDBの説明文が必要でしたが、廃止されました(JavaScriptでは必要以上の引数を渡しても特に問題はないので、第2引数が残っていてもそのまま動作します)。
新しい仕様では第2引数が復活し、今度はDBのバージョン番号を指定するようになりました。

また、以前の仕様では返り値としてIDBRequestオブジェクトが返ってきていたのが、新しい仕様ではIDBOpenDBRequestオブジェクトが返ってくるようになりました。

そのオブジェクトのonsuccessonerrorの各プロパティに、成功時と失敗時のコールバック関数を設定する点は同じです。

バージョン変更・アップグレード

DBに接続しただけではデータの操作はできません。
データの置き場、RDBでいうところの表に相当するオブジェクトストアと、データを検索するためのインデックスを作成する必要があります。

IndexedDBではDBに対するすべての操作はトランザクションの内部で行います。
MySQLのようにトランザクションなしで操作することは出来ません。
トランザクションには読み込み専用(READ_ONLY)、読み書き可能(READ_WRITE)、バージョン変更(VRESION_CHANGE)の3つの種類があり、状況によって使い分けるのですが、オブジェクトストアやインデックスの作成と削除は3つ目のバージョン変更のトランザクション内部でのみ実行できます。

そういうわけで、バージョン変更トランザクションを呼び出すのですが、その呼び出し方が新しい仕様では大きく変わっています。

まず以前の仕様でのコードです。

//Chrome16, Chrome18(canary), Firefox 8 で動作します
var dbName = 'mzsmTest'; //データベース名
var dbVersion = '2011.12.12'; //バージョン名
var idb;

var dbConnect = indexedDB.open(dbName);
console.log(dbConnect); //-> {IDBRequest}
dbConnect.onsuccess = function(e){
    var successCallback = function(){
        console.log('DB接続完了');
        /*
         * 接続完了後の処理(省略)
         */
    }

    idb = e.target.result; //IDBDatabaseオブジェクト
    //バージョン確認
    var currentVersion = idb.version;
    if(currentVersion !== dbVersion){
        //バージョン変更
        var setVersion = idb.setVersion(dbVersion);
        setVersion.onsuccess = function(e){
            var tx = e.target.transaction; //{IDBTransaction} e.target.resultも同内容
            var oldVersion = currentVersion; //変更前のバージョン
            /*
             * オブジェクトストアの作成などバージョン変更に伴う処理(省略)
             */
            successCallback();
        };
        setVersion.onerror = function(e){
            console.log('バージョン変更失敗', e);
        };
    }else{
        successCallback();
    }
};
dbConnect.onerror = function(e){
    console.log('DB接続失敗', e);
};

オブジェクトストアやインデックスを作成する部分については後で改めて説明するため、ここでは省略しています。

onsuccessコールバックの内部で、DBに設定されているバージョンとソースコードに書かれたバージョンを比較し、違っていればIDBDatabase.setVersionメソッドを叩きます。
さらにその戻り値に対してonsuccessとonerrorのコールバック関数を設定し、onsuccessコールバックの内部でバージョン変更に伴う処理を行います。

これが新しい仕様ではこうなります。

//Firefox Aurora 10a2, Firefox Nightly 11a1で動作します
var dbName = 'mzsmTest'; //データベース名
var dbVersion = 20111212; //バージョン番号
var idb;

var dbConnect = indexedDB.open(dbName, dbVersion);
console.log(dbConnect); //-> {IDBOpenDBRequest}
dbConnect.onsuccess = function(e){
    idb = e.target.result; //{IDBDatabase}
    console.log('DB接続完了');
    /*
     * 接続完了後の処理(省略)
     */
};
dbConnect.onerror = function(e){
    console.log('DB接続失敗', e);
}
dbConnect.onupgradeneeded = function(e){
    //バージョン変更
    idb = e.target.result; //{IDBDatabase}
    var tx = e.target.transaction; //{IDBTransaction}
    var oldVersion = e.oldVersion; //変更前のバージョン
    //ちなみに e.newVersion には変更後のバージョンが入っています
    /*
     * オブジェクトストアの作成などバージョン変更に伴う処理(省略)
     */
};

だいぶすっきりとしました。

新しい仕様では先述のように、DBへの接続を開くときにバージョンを指定します。
なお、以前の仕様ではバージョンは文字列(DOMString)だったのですが、新しい仕様では整数(unsigned long long int)に変更されています。
また、新しい仕様ではDBに設定されているバージョンよりも小さいバージョン番号を指定するとエラーが発生します。
バージョンを変更する場合には、以前のバージョンよりも大きくなるように変更していきましょう。

indexedDB.openの戻り値IDBOpenDBRequestのonupgradeneededプロパティにバージョン変更時のコールバック関数を指定します。

以前の仕様では接続完了時の処理を関数内関数で定義して、バージョン変更がなければすぐに、バージョン変更を行うときは処理完了後に呼び出していましたが、新しい仕様ではonupgradeneededコールバック終了後に続けてonsuccessコールバックが呼び出されるため、特にそのようにする必要はありません。

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

先ほど省略していた部分です。
前述のコードのバージョン変更トランザクション内の省略部分に次のコードが当てはまっているものと考えてください。

ここも新しい仕様では変わっている点があるのですが、この仕様変更については今のところどのブラウザでも実装されていません。(そのため新仕様に対応したサンプルコードはありません)

//Chrome16, Chrome18(canary), Firefox 8, Firefox Aurora 10a2, Firefox Nightly 11a1で動作します
// {IDBDatabase} idb データベース
// {DOMString} oldVersion 変更前のバージョン

//共通化のため条件を2つ書いていますが、
//バージョンがDOMStringであるChrome16, Chrome18(canary), Firefox 8では前者、
//整数であるFirefox Aurora 10a2, Firefox Nightly 11a1では後者のみ機能します
if(oldVersion === '' || oldVersion === 0){
    var name = 'Users';
    var optionalParameters = {
        keyPath: 'userId',
        autoIncrement: false
    };
    var store = idb.createObjectStore(name,  optionalParameters);
    console.log(store); //-> {IDBObjectStore}
    /*
     * インデックス作成処理(省略・後述)
     */
}else{
    /*
     * バージョンアップに伴う処理(省略)
     */
}

IDBDatabase.createObjectStoreメソッドを叩くとオブジェクトストアが作成できます。
戻り値はIDBObjectStoreオブジェクトです。これはこの後インデックスを作成するときに利用します。

第1引数nameはオブジェクトストア名、第2引数optionalParametersはオプションパラメータを含むオブジェクトです。
なお、新しい仕様では第2引数optionalParametersがObject型オブジェクトからIDBObjectStoreParametersオブジェクトへ変更になりますが、先ほど説明したとおりまだどのブラウザでも実装されていません。

optionalParameters.keyPathはその名の通りキー(key)の場所(path)です。
キーはオブジェクトストアの中で各レコードを識別するためのもので、オブジェクトストア毎にユニークである必要があります。
IndexedDBのレコードはObjectオブジェクトの形で格納されるのですが、keyPathで指定した名前のプロパティの値がレコードのキーとなります。例えばサンプルコードの場合は、userIdというプロパティの値がキーになります。RDBで、どの列を主キーに設定するかと同じようなものだと考えるとわかりやすいかと思います。この場合、データオブジェクトの中にキーが存在するため、in-lineキーと呼びます。
省略するかnullを指定した場合はデータとは別にキーを指定する形になります。この場合はキーがデータオブジェクトの外に存在するため、out-of-lineキーと呼びます。

optionalParameters.autoIncrementにtrueを指定した場合は、RDBで主キーにAUTO INCREMENTを指定した場合と同じように、キーを省略したときにIndexedDB側で自動的にキーを付与してくれます。もちろん自分で明示的に設定することも出来ます。
falseを指定した場合は必ず自分で明示的に設定する必要があります。

インデックスの作成

先ほどと同じように、オブジェクトストア作成のサンプルコードの省略部分です。

IndexedDBの名前の由来でもあるインデックスを作成します。
RDBのそれと同じように、オブジェクトストアに格納されているレコードを、特定のプロパティの値で検索するための索引です。

RDBではインデックスを張っていない列に対して検索をかけることもできますが、IndexedDBではインデックスを張っていないプロパティに対して検索をかけることはできません。
正確に言えば、全てのデータを走査して条件に当てはまるデータだけを抜き出すという力技もできるのですが。(RDBでのインデックスを張っていない列に対する検索と同じです)

オブジェクトストアの作成時と同じくまだどのブラウザでも実装されていない仕様変更があるのですが、これに対応したサンプルコードはありません。

//Chrome16, Chrome18(canary), Firefox Nightly 11a1で動作します
// {IDBObjectStore} store オブジェクトストア
var name =  'userNameIndex';
var keyPath = 'userName';
var optionalParameters = {
    unique: false,
    multiEntry: false //Firefox10以前ではこの行を削除しないと動きません
};

var idx = store.createIndex(name, keyPath, optionalParameters);
console.log(idx); //-> {IDBIndex}

IDBObjectStore.createIndexメソッドを叩くとインデックスを作成できます。
第1引数nameはインデックス名で、任意の名前を付けられます。
第2引数keyPathはオブジェクトストア作成時に指定したkeyPathと同じようなもので、どのプロパティの値を使ってインデックスを作成するかを指定します。
サンプルコードではインデックス名としてkeyPathのプロパティ名と違う名前を指定していますが、同じ名前にしてももちろん問題ありません。(ていうかそのほうが検索するときにわかりやすいかもしれません)
第3引数optionalParametersはオプションパラメータを含むオブジェクトです。
なお、新しい仕様では第2引数optionalParametersがObject型オブジェクトからIDBIndexParametersオブジェクトへ変更になりますが、やはりこちらもまだどのブラウザでも実装されていません。

optionalParameters.uniqueはRDBのUNIQUE制約と同じで、trueの場合は対象プロパティの値が同じオブジェクトストアの中で重複してはいけないという制約が付けられます。これに反するデータを保存しようとすると失敗します。
optionalParameters.multiEntryは、対象プロパティの値が配列だった場合のインデックス化のしかたを指定します。詳しくはこの後説明します。
FirefoxではoptionalParametersのオブジェクト内に不要なプロパティが含まれているとエラーになるので、Firefox10以前で試す場合は削除してください。Chromeでは不要なプロパティは無視されます。

ちなみに、multiEntryは古い仕様ではmultirowという名前でした。
また、12月のWDではmultientryと書かれていますが、実際にはさらに変更されてmultiEntryになっています。(参考:Bug 15029)
Windows8 Developer Previewやその上で動くIE10PP4では変更前の仕様に従いmultientryとして実装されているようですが、こちらも次のリリースでは変更されるでしょう。

multiEntry

Chromeではver.17から、Firefoxではver.11からmultiEntryフラグが実装されています。

multiEntryフラグがtrueの場合は、対象のプロパティが配列であれば、配列の各要素がインデックスに追加されます。
multiEntryフラグがfalseの場合は、その配列自体がインデックスに追加されます。
デフォルト値はfalseで、省略した場合はこちらになります。

これだけでは理解しづらいので具体的な例で説明しましょう。
multiEntryフラグをtrueにして利用する例としては、記事に対する“タグ”があげられます。
タグとは、記事などについて特徴を表す短いキーワードを設定することで検索をしやすくするアレのことです。
RDBであればタグは記事とは別テーブルにする必要がありますが、IndexedDBでは配列が格納できるのでその必要はありません。1件の記事のデータはこのように表現できます。

{
    id: 12345,
    title: '記事タイトル',
    body: '本文本文本文本文本文本文...',
    tags: ['HTML', 'JavaScript', 'PHP']
}

このとき、tagsプロパティを対象にmultiEntryフラグがtrueのインデックスが作成されていれば、HTMLで検索してもJavaScriptで検索してもPHPで検索してもこの記事がヒットします。
もしもmultiEntryフラグがfalseだった場合は、['HTML', 'JavaScript', 'PHP']という配列で検索しないとこの記事はヒットしません。これではタグとして機能しませんね。

一方、multiEntryフラグをfalseにして配列をインデックスに追加する使い方として、複合インデックスがあるのですが、これについてはデータ検索の項目で説明します。


ここまでで、とりあえずデータを保存したり取得したりするための準備はできました。
サンプルコード中の、バージョンアップに伴う処理の部分は今回の記事では説明しません。
アプリケーションの運用を開始してからオブジェクトストアやインデックスを追加あるいは削除する必要がでてきたとき、初めてアプリケーションを利用するユーザーと、これまで利用してきたユーザーとでDBの状態に不整合が起きないようにするための処理を書きます。
テスト中は、バージョンを変更するごとに毎回全てのオブジェクトストアを一旦削除してから改めて作りなおすほうが楽でしょう。

というわけでここからはデータの追加、取得と更新について説明していきます。
ここから後に紹介するサンプルコードは全て、DBへの接続が完了したあとに実行されるものとします。
これまでのサンプルコードでいう「接続完了後の処理(省略)」の部分に当てはめてください。

トランザクション

先ほども書きましたが、IndexedDBではDBに対するすべての操作はトランザクションの内部で行います。
3つあるトランザクションの種類のうち、バージョン変更(VRESION_CHANGE)トランザクションだけは最初のほうで説明したように特殊な呼び出し方をしましたが、残り2つの読み込み専用(READ_ONLY)と読み書き可能(READ_WRITE)は次の方法で呼び出します。
接続が完了してからは、ほぼこちらしか使いません。

// {IDBDatabase} idb 接続中のデータベースオブジェクト
var storeNames = ['Users'];
var mode = IDBTransaction.READ_ONLY;
//var mode = IDBTransaction.READ_WRITE; (書き込みもする場合)

var tx = idb.transaction(storeNames, mode);
console.log(tx); //-> {IDBTransaction}

トランザクションを開始するには、IDBDatabase.transactionメソッドを叩きます。
第1引数storeNamesはトランザクションの範囲の指定です。そのトランザクション中で操作する対象のオブジェクトストア名を配列で列挙します。対象のオブジェクトストアが1つだけの場合は配列にせず、文字列で指定することも出来ます。

// {IDBDatabase} idb 接続中のデータベースオブジェクト
var tx = idb.transaction('Users', IDBTransaction.READ_ONLY);

以前の仕様では、オブジェクトストアを指定しない場合(全てのオブジェクトストアを対象とする場合)は第1引数に空の配列([])を指定することができました。しかし現在は空配列を指定するとエラーになります。
全てのオブジェクトストアを対象としたい場合は、IDBDatabase.objectStoreNamesプロパティを使いましょう。
IDBDatabase.objectStoreNamesは、そのDBに作成されているすべてのオブジェクトストア名が格納されているプロパティです。

// {IDBDatabase} idb 接続中のデータベースオブジェクト
var tx = idb.transaction(idb.objectStoreNames, IDBTransaction.READ_ONLY);

第2引数modeは読み込み専用(READ_ONLY)と読み書き可能(READ_WRITE)のどちらのモードで開くかを指定します。IDBTransactionオブジェクトにそれぞれのモード用の定数が設定されているので、それを利用します。引数を省略した場合は読み込み専用モードになります。

ここまで読んで、常に全てのオブジェクトストアを対象にして読み書き可能モードでトランザクションを実行すれば、範囲やモードをいちいち気にしないで済むと思われた方もいるかもしれません。
確かにそう指定すればまず動かないことはないのですが、Mozillaのドキュメントを読むと、トランザクションの範囲はなるべく小さくして必要なオブジェクトストアだけを指定すること、そして読み書き可能モードは書き込みが必要な時だけにしてそれ以外は読み取り専用モードを使うべきだと書かれています。
これらを適切に指定するのとしないのとでは実行パフォーマンスに大きく関わってくるためです。

IndexedDBでは、範囲が重なっていない複数のトランザクションは並列で実行できます。
例えばfooStoreとbarStoreの2つのオブジェクトストアがあるとき、トランザクションAはfooStoreだけ、トランザクションBはbarStoreだけをトランザクション範囲と指定していれば、この2つのトランザクションは並列に実行できます。
しかし、どちらのトランザクションもfooStoreとbarStoreの両方をトランザクションの範囲に指定してしまうと、先に開始したトランザクションが完了するまでもう1つのトランザクションは待機しなければならなくなり、そのぶん実行時間は長くなってしまいます。
なお、読み取り専用モードのトランザクションしか実行されていないときは、トランザクション範囲が重なっていても並列で実行できます。
なので、トランザクションの範囲とモードは適切に指定するようにしましょう。
ただしwebkit(Chrome)では現在のところまだトランザクションの並列実行には対応できていないのですが。(Issue 64076)

トランザクションは、そのトランザクションを開始した関数を抜け出すときに自動的にコミットされます。
明示的にコミット用のメソッドを叩いたりする必要はありません。
逆にトランザクションを中止したい場合はIDBTransaction.abortメソッドを叩きます。

// {IDBDatabase} idb 接続中のデータベースオブジェクト
var tx = idb.transaction(['Users'], IDBTransaction.READ_WRITE); //トランザクション開始
/*
 * :
 * トランザクション処理内容
 * :
 */
tx.abort(); //トランザクション中止

トランザクションの完了後あるいは中止後に何らかの処理をしたい場合は、IDBTransactionのoncompleteonabortそれぞれのプロパティにコールバック関数を指定します。

// {IDBDatabase} idb 接続中のデータベースオブジェクト
var tx = idb.transaction(['Users'], IDBTransaction.READ_WRITE); //トランザクション開始

tx.oncomplete = function(e){
    //トランザクション完了後の処理
};
tx.onabort = function(e){
    //トランザクション中止後の処理
};
/*
 * :
 * トランザクション処理内容
 * :
 */

データの保存

ではDBにデータを保存してみます。

// {IDBDatabase} idb 接続中のデータベースオブジェクト
var tx = idb.transaction(['Users'], IDBTransaction.READ_WRITE); //トランザクション開始
console.log(tx); //-> {IDBTransaction}

var store = tx.objectStore('Users');
console.log(store); //-> {IDBObjectStore}

var value = {
    userId: 1,
    userName: 'Jane',
    address: 'Kobe'
};
var req = store.put(value);
console.log(req); //-> {IDBRequest}

DBへの書き込みをするため、トランザクションは読み書き可能モードで開きます。
IDBDatabase.transactionの戻り値であるIDBTransactionオブジェクトのobjectStoreメソッドを叩き、IDBObjectStoreオブジェクトを取得します。引数はオブジェクトストア名の文字列です。

IDBObjectStore.putメソッドに保存したいObjectオブジェクトを渡すと保存リクエストが生成されます。
保存リクエストは非同期で実行されるので、保存成功後あるいは失敗後に何らかの処理をしたい場合は、onsuccessonerrorそれぞれのプロパティにコールバック関数を指定します。

req.onsuccess = function(e){
    //リクエスト成功後の処理
};
req.onerror = function(e){
    //リクエスト失敗後の処理
};

なお、先ほどのサンプルコードはオブジェクトストア作成時にkeyPathを指定した場合(in-lineキーの場合)のものです。
in-lineキーかout-of-lineキーか、またautoIncrementでキーを自動生成するか明示的に指定するかどうかで引数の渡し方が変わります。以下にそれぞれの場合の書き方を示します。

//autoIncrementがtrueで、キーを自動生成する場合
store.put({ userName: 'Jane', address: 'Kobe' });
//in-lineキーで、キーを明示的に指定する場合
//※keyPathはuserId
store.put({ userId: 1, userName: 'Jane', address: 'Kobe' });
//out-of-lineキーで、キーを明示的に指定する場合
store.put({ userName: 'Jane', address: 'Kobe' }, 1);

in-lineキーで、キーを自動生成する場合はkeyPathに設定したプロパティは含まないようにします。
out-of-lineキーで、キーを明示的に指定する場合は第2引数にキーの値を渡します。

なお、IDBObjectStore.putメソッドとほとんど同じ動きをするメソッドとしてIDBObjectStore.addメソッドがあります。
唯一の違いは、putメソッドでは同じキーのレコードが既に存在している場合は上書きされますが、addメソッドではエラーが発生して失敗するという点です。
ログのように追加していくばかりで更新することがない場合、あるいは同じキーで上書きされることを防ぎたい場合はadd、それ以外はputを使ったほうが便利でしょう。

次の2つのトランザクションは、putのほうは成功しますが、addのほうは失敗します。

(function(){
    var tx = idb.transaction(['Users'], IDBTransaction.READ_WRITE);
    tx.oncomplete = function(){ console.log('put: complete.'); };
    tx.onabort = function(){ console.log('put: abort.'); };

    var store = tx.objectStore('Users');
    store.put({ userId: 200, userName: 'John' });
    store.put({ userId: 200, userName: 'Jane' });
})();
(function(){
    var tx = idb.transaction(['Users'], IDBTransaction.READ_WRITE);
    tx.oncomplete = function(){ console.log('add: complete.'); };
    tx.onabort = function(){ console.log('add: abort.'); };

    var store = tx.objectStore('Users');
    store.add({ userId: 100, userName: 'John' });
    store.add({ userId: 100, userName: 'Jane' });
})();

//実行結果:
//put: complete.
//add: abort.

データの取得

保存したデータを取り出してみます。

var tx = idb.transaction(['Users'], IDBTransaction.READ_ONLY);
var store = tx.objectStore('Users');

var req = store.get(1);
console.log(req);//-> {IDBRequest}
req.onsuccess = function(){
    if(this.result === undefined){
        console.log('not found.');
    }else{
        console.log(this.result);
    }
}
req.onerror = function(){
    console.log('request error.');
}

//実行結果
// [object Object]  {userId: 1, userName: 'Jane', address: 'Kobe'}

IDBObjectStore.getメソッドに取得したいデータのキーを渡すとIDBRequestオブジェクトが生成されます。
onsuccessプロパティに指定したコールバック関数の中でresultプロパティを見るとDBから取得したデータが格納されています。
該当するキーのデータが存在しない場合は、resultプロパティはundefinedになります。

先ほどはキーの値でデータを取得しましたが、インデックスを使ってデータを取得することも出来ます。

var tx = idb.transaction(['Users'], IDBTransaction.READ_ONLY);
var store = tx.objectStore('Users');
var idx = store.index('userNameIndex');

var req = idx.get('Jane');
console.log(req);//-> {IDBRequest}
req.onsuccess = function(){
    if(this.result === undefined){
        console.log('not found.');
    }else{
        console.log(this.result);
    }
}
req.onerror = function(){
    console.log('request error.');
}

//実行結果
// [object Object]  {userId: 1, userName: 'Jane', address: 'Kobe'}

IDBObjectStore.indexメソッドに対象のインデックス名(keyPathのプロパティ名ではない)を渡してIDBIndexオブジェクトを取得します。
IDBIndex.getメソッドに取得したいインデックスの値を渡すとIDBRequestオブジェクトが生成されます。
そこから先は先ほどと同じです。
インデックスにユニーク制約を付けていない場合は複数のレコードが該当することがありますが、その場合は最初の1つだけが取得されます。

データの検索

getメソッドでは、データを1件取得することしかできませんでした。
ある条件でデータを検索し、該当するデータ全てに何らかの操作をするような場合はカーソルという機能を使います。
例えば、キーが1〜10の間のデータを検索するには次のようにします。

var tx = idb.transaction(['Users'], IDBTransaction.READ_ONLY);
var store = tx.objectStore('Users');

var range = IDBKeyRange.bound(1, 10);
var direction = IDBCursor.NEXT;

var req = store.openCursor(range, direction);
console.log(req);//-> {IDBRequest}

req.onsuccess = function(){
    var cursor = this.result;
    if(cursor){
        console.log( cursor.value );
        cursor.continue(); //次を検索
    }else{
        console.log('end.');
    }
}
req.onerror = function(){
    console.log('request error.');
}

//実行結果
// [object Object]  {userId: 1, userName: 'Jane', address: 'Kobe'}
// :
// [object Object]  {userId: 10, userName: 'John', address: 'Osaka'}
// end.

IDBObjectStore.openCursorメソッドを叩いてカーソルを生成します。
第1引数rangeはキーの範囲を指定します。IDBKeyRangeオブジェクトのbound、upperBound, lowerBound, onlyの各メソッドを使いますが、詳しい説明はMDNのページを参照してください。また、nullを指定した場合は「全て」となります。
第2引数directionは昇順で検索するか降順で検索するかを指定します。IDBCursorオブジェクトにNEXT(昇順)、NEXT_NO_DUPLICATE(昇順・重複なし)、PREV(降順)、PREV_NO_DUPLICATE(降順・重複なし)という4つの定数プロパティが用意されているので、これを使います。

onsuccessプロパティのコールバック関数の内部で、IDBRequest.resultに値(IDBCursorオブジェクト)が入っているかどうかを確認します。
undefinedであれば、もう該当するレコードは存在しないということで検索は終了します。

入っている場合は、IDBCursorオブジェクトを使って、レコード1件1件に対して操作を行えます。
取得したデータの中身はvalueプロパティに、データのキーはprimaryKeyプロパティに格納されています。
updateメソッドを引数にオブジェクトを渡して叩くと、レコードのデータをそのオブジェクトで更新します。

cursor.update({
    userId: 1,
    userName: 'Jane',
    address: 'Okayama'
});

deleteメソッドを叩くと、そのレコードを削除します。

cursor.delete();

処理が終わったらcontinueメソッドを叩いて、次の検索結果へ移動します。

そして、getのときと同じく、openCursorはインデックスでも使うことができます。

var tx = idb.transaction(['Users'], IDBTransaction.READ_ONLY);
var store = tx.objectStore('Users');
var idx = store.index('userNameIndex');

var range = IDBKeyRange.bound('Jane','Jessica');
var direction = IDBCursor.PREV;

var req = idx.openCursor(range, direction);
console.log(req);//-> {IDBRequest}

req.onsuccess = function(){
    var cursor = this.result;
    if(cursor){
        console.log( cursor.value );
        cursor.continue(); //次を検索
    }else{
        console.log('end.');
    }
}
req.onerror = function(){
    console.log('request error.');
}

//実行結果
// [object Object]  {userId: 30, userName: 'Jessica', address: 'Nara'}
// [object Object]  {userId: 4, userName: 'Jennifer', address: 'Kyoto'}
// :
// [object Object]  {userId: 17, userName: 'Janet', address: 'Wakayama'}
// [object Object]  {userId: 1, userName: 'Jane', address: 'Kobe'}
// end.

onsuccess内での処理方法は同じです。

複合インデックス

次のようなユーザーのデータがオブジェクトストアに格納されており、familyNameとfirstNameの各プロパティを対象にしたインデックスが作成されているとします。

{
    id: 123456,
    firstName: 'Jane',
    familyName: 'Smith',
    address: 'Kobe'
}

このデータを取得したいとき、firstNameがJaneでかつ、familyNameがSmith、というような検索はIndexedDBではできません。

そのときは予めデータ格納する前に、他のプロパティの値を各要素にもつ配列を入れた、複合インデックス用のプロパティを作るのがいいでしょう。

{
    id: 123456,
    firstName: 'Jane',
    familyName: 'Smith',
    name: ['Jane', 'Smith'],
    address: 'Kobe'
}

こうすることで、1つのインデックスだけを使って検索・取得ができます。

// {IDBObjectStore} store オブジェクトストア
// nameプロパティに対してnameIndexというインデックスが作成されているとする
store.index('nameIndex').get( ['Jane', 'Smith'] );

RDBに慣れた人からすると正規化されていなくて冗長でキモいと思うのかもしれませんが、NoSQLでは必ずしも正規化が常に正しいわけではありません。

なお、複数のプロパティの値を対象に検索をする場合は、どれか一つのインデックスを使った検索の結果から、さらにプログラム側でその他の条件に当てはまるかどうかを調べていくという方法もあります。
検索条件が複雑になる場合はこちらの方法を取ったほうがいいでしょう。

WDを読むと、IDBObjectStore.createIndex()の第2引数(keyPath)に、複数のkeyPathを含む配列(上の例では['firstName', 'familyName']のような)が指定できて、それによって複合インデックスが作れるかのような書き方がされているのですが、実際には指定できません。

件数カウント

Firefox10以降では該当するレコード数を数えるcountメソッドが使えます。

//Firefox Aurora 10a2, Firefox Nightly 11a1で動作します
var tx = idb.transaction(['Users'], IDBTransaction.READ_ONLY);
var store = tx.objectStore('Users');

var req = store.count();
req.onsuccess = function(){
    console.log( this.result + ' records in this object store.' );
}

引数を指定しない場合は全てのレコードの件数が、openCursorの時と同じようにIDBKeyRangeで範囲を指定した場合は該当するレコードの件数が取得できます。

しかし、Chromeは18の段階でまだ実装されていません。
未実装のブラウザでカウントしたい場合は、openCursorを使って全件検索して1つ1つ数えていくしかないでしょう(時間はかかりますが)。


というわけで、IndexedDBをひと通り使うための方法を駆け足で説明しました。

ところで、
なんでこんなこと調べてたの?
…という風に思われた方もいらっしゃるかもしれません。

はじめに書いたとおり仕事でIndexedDBを使おうとしていたのですが、見ての通りブラウザごとの実装状況がまだバラバラで、しかも仕様が変わるとコードの書き方まで変わるなんていちいち対応してられません。
ということで、そういう部分を隠蔽していい感じに使えるライブラリはないかなーと探していたのですが、なかなか見つかりませんでした。

だったら自分で作ってみようかなと思い、Jaidという名前のライブラリを作り始めました。
まだまだ開発の初期段階で、ちゃんと動くものはできていないのですが、早く使えるものになるよう頑張ります。
ライブラリはMITライセンスのオープンソースにするつもりです。

この記事を書いたのは、そのライブラリを開発するために調べてわかったことを自分だけのものにするにはもったいないと思ったからです。
やっぱり技術は使われてナンボのものですからね。

こういう文章を書き慣れていないこともあり、間違いや説明が不足しているが多々あるかも知れませんが、その場合はやさしく指摘していただければ幸いです。

参考資料


2件のコメント

  1. akkie2 2012/01/18

    ちょうど、クライアントのDBAPIを試したいなと思っていたところでした。
    (前に作っていた Gears DBを変更したい。)

    Janetterのプラグインで使いたいと思ってます。(ユーザーのカラー表示で)。おらに出来るかな~^^;;;
     

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です