uhyohyo.net

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

十四章第二回 Indexed Database

今回紹介するIndexed Database、通称IndexedDBは、前回のStorageが進化したようなもので、ある種のデータベースをJavaScriptから作ってブラウザに保存しておいてもらえるものです。

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

データベースの構造

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

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

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

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

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

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

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

データベースを扱う

それでは、JavaScriptにおけるIndexedDBの使い方を見ていきます。IndexedDBには同期API非同期APIがありますが、ここで主に解説するのは非同期APIです。非同期とはイベントを用いて操作の結果を得るやつです。やはり同期よりもこちらのほうが使い勝手が良いため、よく利用されています。

IDBRequest

まず最初にIDBRequestというオブジェクトを紹介します。これはIndexedDBに対して何か操作を行った時の結果を返すもので、EventTargetの一種(イベントが発生するオブジェクト)です。

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

さらに操作に対しては結果が存在します。例えばデータベースからデータを読み込んだなら、その読み込まれたデータが結果としてIDBRequestから取り出せます。結果はresultプロパティに入っています。resultプロパティの中身は、当然ながらどんな操作をしたかによって異なります。

実は、データベースに対して何かを要求した場合、即座に結果のIDBRequestオブジェクトが得られます。ただし非同期ですから、IDBRequestを手に入れた直後はまだ結果は分かりません。IDBRequestのreadyStateプロパティをみると、操作が完了して結果が取り出せるかどうか分かります。readyStateはいろいろなオブジェクトで出てきて、数値という印象が強いように思えますが、ここでは文字列で、2種類あります。次のようになっています。

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

ただし普通は、いちいちreadyStateで確かめるのではなくて、successイベントやerrorイベントが発生したら処理終了と判断します。

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

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

データベースにアクセスするには、IDBFactoryと呼ばれる種類のオブジェクトを使います。IDBFactoryのインスタンスであるindexedDB変数が、windowのプロパティとして用意されています(正確にはIDBEnvironmentのプロパティですが、そんなに気にすることはありません)。ちなみにブラウザ上では、windowのもつプロパティはグローバル変数としてアクセス可能です。

console.log(window.indexedDB);	//IDBFactoryオブジェクトであることが表示される
console.log(indexedDB);	//実はグローバル変数としてもアクセスできる

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

いきなり返り値の説明から入りますが、先ほどの説明からいけば返り値はIDBRequestのはずですね。しかしいきなりここでは、IDBRequestの進化系であるIDBOpenDBRequestが登場します。しかしこれはIDBRequestを継承したもので、基本は変わりません。違いは、blockedイベントupgradeneededイベントが追加されているだけです。この二つはあとで紹介します。

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

動作は単純に、そのデータベースを開くというものですが、そのデータベースが存在しない場合は作ってくれます。つまり、とにかくデータベースを使いたければopenということです。それでは、引数として与えられたバージョンの意味を説明します。

引数のバージョンの意味は、簡単に言うと「このバージョンで開きたい!」と宣言するものです。データベースというのは、自分のバージョンを記憶しています。バージョンというのは、そのデータベースにどのようなデータが入っているかを示すもので、もしアプリのアップデート等によりデータ構造が変わってしまったら、バージョンを上げる必要があります。

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

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

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

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

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

さて、無事データベースを開けた場合は、IDBOpenDBRequestのresultプロパティには、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);
});

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

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

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

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

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

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

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

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

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

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

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

トランザクションとは実際にデータベースを操作するときに使う仕組みで、まだオブジェクトストアの話をしていますから順序が逆になってしまうのですが、そういう仕様になっているので仕方ありません。

しかしここではトランザクションには深入りしません。内部ではいろいろと複雑なことをしていますが、以下では表面的な説明をします。

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

ちなみに、存在しないデータベースに対してopenメソッドを使用して新しいデータベースを作った場合には、必ずversionchangeトランザクションが発動します。なぜなら、バージョンとして引数に指定可能な数字は1以上であるのに対し、新しくデータベースを作成した場合、バージョンは0として扱われるからです。このように、初めての場合もversionchangeトランザクションが起きるというのは実は重要です。

さて、versionchangeトランザクションが発動すると何が起こるかというと、表面的に起こるのは、openの返り値であるIDBOpenDBRequestにおいて、upgradeneededイベントが発生します。upgrade(更新)needed(が必要)という意味です。つまり、upgradeneededイベントは、新しいバージョンのデータベースが必要なので、古いデータベースを新しいやつに対応させて下さいと言っているのです。だからそれに対する処理として、新しいデータベースに必要なオブジェクトストアをcreateObjectStoreによって追加したり、いらないオブジェクトストアをdeleteObjectStoreで消したりなどの処理をすればいいわけです。

また、はじめてデータベースを作ったときもversionchangeトランザクション、しいてはupgradeneededイベントが発生するので、ここでデータベースの初期化をしてやればいいのです。つまり、コードで表すとこんな感じです。

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が入っている
});
          

実はcreateObjectStoreは、このようにversionchangeトランザクション中に呼ばないとエラーになるのです。だから上で、この方法専用と述べたのです。

また、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こ全部が追加されます。

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

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

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

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

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