uhyohyo.net

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

十四章第三回 Indexed Database 2

このページの最終更新日:

key

今回はまずkeyについて説明します。

keyは前回の最初の説明でも少し触れましたが、データベースに入れた個々のレコードに一意な名前をつけるものです。

レコードに対してkeyをつける方法は二種類あります。ひとつはin-line keysという方法で、もう一つはout-of-line keysという方法です。

in-line keysというのは、レコード内にkeyの情報が含まれる形です。out-of-line keysというのは逆に、レコード本体には情報が含まれず、どこか別のところにとっておいてある形です。

詳しくは後述しますが、in-line keyを持つレコードの例は次のような感じです。


{
  userid: 5,
  name: "Johm Smith",
  age: 30,
}

このオブジェクトは何かの人物を表すオブジェクトです。name(名前)とage(年齢)の他にuseridというプロパティがあり、これはユーザーIDを意図しています。IDというくらいですから、ユーザーひとりひとりに固有のものでなければいけません。このようなものはkeyに適しています。

keyに関連する概念としてキージェネレータ(key generator)というものがあります。キージェネレータはオブジェクトストアに持たせることが可能です。

これはその名の通り、自動でkeyを作ってくれる機能です。キージェネレータが作るキーは連番の正の整数で、最初は1からスタート、2,3,...と増えていきます(ちなみに、通常ありえませんが、253(9007199254740992)まで到達するとストップし、使えなくなります)。

ただし、キージェネレータに頼らず自分でキーを付けてやることも可能で、その場合keyとしては使用可能なのは正の整数だけではありません。

keyの種類

では、どのようなものがkeyとして使用可能かについて解説します。

簡単にいうと、配列文字列Dateオブジェクト数値(0以下の数や小数も含む)です。

ただし配列は、その要素が全て正しいkey(ここで挙げた4つのいずれか)でないといけません。もっとも実際には、配列をkeyとして使う場面がどれだけあるか分かりませんが。

また、数値は、NaNはダメということになっています。InfinityはOKです。

keyの比較

keyというのは順番に並べることができなければいけません。これは並び替えやインデクシングなどの機能を提供するためです。数値のkeyの場合は並べ方は明らかですね。昇順(降順でもいいですが)に並べればいいのです。

ただ、文字列や配列などがkeyとして使われだした場合、どう並べればいいのかはよく分かりませんね。そこで、IndexedDBにおいてはkeyどうしの大小関係が定められています。この大小関係に従ってkeyは並べられます。

まず、異なる種類の値については、大きい方から、配列、文字列、Date、数値の順になっています。

つまり、配列と文字列を比べると絶対に配列のほうが大きいし、Dateや数値とくらべても同様です。また、文字列は、どんなDateや数値とくらべても大きいということです。その他も同様です。

同じタイプどうしの比較は、文字列は文字コード順、Dateは日付順、数値は大きさ順となります。直感的ですね。配列どうしの比較は次のように行います。

2つの配列の、0番目、1番目、・・・どうしの要素を比較して、どちらかが大きければ、そちらの配列が大きくなります。

どちらかの配列を全て見尽くすまで比較した場合は、長いほうが大きくなります。長さまで同じ場合は配列が完全に同じなので、同じということになります。例えば、


[1,2,3,5,4]は[1,2,3,4,5]より大きい
[5]は[4,8,12,16,20]より大きい
[0,1,2,3,4]は[0,1,2,3]より大きい

というようなことになります。

createObjectStoreのオプション

前回紹介したところによると、オプションにはkeyPathとautoIncrementの2つがありました。

keyPathにはkeyのプロパティ名を文字列で指定します。省略したり、nullやundefinedの場合はレコードにkeyが含まれない、すなわちout-of-lineになります。

逆に言えば、keyPathに文字列を指定した場合、そのオブジェクトストアのレコードはin-line keyを持つものとして扱われます。

先ほど見せた例はuseridプロパティの値をkeyとして扱っていましたから、このレコードが入るオブジェクトストアはkeyPathが"userid"とするのがよいことになります。


{
  userid: 5,
  name: "Johm Smith",
  age: 30,
}

また、オブジェクトは入れ子になることもでき、そのようなオブジェクトのプロパティをkeyPathで指定することもできます。例えば、"foo.bar"がkeyPathの場合、fooに入っているオブジェクトのbarプロパティがkeyになります。つまり、


{
  foo:{
    bar:3,	//←これがkey
  },
}

ということです。

もう一つのプロパティautoIncrementは、真偽値でしたね。分かりにくい名前ですが、trueだとキージェネレータを使うことを意味します。逆にfalseだったり省略したりした場合は使わないことになります。

キージェネレータの動作は、詳しくは後述しますが、レコードを追加するときに自動的にkeyを付加してくれます。付加する場所は、in-line keyならkeyPathの場所、out-of-lineなら目に見えないどこかです。

キージェネレータを使うことのメリットは、一意な連番を勝手にレコードに割り振ってくれることです。特に自分で用意したいIDがない場合はキージェネレータを使うのもよいでしょう。

データベースを操作する

さて、長かったですが以上でcreateObjectStoreの説明が終わり、下準備がすみました。ここからいよいよデータベースを操作する方法を紹介していきます。

データベースを操作するにはまずトランザクション(transaction)を作ります。データを書き込むときだけでなく、データベース中のデータを読むときにもトランザクションが必要です。

トランザクションとはデータベース操作のひとまとまりです。トランザクション中には複数のデータ操作を含むことができますが、ひとまとまりなので、トランザクションが失敗した場合はトランザクション全体が無効となり、無かったことになります。だから、トランザクションはデータベースにアクセスする必要が生じるたびに作り、一旦操作が終わったら終了するのがよいでしょう。

また、2つ以上のデータをセットで書き込みたいときは、同じトランザクション中で操作するようにして下さい。そうすれば、もし失敗するとまとめてトランザクションを無効にすることが容易で、片方だけ書いた中途半端な状態で終わってしまったというような問題が起こりにくくなります。

トランザクションを作るには、IDBDatabaseのtransactionメソッドを用います。返り値としてIDBTransactionオブジェクトが得られます。これがトランザクションを操作するオブジェクトです。

transactionは2つ引数を持ち、ひとつめは操作対象のオブジェクトストア名、もう一つはモード(省略可能)です。

トランザクションを作る時点で操作対象のオブジェクトストアを明示してやる必要があります。また、モードはトランザクションの使用目的を表すもので、以下の2つがあります。

"readonly"
オブジェクトストアからデータを読み込むだけで、書き込みはしないモードです。
"readwrite"
データを読み込むほかに、データを書き込んだり削除したりできるモードです。

データを読むだけのときもreadwriteを使うことは可能ですが、readonlyにはメリットがあります。複数同時にデータを読みに行けることです。

readwriteの場合は、データの整合性をとるために、複数のトランザクションが生じた場合順番待ちになります。それに対しreadonlyでは、順番がどうなろうとデータに変化がないため、同時に読むことができるのです。

第2引数が省略された場合はreadonlyになります。

また第1引数のオブジェクトストア名というのはふつう文字列ですが、配列を用いて複数のオブジェクトストア名を同時に指定することが可能です。この場合ひとつのトランザクションで複数のオブジェクトストアを同時に扱えます。もちろん、トランザクション中で、その中の個々のオブジェクトストアに対しアクセスすることが可能です。ここで指定したオブジェクトストアのことをスコープ(scope)といいます。逆にいえば、トランザクションのスコープに含まれないオブジェクトストアは、そのトランザクションで操作することはできません。

IDBTransaction

トランザクション中における操作はIDBTransactionオブジェクトを通じて行います。とりあえず基本的なプロパティとしては、dbプロパティ(そのトランザクションが属するIDBDatabaseが得られる)、modeプロパティ(先述のモード("readonly","readwrite"など))、errorプロパティ(何かエラーが発生したときにエラーの内容が入っている)があります。

またabortメソッド(引数無し)があり、呼び出すとトランザクションを強制的に失敗させます。前述のように、この場合そのトランザクションの操作全てが無効になります。

実は、いざデータベースをいじるには、もう一段階必要です。実際にいじる対象となるのはデータベースそのものではなくその中のオブジェクトストアなので、オブジェクトストアを表すオブジェクトを取得する必要があります。これはIDBObjectStoreというオブジェクトで、実は前回ちらっとでています。createObjectStoreの返り値ですね。

トランザクションからオブジェクトストアを得るには、IDBTransactionのobjectStoreメソッドを呼び出します。引数は一つで、操作したいオブジェクトストアの名前です。返り値はIDBObjectStoreです。当然ながら、transactionメソッドでトランザクションを作るときに指定したオブジェクトストアでないと取得できません。

説明が長いので、ここまでをソースでまとめてみましょう。


var request = indexedDB.open("test",1);	//testというデータベースをバージョン1で開く
//データベースの更新処理
request.addEventListener("upgradeneeded",function(e){
  var db=request.result;	//resultにはIDBDatabaseが入っている
  db.createObjectStore("foo");	//fooというオブジェクトストアを作る
});
//成功したときのイベントハンドラ
request.addEventListener("success",function(e){
  var db=request.result;	//resultにはIDBDatabaseが入っている

  //操作したい! まずはトランザクションを作る
  var transaction=db.transaction("foo","readwrite");
  //次にオブジェクトストアを呼び出す
  var objectStore=transaction.objectStore("foo");
  //次にいざobjectStoreに対して操作する
});
//失敗したとき
request.addEventListener("error",function(e){
  console.error(request.error);
});

※今までのサンプルでtestというデータベースのバージョンを5に上げたりしていたので、このままだとopenの時点でエラーが出るかもしれません。適宜、次のコードを実行してデータベースを消去してからやり直したりしてみましょう。以後のサンプルも同様です。


//データベースtestを消去
indexedDB.deleteDatabase("test");

versionchangeトランザクションについて

ところで前回、createObjectStoreの返り値として、既にIDBObjectStoreが登場しています。このIDBObjectStoreも今回トランザクションを用いて取得したオブジェクトストアと同じで、オブジェクトストアの操作に使用できます。

しかし、オブジェクトストアを操作するには必ずトランザクションを作る必要がありました。ここで注目するのが前回何気なく登場していたversionchangeトランザクションです。

これもトランザクションのひとつで、"readonly","readwrite"に続く、特別なモードである"versionchange"を持っているのです。これがversionchangeトランザクションです。このトランザクションは自分で作ることはできず、upgradeneededイベントの発生時のみ作られます。

つまり、upgradeneedイベント時におけるデータベース操作は、このversionchangeトランザクションを根拠として行われているのです。versionchangeトランザクションはreadwriteトランザクションの上位互換で、データベースへの読み書きが行えるほか、オブジェクトストアを作ったり消したりすることまでできます(実はまだありますが、それは後で紹介します)。

versionchangeトランザクションが発生している間は、他のトランザクションを作ることができません。しかし、upgradeneededイベント時に既存のオブジェクトスコアのデータをいじりたいという場合はこのversionchangeトランザクションを利用することで可能です。versionchangeトランザクションはreadwriteの上位互換なので、オブジェクトストアを作ったり消したりだけではなくオブジェクストアの中にレコードを追加したりすることもできます。その方法を説明するのはこれからですが。

前回紹介したIDBRequestは実はtransactionプロパティを持っています。前回では、openメソッドで返されたIDBOpenDBRequestが、このIDBRequestの一種でした。IDBRequestはまたのちのち登場しますが、openメソッド(やdeleteDatabaseメソッド)で返されるIDBRequestでは、transactionプロパティに、このversionchangeトランザクションを表すIDBTransactionオブジェクトが入っています。これを用いて各種の操作が可能です。

実は、versionchangeトランザクションのスコープはそのデータベースが持つ全オブジェクトストアとなっています。ですから既存のオブジェクトストアに対して操作することもここでは可能です。例えば次のサンプルのような感じです。


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

  //以前からfooというオブジェクトストアがあると仮定して、fooを操作する
  var transaction=request.transaction;	//IDBRequestのtransactionプロパティにIDBTransactionが入っている
  var foo=transaction.objectStore("foo");	//versionchangeトランザクションを用いてオブジェクトストアfooを操作できる
});
//成功したときのイベントハンドラ
request.addEventListener("success",function(e){
  var db=request.result;	//resultにはIDBDatabaseが入っている
});
//失敗したとき
request.addEventListener("error",function(e){
  console.error(request.error);
});

オブジェクトストアにレコードを追加する

さて、やっと本題にたどり着きました。いよいよオブジェクトストアにレコードを追加します。レコードとは、オブジェクトストアに、つまりはデータベースに保存することができる一単位のデータでしたね。

まず初めに、どのようなものをレコードとしてデータベースに入れることができるかを解説します。データベースに入れることができるものの幅はけっこう広いです。

具体的には、プリミティブ(真偽値、数値、文字列、nullとundefined)はOKです。さらにDateオブジェクトや正規表現オブジェクト、FileやBlob、FileList(十二章第五回)やImageData(これはまだ解説していませんが、canvasを扱うときに出てきます)もOKです。

さらに、以上のものを要素としてもつ配列やオブジェクトもOKです。つまり、データとして利用できそうな大抵のものはOKです。

逆に入れることができないのは、関数や、NodeのようなDOM関係のオブジェクトです。

また、オブジェクトについたゲッタやセッタや、enumerableなどのプロパティの属性はコピーされず、デフォルトの状態になるので注意しましょう。まあ、そういったものはデータとしてはあまりふさわしくないので、それで困る機会はないことでしょう。

ただし、上で紹介したkeyの種類によってはさらに制約が出てきます。上でkeyPathの話をしましたが、keyPathを用いる場合、つまりin-line keyの場合には、keyはオブジェクトのプロパティとなります。つまりプリミティブなどはプロパティを持てないのでレコードとしては不適で、エラーになります。また配列なども許可されず、普通のオブジェクトでなければなりません(もちろん、そのオブジェクトのプロパティには上で紹介した何が入っても構いません)。

逆にout-of-line keyの場合には、レコードそのものはkey情報を持たないので、レコードにこれ以上の制限はありません。

IDBObjectStore

上で出てきたIDBObjectStoreについて、基礎的な説明を加えておきます。

nameプロパティはそのオブジェクトストアの名前です。またkeyPath,autoIncrementプロパティは、createObjectStoreに渡されたオプションそのままです。これらの設定はオブジェクトストアを作ったあとは変えることはできないので、IDBObjectStoreのプロパティに代入して変更することはできません。

他には、transactionプロパティをもち、これは現在の操作におけるトランザクションを示すIDBTransactionです。

レコードを追加する

それではいざ、レコードを追加しましょう。IDBObjectStoreがもつメソッドを使って追加します。

レコードを追加するメソッドは2つありますが、似ているので同時に紹介します。それはaddputです。

第1引数が追加するレコード、第二引数がそのレコードにつけるkeyです。

ただし、第2引数のkeyというのは、out-of-line keyのときしか使えません。なぜなら、in-lineのときは前述のようにレコード中にkeyが含まれるからです。そちらでkeyを指定する必要があります。

キージェネレータはin-line keyでもout-of-line keyでも使用することができます。キージェネレータが有効のときはkeyを省略することができますが、そうでない場合はkeyは必須となり、keyを指定しない場合はエラーとなります。

キーの省略は、in-line keyの場合はレコードのkeyPathのところにキーが無いことを、out-of-line keyの場合は第二引数が指定されていないことを指します。

また、キージェネレータがあるにもかかわらずkeyが指定された場合は、指定されたkeyが優先され、キージェネレータは使われません。

addputの違いは、既に同じkeyのレコードがオブジェクトストアがあった場合の動作です。keyはレコードに対して一意である必要がありますから、同じkeyのレコードはオブジェクトストア内に複数存在できません。

addの場合は、既存のレコードを尊重して、エラーを出して失敗します。putの場合は新しいほうを尊重して、既存のレコードを上書きします。

以上がadd,putの動作ですが、keyについて注意点があります。キージェネレータがある場合で、自分で数値のkeyを指定した場合です。先ほど説明したように、自分でkeyを指定した場合はそちらが優先されますが、数値を指定した場合はキージェネレータに影響があります。具体的には、keyとして正の数値を指定した場合で、かつその値がキージェネレータから発生する次の数値以上の場合、キージェネレータの数値が「その値より大きい最小の整数」に変更されます。

例えば今までにキージェネレータが1,2,3,4というkeyを作ったとします。ここで、10というkeyをもつレコードを追加したとすると、この10という値はキージェネレータから次に発生するkeyである5より大きい値です。したがって、キージェネレータが次に生み出すkeyは11に変更されます。

例を出すと、なぜこうなるのか分かりますね。キージェネレータをそのままにしておくと、そのうちキージェネレータが10というkeyを作った時に重複してしまい困ります。キージェネレータによってkeyが重複することがないようにするための処置というわけです。

IDBRequest

さて、addやputの返り値はIDBRequestです。これは前回も少し出て来ましたね。

このような、オブジェクトストアに対するひとつひとつの操作をリクエストと呼び、その結果を知らせてくれるのがIDBRequestです。ひとつのリクエストについてひとつのIDBRequestが作られます。

前回も少し触れましたが、IDBRequestのプロパティは次の通りです。resultはリクエストの結果(内容は場合によって違います)、errorは起きたエラー(エラーが起きた場合のみ)、sourceは操作対象のIDBObjectStore、transactionは現在操作しているIDBTransactionです。そして、readyStateは前回紹介したように、"pending"または"done"です。

IDBRequestではイベントが発生する可能性があります。成功したときはsuccessイベント、失敗したときはerrorイベントです。successイベントは次の処理に進むためによく使いますし、errorイベントは、前述のように、中途半端に失敗したらトランザクションを中断して無効にするというときに、失敗の検出をするために使えます。

さて、addやputの場合には、IDBRequestのresultは追加されたレコードのkeyになります。自分でkeyを指定した場合はいいですが、キージェネレータでキーが生成された場合にも、これを用いてそのレコードのkeyを知ることができます。

それでは、試しに1つレコードを追加してみるサンプルを紹介して、続きは次回にします。


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

  //fooというオブジェクトストアを作っておく。out-of-line keyでキージェネレータを使用する。
  db.createObjectStore("foo",{
    autoIncrement:true
  });
});
//成功したときのイベントハンドラ
request.addEventListener("success",function(e){
  var db=request.result;	//resultにはIDBDatabaseが入っている

  //トランザクションを作る
  var transaction=db.transaction("foo","readwrite");	//書き込むのでreadwriteにする
  //オブジェクトストアを得る
  var objectStore=transaction.objectStore("foo");
  //値を追加してみる
  var req=objectStore.add({
    hello:"world"
  });	//キージェネレータを使うので、第二引数のkeyは省略してもよい

  //返り値はIDBRequest。イベントを監視して結果を得る
  req.addEventListener("success",function(e){
    console.log("追加に成功しました",req.result);	//resultはkey
  });
  req.addEventListener("error",function(e){
    console.log("追加に失敗しました",req.error);
  });
});
//失敗したとき
request.addEventListener("error",function(e){
  console.error(request.error);
});

ちなみに、データベースの中身は、Chromeの場合だとDeveloper toolsのApplicationsから見ることが可能です。試しに見てみましょう。