uhyohyo.net

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

十四章第二回 Indexed Database

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

今回紹介するIndexed Database、通称IndexedDBは、前回のStorageが進化したようなもので、ある種のデータベースをJavaScriptから作ってブラウザに保存しておいてもらえるものです。localStorageと比べると、文字列だけでなくオブジェクト等を保存できる点や検索機能がある点でより強力です。

昔はSQLを用いたクライアントサイドデータベース(WebSQL)の仕様策定が進んでいましたが、のちに破棄され、SQLを使わない方向の新しいデータベースAPIとしてIndexedDBが用意されたのです。

IndexedDBにはバージョン1と2がありますが、ここではバージョン1の範囲で説明します。

データベースの構造

データベースは、localStorageと同様に同じオリジンでは共有されます。また、データベースは同オリジン内に複数作ることができ、名前をつけることで区別します。

またデータベースはバージョンを持ちます。アプリが進化してくると、データベースの仕様も変化して、互換性がなくなることがありますね。データベースにバージョン情報をもたせることで問題を回避できます(詳しくは後述)。

当然ながら、データベースにはデータを入れることができます。ひとつひとつのデータはレコード(record)といい、レコードはkeyvalueを持つとされています。

名前の通り、keyとはあるレコードに対してつけられた名前のようなもので、valueとはデータの中身です。keyはレコードをソートしたりするときに使われます。

データベースはオブジェクトストアを保持して、レコードはオブジェクトストアの中に保存されることになっています。

オブジェクトストアには名前をつけることができ、データベースの中に複数存在することが可能です。

まとめると、レコードはオブジェクトストアの中に保存されていて、1つ以上のオブジェクトストアをまとめたものがデータベースだということです。

データベースを扱う

それでは、JavaScriptにおけるIndexedDBの使い方を見ていきます。IndexedDBには同期APIと非同期APIがありますが、例によって、ここで主に解説するのは非同期APIです。

IDBRequest

まず最初にIDBRequestというオブジェクトを紹介します。これはIndexedDBに対して何か操作を行った時の結果を表すオブジェクトです。操作の結果は、このオブジェクトで発生するイベントを通じて得ることができます。

IDBRequestで発生するイベントは2種類あり、successerrorです。操作が成功したらsuccess、失敗したらerrorが発生します。

IndexedDBに対する操作に対しては結果が伴います。例えばデータベースからデータを読み込んだなら、その読み込まれたデータが結果となります。操作の成功時には、IDBRequestオブジェクトから結果を取り出すことができます。実は、結果はresultプロパティに入っています。resultプロパティの中身は、当然ながらどんな操作をしたかによって異なります。

実は、データベースに対して何かを要求した場合、即座に結果のIDBRequestオブジェクトが得られます。ただし非同期ですから、IDBRequestを手に入れた直後はまだ結果は分かりません。基本的にはイベントハンドラを登録して操作の完了を待てばよいですが、もしイベント以外で操作の状態を知りたい場合はIDBRequestのreadyStateプロパティが役に立ちます。readyStateという名前はこれまでも何度か出てきましたね。数値という印象が強いように思えますが、ここでは文字列で、2種類あります。次のようになっています。

"pending"
まだ処理中(結果を利用できない)
"done"
処理終了(結果利用可能)

ちなみに、他に利用可能なプロパティは、errorプロパティ(処理終了時にエラーが発生した場合、エラーオブジェクトが入っている。正常終了の場合はnull)があります。さらにsourceプロパティ、transactionプロパティがありますが、これらは後述します。

データベースにアクセスする

データベースにアクセスするには、window.indexedDBを用います。今さら言うまでもなく、windowのプロパティはグローバル変数としても利用できます。なお、indexedDBプロパティはIDBFactoryというオブジェクトのインスタンスです。

さて、このIDBFactoryは3つのメソッドを持ちますが、まずはそのうち2つを紹介します。opendeleteDatabaseです。openはデータベースを開くメソッドです。データベースを操作するには、まずそのデータベースを開く必要があるのです。deleteDatabaseはその名の通り、データベースを削除するメソッドです。

いきなり返り値の説明から入りますが、先ほどの説明からいけば返り値はIDBRequestのはずですね。しかしopenメソッドの場合だけは特別で、IDBRequestの進化系であるIDBOpenDBRequestオブジェクトとなります。しかしこれはIDBRequestを継承したもので、基本は変わりません。違いは、blockedイベントupgradeneededイベントという2つのイベントが追加されている点です。この二つはあとで紹介します。

それで、openメソッドは、2つの引数を持ちます。一つ目はデータベースの名前です。データベースは複数存在可能ですから、名前を付けて区別するわけです。もう一つはデータベースのバージョンです。バージョンは1以上の整数です。

openはデータベースを開くメソッドですが、そのデータベースが存在しない場合は作ってくれます。つまり、とにかくデータベースを使いたければopenということです。

第2引数(バージョン)の意味は、簡単に言うと「このバージョンで開きたい!」と宣言するものです。データベースというのは、自分のバージョンを記憶しています。データベースのバージョンは自分で決めていいのですが、基本的にはアプリのアップデート等によりデータ構造が変わってしまった場合にバージョンを上げます。

引数で指定したバージョンが開こうとしたデータベースより低い場合(すなわち、保存されているデータベースのバージョンが高いのにそれより低いバージョンのデータベースを要求した場合)、このアプリは新しいデータベースに対応していなくて誤動作を起こす可能性があるということなので、エラーを起こしデータベースは開けません。

同オリジンポリシーがあるのでデータベースを扱うのは自分だけだからそんな心配はないと思うかもしれませんが、例えば古いページのキャッシュが残っていて新しいページと古いページが混在した時などに厄介なことになりかねません。適切にバージョン管理をしていれば、古いほうは動かなくなり問題は起きません。

指定したバージョンが、現在のデータベースのバージョンと同じなら、問題なくデータベースが開けます。

それでは、データベースのバージョンを上げたい場合はどうしたいかというと、現在より新しいバージョンを指定してopenすればいいのです。つまり、何かデータベースの仕様変更があったときは、openメソッドに指定するバージョン番号を上げてやれば、自動的に新しいバージョンのデータベースに移行するというわけです。

ちなみにバージョン番号は省略可能で、その場合は普通にデータベースを開くことができ、データベースのバージョンは上がりません。つまり、現在のバージョンと同じ番号を指定したのと同じ動作です。このように、データベースを開くだけならバージョンは省略できますが、やはり省略しないほうが安全でしょう。バージョンを上げる場合のより具体的な方法については後述として、ひとまず説明を進めます。

さて、無事データベースを開けた場合は、IDBOpenDBRequestのresultプロパティには、IDBDatabaseのインスタンスであるオブジェクトが入っています。これはデータベースに対する操作の入り口となるオブジェクトであり、これを用いていよいよデータベースの中身をいじることができます。つまり、openメソッドによりデータベースを開く操作は具体的にいえばIDBDatabaseオブジェクトを取得することに対応しています。ここまでを確認しましょう。


var request = indexedDB.open("test",1); //testというデータベースをバージョン1で開く。openの返り値はIDBOpenDBRequest
//成功したときのイベントハンドラ
request.addEventListener("success",function(e){
  console.log(request.result);	//resultにはIDBDatabaseオブジェクトが入っている
});
//失敗したとき
request.addEventListener("error",function(e){
  console.error(request.error);
});

サンプルページは用意していませんが、上のサンプルを実行すると正常にデータベースが開かれIDBDatabaseオブジェクトがコンソールに表示されるはずです。

次に、データベースの操作に移る前に、deleteDatabaseも紹介しておきます。これは簡単で、引数はデータベース名のみです。

返り値はopenと同じくIDBOpenDBRequestで、successイベントやerrorイベントが発生します。また、データベースがまだ処理中で消去できない場合は、終わるのを待ってから(といってもどうせ消えてしまいますが)消去します。待たされた場合は、上で述べたblockedイベントが発生します。注意すべきは、blockedイベントが発生しても処理に失敗したわけではないということです。blockedイベントの発生後、消去処理が可能になり次第処理が行われ、success(成功時)またはerror(失敗時)イベントが発生して、それで終了となります。

それではデータベースの操作の話に移りますが、その前にIDBDatabaseの基礎的な話をします。

IDBDatabaseはnameプロパティ(データベースの名前が文字列で入っている)とversionプロパティ(データベースの現在のバージョン)を持ちます。nameには代入できません。つまり、データベースの名前は一度作ったら変えられないということです。消して作りなおせば別ですが。versionも代入できません。変えるには、さっき説明したようにopenメソッドでバージョンを上げます。

また、closeメソッドを持ちます。これはそのデータベースに対する操作を終了することを明示するメソッドです(消すわけではありません)。返り値も引数もありません。

オブジェクトストアを扱う

データベースはオブジェクトストアを保持すると上で紹介しました。ですから、データベースがあっても、中にオブジェクトストアを作ってやらないと、レコードを入れることはできません。

オブジェクトストアはひとつのデータベースの中に複数作れます。複数の領域を用意して異なる情報を入れておけるというわけですね。ですから、基本的に一つのアプリケーションなら一つのデータベースで完結するのがよいのではないでしょうか。

そこでまず、IDBDatabaseがもつ、オブジェクトストアを作るメソッドcreateObjectStoreを紹介します。引数は2つで、1つ目は作るオブジェクトストアの名前(文字列)、次はオプション(省略可能)です。

データベース内のオブジェクトストアも名前で区別するので、作るときに名前をつけてやります。createObjectStoreの返り値は、IDBObjectStoreというオブジェクトで、これがオブジェクトストアを表します。IDBObjectstoreについては次回解説します。

しかしよく考えると、createObjectStoreはいつ呼べばいいのか、ちょっと困りませんか。いざデータを保存しようとするとオブジェクトストアが必要なので作るわけですが、オブジェクトストアを作るのは最初の一回だけで十分です。ですから、普通なら、データベースが持っているオブジェクトストアを調べて、目当てのが無かったらcreateObjectStoreを呼ぶ、という手段を踏みたくなるところです。しかしIndexedDBにおいてはやや違う方法をとります。それが、versionchangeトランザクションというものをを使う方法です。実はcreateObjectStoreはversionchangeトランザクション中しか使えないメソッドなので、必然的にこれを使うことになります。トランザクションとは何かということはまた後で説明します。

上で説明したopenメソッドでデータベースを開くときに、データベースのバージョンが引数で与えられたバージョンより古い場合、またはデータベースが無いので新しく作られる場合にversionchangeトランザクションが発動します。

つまり、オブジェクトストアの作成などのデータベースの構造が変わってしまう処理は、データベースを作るときかバージョンを上げるときにしかできないということです。

versionchangeトランザクションが発動した場合、openの返り値であるIDBOpenDBRequestにおいて、upgradeneededイベントが発生します。データベースのバージョンが上がる(または作られる)のでupgrade(更新)がneeded(必要)という意味です。このイベントが発生したら、新しいデータベースに必要なオブジェクトストアをcreateObjectStoreによって追加したり、いらないオブジェクトストアをdeleteObjectStoreで消したりなどの処理をすればいいわけです。

ここまでをサンプルで振り返ります。


var request = indexedDB.open("test",1); //testというデータベースをバージョン1で開く。openの返り値はIDBOpenDBRequest
//データベースの更新処理
request.addEventListener("upgradeneeded",function(e){
  // ここにデータベースの更新処理を書く
});
//成功したときのイベントハンドラ
request.addEventListener("success",function(e){
  console.log(request.result);	//resultにはIDBDatabaseが入っている
});

実は、upgradeneededは、success(やerror)よりも前に発生することになっています。しかし、upgradeneededの時点でIDBOpenRequestのresultプロパティ(IDBDatabaseが入っている)は利用可能になっています。したがって、これを用いてcreateObjectStoreすればいいのです。簡単にいうと次のような感じです。


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){
  console.log(request.result);	//resultにはIDBDatabaseが入っている
});

また、upgradeneededにおけるイベントオブジェクトはIDBVersionChangeEventと呼ばれる種類のものであり、この場合oldVersionnewVersionという2つのプロパティが利用できます。

それぞれ、変更前のデータベースのバージョン、変更後のバージョンです。これにより、バージョンが何から何へ上がろうとしているのか知ることができます。上で説明した通り、データベースが新規作成された場合もupgradeneededが発生しますが、このときはoldVersionは0です。

これを用いて、例えばデータベースがどんどん複雑化してバージョンが何度も上がっている場合、upgradeneededのイベントハンドラは次のようになっていることが想定できます。


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

  if(old<1){
    //データベースが初めて作られたときの処理
    db.createObjectStore("foo");	//fooというオブジェクトストアを作る
  }
  if(old<2){
    //データベースのバージョンが1から2に上がった時に追加されたオブジェクトストア
    db.createObjectStore("bar");	//barというオブジェクトストアを作る
  }
  if(old<3){
  //同様に2から3のとき
    db.createObjectStore("baz");
  }
  if(old<4){
    db.createObjectStore("qux");
  }
  if(old<5){
    db.createObjectStore("hoge");
  }
});
//成功したときのイベントハンドラ
request.addEventListener("success",function(e){
  console.log(request.result);	//resultにはIDBDatabaseが入っている
});

このデータベースには、foo,bar,baz,qux,hogeという5つのオブジェクトストアがありますが、バージョン1のときはfooしか無かったのでしょう。2に上がるにあたって、bazが追加されました。以下同様に、バージョンが上がるごとに1つずつオブジェクトストアが追加されていったのです。

条件判定に不等号を用いることで、柔軟に対応することができます。

例えばバージョン2のデータベースを持つ人がこのページを読み込んだ場合、「old<2」は満たさないが「old<3」からは満たすので、baz,qux,hogeの3つが追加されます。初めて来た人も、同様にちゃんと5こ全部が追加されます。

ここまでのポイントは、オブジェクトストアを使いたい場合upgradeneededイベントで作るということです。また、途中で新しいオブジェクトストアを使いたくなった場合は、データベースのバージョンを上げる必要があります。なぜなら、既にデータベースが作成されている人は、バージョンが上がらないとupgradeneededイベントが発生しないからです。

ちなみに、オブジェクトストアを削除するdeleteObjectStoreメソッドもあり、これもcreateObjectStoreと同様に、versionchangeトランザクション中しか呼べません。引数は削除するオブジェクトストア名のみです。

さて、createObjectStoreには第二引数を指定することができるといいましたが、ここまで登場していませんね。第二引数はオブジェクトです。複数(といっても2つですが)のオプションをまとめて一つの引数で渡すためにオブジェクトの形になっています。

すなわち、渡すオブジェクトのプロパティとして、各オプションを教えてあげるわけです。オプションとなるプロパティ名は2つ、keyPath(文字列またはnull)、autoIncrement(真偽値)です。例えば、次のように渡します。


db.createObjectStore("foo", {
  keyPath:"hoge",
  autoIncrement:true
});

これら2つのオプションは、keyに関係してきます。そこで次回は、keyとは何かから説明したいと思います。