uhyohyo.net

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

十四章第三回 Indexed Database2

key

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

keyは前回の最初の説明でも少し触れましたが、データベース内のデータを効率よく検索したりするために必要なものです。

まあ大体は、データベースに入れた個々のレコードに何となく一意なID的なものをつけておけばいいと思います。

では、具体的にはどのようにして、レコードに対しkeyを設定すればいいかということですが、実はkeyの付け方には二種類あります。ひとつはin-line keysという方法で、もう一つはout-of-line keysという方法です。

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

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

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

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

keyの種類

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

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

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

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

keyの比較

keyは並び替えなどに使われることがあるので、keyどうしの大小関係が定められています。

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

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

さらにその中で、同じタイプどうしの比較は、文字列は文字コード順、Dateは日付順、数値は大きさ順となって問題ないですね。配列どうしの比較は次のように行います。

二つの配列の、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の場合はout-of-lineになります。

ちゃんと文字列の場合は、in-lineになるのですが、この場合追加するレコードの中にkeyが含まれていることになるので、keyのプロパティ名を教えます。

つまり、keyPathが"foo"であるオブジェクトストアに対して次のようなオブジェクトをレコードとして追加する場合を考えましょう。

{
  name:"John",
  age :32,
  foo :1,
}

この場合、fooの値である1がこのレコードのkeyとして扱われます。

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

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

ということです。

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

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

in-lineでキージェネレータを使うことのメリットは、一意な連番を勝手にレコードに割り振ってくれることです。

データベースを操作する

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

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

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

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

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

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

前にも述べたようにデータベースは複数のオブジェクトストアを持ちますから、操作対象を明示してやる必要があります。また、モードはトランザクションの使用目的を表すもので、以下の2つがあります。

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

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

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

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

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

IDBTransaction

それではこの、IDBTransactionの扱い方を紹介します。基本的なプロパティとして、dbプロパティ(そのトランザクションが属するIDBDatabaseが得られる)、modeプロパティ(先述のモード("readonly","readwrite"など))、errorプロパティ(何かエラーが発生したときにエラーの内容が入っている)があります。

またabortメソッド(引数無し)があり、呼び出すとトランザクションを強制的に失敗させます。前述のように、この場合そのトランザクションの操作全てが無効になります。例えば、一つのトランザクション中に複数のデータを書き込むときに、途中で失敗したら、複数のデータのうち一部だけが中途半端に書き込まれてしまい困るというときには、この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");	//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トランザクションであり、open時に必要ならば自動で作られます。

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

versionchangeトランザクションが発生している間は、他のトランザクションを作ることができません。しかし、upgradeneededイベント時に既存のオブジェクトスコアのデータをいじりたいという場合はこのversionchangeトランザクションを利用することで可能です。

前回紹介した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);
});
        

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

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

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

HTML5にはstructured clone algorithmという規定があります。簡単にいうとオブジェクトなどを複製する方法が定められています。これにより複製できるものなら何でもデータベースに入れることができます。

具体的には、プリミティブ(真偽値、数値、文字列、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です。

第一引数が追加するレコード、第二引数がkeyです。

ただ、第二引数のkeyというのは、out-of-line keyのときしか使えません。in-lineのときは前述のように、レコード中にkeyが含まれますね。ですから、第二引数でkeyを指定できません。

また、キージェネレータがある場合はキーを自動で付加してくれるからいいのですが、キージェネレータがない場合は、keyは必須ですので、キーがない場合はエラーになります。

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

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

addとputの違いは、既に同じkeyのレコードがオブジェクトストアがあった場合の動作です。同じkeyのレコードはオブジェクトストア内に複数存在できません。

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

以上がadd,putの動作ですが、keyについて注意点があります。キージェネレータがある場合で、自分で数値のキーを指定した場合です。先ほど説明したようにそちらが優先されますが、値によってはキージェネレータが生み出すキーが今後重複する可能性があります。つまり、今までにキージェネレータが1,2,3,4というkeyを作ったとします。ここで、10というkeyをもつレコードを追加したとすると、そのうちキージェネレータが10というkeyを作った時に重複してしまい困ります。

そこで、自分で指定したキーが正の数値だった場合は、キージェネレータの数値が「その値より大きい最小の整数」に変更されます。つまり今回の場合だとキージェネレータは、10より大きくて最小の整数である11に変更され、次にキージェネレータが生み出すkeyは11ということになります。5〜9は欠番になります。

自分のkeyが0や負の数だった場合は、重複することはありませんので何もおきません。

ただし、例えば8.5とか、正の小数の場合も重複はしないはずですが、この場合も上記と同様のことがおき、8.5より大きい最大の整数である9になります。数値以外のkeyを指定した場合は何も起きません。

IDBRequest

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

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

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

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

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

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

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のresourcesから見ることが可能です。試しに見てみましょう。