uhyohyo.net

JavaScript初級者から中級者になろう

十四章第四回 Indexed Database3

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

前回はIDBObjectStoreのメソッドaddとputを紹介しました。

今回はさらにメソッドを紹介します。まずはgetです。これは、引数にkeyを指定すると、そのkeyをもつレコードが得られるというものです。ただし、そのkeyをもつレコードがない場合はundefinedが帰ってきます。

ただし、返り値として帰ってくるわけではありません。getの返り値はIDBRequestです。

addやputのときと同様に、IDBRequestで発生するsuccessイベントを待って、そのresultプロパティに入っているものを見ます。また、addやputの動作から分かるように、同じkeyを持つレコードがオブジェクトストア内に2つ存在することはありません。よってgetで得られるレコードはひとつです。

またdeleteメソッドもあり、これも同様に引数にkeyを指定して、当てはまるレコードを削除するものです。これも当然戻り値はIDBRequestです。今回もsuccessイベントは発生しますが、resultは特にありません。

さらに、clearメソッドもあります。引数はありません。これはオブジェクトストア内のレコードを全削除します。返り値は当然IDBRequestで、扱い方も同様です。

以上が、極めて基本的なオブジェクトストアの操作です。ここまで分かっていればlocalStorageみたいな使い方くらいはできますね。

Key Range

そこで次に紹介するのがKey Rangeです。getやdeleteでは、ひとつのレコードしか同時に扱えませんでした。それは、一つのkeyに対して一つのレコードしか存在しないからです。それならば、複数のkeyを同時に扱えば複数のレコードを同時に扱うことが可能になります。

とはいっても、何の脈絡もない複数のkeyを同時に扱う意味はそんなにありません。Rangeというのは、「範囲」ということです。つまり、keyの範囲を扱うのです。

例えば、keyが1から5までのレコードをまとめて扱うといったことが可能です。

範囲を扱うにはkeyどうしの大小関係が必要ですが、それは前回説明しましたね。

それで、Key Rangeは、二つの端点によって定義されます。範囲の中で最小の点と最大の点ですね。また、開いているか閉じているかの区別も必要です。開いているというのは、端点が範囲に含まれないということです。閉じているというのは、端点が範囲に含まれるということです。つまり、端点が3と5だといっても、範囲は次の4通りの可能性があります。

3<x<5
3≤x<5
3<x≤5
3≤x≤5

これは、3が含まれるか、5が含まれるかをそれぞれ決めてやれば決定できるというわけです。

さて、key rangeは、IDBKeyRangeというオブジェクトで表されるのですが、作り方がほんの少し特殊です。IDBKeyRangeコンストラクタ自体がもつメソッドを使います。メソッドは4種類あり、まず紹介するのはboundです。

boundは4つの引数からなり、まず小さい方の端点、次に大きい方の端点、次に小さい方の端点が開いているかどうか(真偽値)、最後に大きい方の端点が開いているかどうか(真偽値)です。つまり、例えば

3<x<5

というIDBKeyRangeオブジェクトを得たいならば次のようにします。

var range=IDBKeyRange.bound(3, 5, true, true);

また、10≦x<20というIDBKeyRangeを得たいならば次のようにします。

IDBKeyRange.bound(10,20,false,true);

また、範囲の上限を指定しないkey rangeを作るには、lowerBoundメソッドを使います。引数は一つで、小さい方の端点と、次にそれが開いているかどうか(真偽値)です。例えば、-10≦xというkey rangeを作りたいならば、

IDBKeyRange.lowerBound(-10,false);

とします。

同様に、下限を指定しないupperBoundもあります。例えばx<8としたいならば、

IDBKeyRange.upperBound(8,false);

とします。

最後に、onlyというというメソッドもあり、これは引数一つとって、その引数のkeyのみを含むようなkeyRangeを作ります。keyが一つだけならkey rangeを使う必要性もないように思えますが、たまに使うかもしれません。たとえば、3のみを含むkeyRangeというのは次のような範囲です。

3≦x≦3

ですから、onlyを使った次の文で得られるIDBKeyRangeは、

IDBKeyRange.only(3);

次のようにboundを使っても同じ結果が得られます。

IDBKeyRange.bound(3,3,false,false);

逆に、lowerBound,upperBoundの結果はboundでは再現できません。

さて、そうして得られたIDBKeyRangeには、lower,upper,lowerOpen,upperOpenという4つのプロパティがあります。これはboundの引数にそのまま対応します。ただし、このプロパティを書き換えることはできません。別のkey rangeが欲しかったら新しく作りなおすしかないのです。

lowerBoundで作った上限のないkey rangeの場合はupperプロパティはundefinedです。upperBoundで作ったものも同様に、lowerプロパティはundefinedです。

さて、このようにして作ったIDBKeyRangeは、keyのかわりに、getやdeleteの引数として使えます。ただし、getにIDBKeyRangeを渡しても、レコードが複数得られるわけではありません。当てはまる最初の一つのレコードのみ帰ってきます。deleteの場合は、当てはまるレコードを全部消去してくれます。

また、IDBKeyRangeが使える新しいメソッドを一つ紹介します。IDBObjectStoreがもつメソッド、countです。

countは引数として、Key Range(またはkey)を受け取ります。そして、そのKey Range(またはkey)に当てはまるレコードの数を数えてくれます。結果は今まで同様に、IDBRequestのresultで数値として受け取ります。当然、Key rangeではなくただのkeyを引数に渡すと、返ってくる結果は0または1になりますね。

カーソルを扱う

しかし、せっかくkey rangeがあるのだから、getのように一つだけじゃなくて、ちゃんと複数のレコードを扱ってみたいものです。そこで登場するのがカーソル(cursor)です。

カーソルは、オブジェクトストアの上を渡り歩き、条件にあうレコードを拾い集めることができる感じの機能です。

カーソルを作るには、IDBObjectStoreのメソッドopenCursorを使います。一つ目の引数はkey range(またはkey)で、カーソルはこの範囲にあるレコードを動きます(省略可能で、その場合オブジェクトストア内のレコード全部になります)。2つめはカーソルの移動方向で、以下の4種類が可能です。

"next"
最初(keyが小さい方)のレコードから、大きい方へ順番に進んでいきます。
"prev"
最後(keyが大きい法)のレコードから、小さい方へ順番に進んでいきます。
"nextunique"
nextと同じですが、重複するkeyをもつレコードは最初のものだけ見ます。
"prevunique"
prevと同じですが、重複するkeyをもつレコードは最初のものだけ見ます。

おおまかに分けて前からか後ろからかの2つですね。それにuniqueかどうかでさらに分かれます。

しかし違和感を感じませんか。「重複するkeyをもつレコード」といっても、もともとkeyは重複できないはずです。確かに現段階では、keyは重複しないのでnextもnextuniqueも変わりません。これは後でインデックスの話をするときに関わってきますので、今は深く考える必要はありません。keyは重複しないということは覚えておいて下さい。

さて、openCursorの返り値も、例のごとくIDBRequestです。resultプロパティの値として、カーソルを示すIDBCursorWithValueオブジェクトが得られます。ちなみにこれは、IDBCursorというオブジェクトを継承したものですが、それは気にする必要はありません。

ただし、そのkey rangeに当てはまるレコードが一つも無かった場合、IDBCursorWithValueではなくnullがresultに入って返ってきます。またこのときのIDBRequestは、後に再利用されますので注意して下さい。この2つの点が、カーソルの扱いに結構大きな影響を与えます。

そこで、IDBCursorの扱い方を今から説明していきます。

例のごとく基礎的な説明からいくと、IDBCursorは以下のプロパティを持ちます。全て書き換えられません。カーソルも、一度作ったら設定を変更できないのです。

source
カーソルが動いているオブジェクトストアを示します。
direction
openCursorの第二引数の方向が入っています。

また、カーソルが現在いる位置のレコードの内容を示す、keyvalueというプロパティを持ちます。それぞれ、そのレコードのkeyとvalueです。valueは、そのレコードの内容そのものでしたね。つまりaddやputで追加した内容です。

さっき説明したように、該当するレコードが一つもない場合はopenCursorの結果はnullです。そうでない場合最低でも一つは該当するレコードがあることになりますね。カーソルは初期状態では、一番初めのレコード(一番最初か最後かはカーソルの向きに依存しますが)を指し示す位置にいます。

さて、しかしカーソルですから、初めのレコードだけでなく次の位置に移動することができます。そのためのメソッドが2つあります。

まず紹介するのはadvanceメソッドです。引数を一つとります。正の整数で、レコード何個分進むかです。

つまり、1ならば次のレコードに移動します。2ならば、一つ飛ばして、2つ先のレコードへ移動します。負の数はだめなので、戻ることはできません。

返り値は、ふつうならIDBRequestといきたいところですが、実はこのメソッドはopenCursorで返されたIDBRequestを再利用して、そこでsuccessイベントなどを発生させるので、advanceの返り値はありません。なぜこんな仕様になっているかということは、後でサンプルを交えて説明します。

IDBRequestのresultには、カーソル自身が入っています。最初のopenCursorのときと同様ですね。

これまたopenCursorと同様に、もう最後まで来てしまい、次のレコードがなくて移動できない場合はresultにnullが入っています。

もう一つ、continueというメソッドがあります。これは第一引数が、数値のかわりにkeyになっています。

これは、何個分レコードを進むということではなく、そのkeyをもつレコードのところまで一気に進みます。ただし、そのkeyにあてはまるレコードがない場合には、そのkeyより大きくて、なるべくkeyが小さいレコードを選びます。

つまり分かりやすくいうと、とりあえず与えられたkeyのところまで一気に進んで、なるべく近いところを見つけるということです。

このメソッドもIDBRequestを再利用し、advanceと同様に振舞います。最後もうなかったらnullになるのも同じです。

これで、とりあえずKey rangeに当てはまるレコードを全部取得するということは達成できますね。advanceで一つずつみていって、nullが出てきたら終了すればいいのです。

ですが、カーソルにはまだメソッドがあるのでそれを紹介します。

次に紹介するのはupdateです。これは、現在の位置のレコードを書き換えるというメソッドです。カーソルは、レコードを見るだけでなく書き換えることも可能なのです。

第一引数に新しいレコード(のvalue)を渡してやると、その内容に書き換わります。ただし、keyが変わってはいけません。out-of-lineのキーの場合にはvalueにはキーは含まれないので問題ありませんが、in-lineの場合はレコードのvalueに、プロパティとしてキーが含まれています。これが前と異なる場合はエラーになります。

updateは、advanceやcontinueのようにIDBRequestを再利用しません。自分のIDBRequestを作って、戻り値として返します。もちろんここでsuccessイベントなどが発生します。ちなみにresultには、IDBObjectStoreのaddやputと同様にkeyが入っていますが、これは特に使う場面はないことでしょう。

最後に、deleteメソッドも紹介します。読んで字の如しといったところでしょうが、現在のカーソル位置にあるレコードを消去します。これも自分のIDBRequestを返します。resultはnullになります。

当然のことながら、advanceやcontinueはreadonlyのトランザクションでも使えますが、updateやdeleteはreadwriteトランザクションでないと使えません。

サンプル

それでは、カーソルを利用して、オブジェクトストア中のデータを全部表示するサンプルを紹介します。

var request = indexedDB.open("test",1);	//testというデータベースをバージョン1で開く
//データベースの更新処理
request.addEventListener("upgradeneeded",function(e){
  var db=request.result;	//resultにはIDBDatabaseが入っている

  //fooというオブジェクトストアを作っておく。out-of-line keyでキージェネレータを使用する。
  var store=db.createObjectStore("foo",{
    autoIncrement:true
  });	//返り値はIDBObjectStore
  //とりあえず最初にいくつかレコードを追加しておく
  store.add("小泉純一郎");
  store.add("安倍晋三");
  store.add("福田康夫");
  store.add("麻生太郎");
  store.add("鳩山由紀夫");
  store.add("菅直人");
  store.add("野田佳彦");
  store.add("安倍晋三");	//←同じデータだけどkeyは異なる
});
//成功したときのイベントハンドラ
request.addEventListener("success",function(e){
  var db=request.result;	//resultにはIDBDatabaseが入っている

  //トランザクションを作る
  var transaction=db.transaction("foo","readonly");
  //オブジェクトストアを得る
  var objectStore=transaction.objectStore("foo");
  //カーソルを作る(全部なのでkey rangeは指定しない)
  //返り値はIDBRequest
  var req=objectStore.openCursor(null,"next");
  req.addEventListener("success",function(e){
    //最初カーソルができたときもadvanceでカーソルが進んだときも呼ばれるイベントハンドラ
    if(req.result==null){
      //IDBRequestのresultがnullのときは、もうデータがない
      console.log("終了しました。");
    }else{
      //IDBRequestのresultはIDBCursorWithValueである
      var cursor=req.result;
      console.log(cursor.value);	//そのレコードを表示
      //次のレコードに進む
      cursor.advance(1);
    }
  });
  req.addEventListener("error",function(e){
    console.error(req.error);
  });
});
//失敗したとき
request.addEventListener("error",function(e){
  console.error(request.error);
});

これを実行すると以下のように出力されるはずです。

小泉純一郎
安倍晋三
福田康夫
麻生太郎
鳩山由紀夫
菅直人
野田佳彦
安倍晋三
終了しました。

今までの説明が結構複雑だったわりには、簡単なサンプルだと思いませんか。openCursorの返り値のIDBRequestが使いまわされるという点がポイントでもあります。

最初openCursorの結果として発生するsuccessと、その後カーソルのadvanceメソッド(やcontinueメソッド)で発生するsuccessイベントを区別しなければいいのです。どちらも、IDBRequestのresultには、もうレコードがない場合はnull、まだある場合はIDBCursorが入っているという点で共通しています。

最初のsuccessを、「カーソルがどこかから最初のレコードへやってきた」と考えれば自然です。このようにして順番にデータベース内のレコードを扱うことができます。

さて、これでひと通りの操作ができるようになりましたが、まだ不満ですね。データベースなんだから、もっと検索とかしたいものです。keyで並んでいるだけでは物足りない気がしますね。

そこで次回はIndexed Databaseの真骨頂ともいえる、インデックスについて解説します。