[HTML5] Indexed DBで検索結果をキャッシュする #3
Indexed DBで検索結果をキャッシュする #2 の続きです。
実装(続き)2>
リストの描画
オブジェクトストアからデータを読み込み、画面に表示します。
//1ページに表示する件数 var RECORDS_IN_PAGE = 20; //トランザクション開始 readonlyモード var tx = db.transaction(STORE_NAME, 'readonly'); //トランザクションを介してオブジェクトストアを参照 var store = tx.objectStore(STORE_NAME); //ソートに使用するインデックス名を画面から取得 var indexName = $('#sort').val(); //ページの位置を画面から取得 var fromNum = $('#page').val() * RECORDS_IN_PAGE; //ソート項目に応じたカーソルを開く var rq; if(indexName) { rq = store.index(indexName).openCursor(); } else { rq = store.openCursor(); } //現在のカーソル位置 var current = 0; //tbody内に挿入するHTML var rows = ''; $('personTable body').empty(); rq.onsuccess = function(e) { cursor = rq.result; if(cursor) { if(current < fromNum) { current = fromNum; //ページの位置までカーソルを進める cursor.advance(fromNum); } else { //カーソル位置のデータを取得し、HTMLを作成する var data = cursor.value; rows += '<tr data-key="' + data.id + '"><td>'; rows += data.id + '</td><td>'; rows += data.name + '</td><td>' rows += data.nameKana + '</td><td>'; rows += data.zipcode + '</td></tr>\n'; current++; if(current < RECORDS_IN_PAGE + fromNum) { //カーソルを次に進める cursor.continue(); } else { //リストを描画 $('#personTable body').append(rows); } } } else { //リストを描画 $('#personTable body').append(rows); } }; [/javascript] <p> 連続してデータを操作する場合、カーソルを使用します。カーソルは、オブジェクトストア自身またはそのインデックスから開きます。前者の場合、データはキーのフィールドでソートされます。後者の場合は対象のインデックスの対象フィールドでソートされます。 </p> <p> カーソルの移動は、カーソルオブジェクトの continue または advance メソッドを使用します。カーソルに設定された範囲と順序に応じて、一件または指定件数分カーソルが移動し、再度 onsuccess コールバックが呼び出されます。データの末尾に達した場合、result は null となります。 </p> //次のレコードへ移動 cursor.continue(); //指定件数先のレコードへ移動 cursor.advance(件数);
注意すべき点として、これらの処理は非同期で実行されます。このため、カーソルを移動するたびに DOM の操作を行うような処理を記述してしまうと、カーソル移動に伴う onsuccess の度に繰り返し何度も画面の描画更新が発生し、描画パフォーマンスに悪影響が出る恐れがあります。上記の例では、1ページ分の繰り返し処理が終わるか、データの末尾に達して初めて DOM を操作しています。
また、トランザクションを readwrite モードで開始した場合、カーソルオブジェクトの update メソッドでデータの更新、delete メソッドでデータの削除ができます。
//カーソル位置のデータを更新 cursor.update({新しい値}); //カーソル位置のデータを削除 cursor.delete();
検索結果のフィルタ
検索結果を、氏名カナの前方一致でフィルタします。
$('find').on('click', function() { var searchStr = $('nameKana').val(); //トランザクション開始 readonlyモード var tx = db.transaction(STORE_NAME, 'readonly'); //トランザクションを介してオブジェクトストアを参照 var store = tx.objectStore(STORE_NAME); //キーレンジの上限値文字列の生成(下限値+Unicodeの最後の文字) var lastStr = searchStr + '\uFFFF'; //キーレンジ作成 var range = IDBKeyRange.bound(searchStr, lastStr); //キーレンジを引数にカーソルを開く var rq = store.index('byNameKana').openCursor(range); $('personTable body').empty(); var rows = ''; rq.onsuccess = function(e) { if(rq.result) { //カーソル位置のデータを取得し、HTMLを作成する var data = rq.result.value; rows += '<tr data-key="' + data.id + '"><td>'; rows += data.id + '</td><td>'; rows += data.name + '</td><td>' rows += data.nameKana + '</td><td>'; rows += data.zipcode + '</td></tr>\n'; //カーソルを次に進める rq.result.continue(); } else { //リストを描画 $personTableBody.append(rows); } } });
氏名カナに作成したインデックスを使用して、前方一致検索を行います。前方一致検索のキーレンジを作成するには、入力された文字を下限値とし、その値の末尾に\uFFFFを付加した文字列を上限値とします。カーソルを進めてリストを描画する要領は先ほどと同じですが、カーソルが末尾に達するまでヒットした件数がわからないため、全件表示にしました。
また、今回使用した bound 以外にも、様々なキーレンジの指定方法があります。
//特定の値のみ range = IDBKeyRange.only(値); //下限 range = IDBKeyRange.lowerBound(下限値, 以上:false / 超える:true); //上限 range = IDBKeyRange.upperBound(上限値, 以下:false / 未満:true); //範囲 range = IDBKeyRange.bound(下限値, 上限値, 以上:false / 超える:true, 以下:false / 未満:true); //昇順 durection = 'next'; //昇順(重複無し) durection = 'nextunique'; //降順 durection = 'prev'; //降順(重複なし) durection = 'prevunique'; rq = store.openCursor(range, direction);
詳細表示
クリックされた行のデータを取得し、全ての情報を詳細欄に表示します。キーが特定できるデータの取得は、カーソルを使用した場合と比べて簡単です。こちらを先に解説すべきなのですが、アプリケーションの構成上後回しになってしまいました。
$('#personTable tbody').on('click', 'tr', function() { //トランザクション開始 readonlyモード var tx = db.transaction(STORE_NAME, 'readonly'); //トランザクションを介してオブジェクトストアを参照 var store = tx.objectStore(STORE_NAME); //イベント対象のTRノードからキーを取得 var key = $(this).attr('data-key'); //キーを元にデータを取得 var rq = store.get(parseInt(key)); rq.onsuccess = function() { //データを詳細欄に表示 $('#lId').text(data.id); $('#lName').text(data.name); $('#lNameKana').text(data.nameKana); $('#lSex').text(data.sex); $('#lBloodType').text(data.bloodType); $('#lBirthday').text(data.birthday); $('#lZipcode').text(data.zipcode); $('#lAddress').text(data.address); $('#lAddressKana').text(data.addresskana); } });
Javascript は、== 演算子を使用すると(それが良いかどうかは別として)柔軟に型変換して比較を行ってくれますが、Indexed DB のキー値の比較に関しては、厳密な比較が行われます。今回、id の値は数値であるため、HTML の属性から取り出した String 値をそのまま get メソッドに渡しても、該当無しとなってしまうので注意が必要です。
接続の切断とデータベース削除
実際のアプリケーションでは、アクセスの度にデータベースを作り直す必要は無い場合が多いと思いますが、今回はサンプルアプリケーションなので、PC に余分なゴミが残らないよう、ウィンドウを閉じる際にデータベースを削除しておきます。Indexed DB のデータベースは、明示的に削除しない限りブラウザに永続的に残ります。
$(window).on('unload', function() { if(db) { db.close(); indexedDB.deleteDatabase(DB_NAME); } });
完成
これで完成です。実際に動かしてみます。
ページ遷移とソート順の変更が凄まじく速いですね。
まとめ
お気づきかとは思いますが、今回作成したアプリケーションの機能は、Indexed DB を使用しないで、配列操作のみで実現する事も可能です。しかし、Indexed DB を活用すると、一旦データをオブジェクトストアに格納した後は、全てのデータの配列をメモリ内に変数として保持する必要も無くなりますし、ソート順を変更するたびに実際に配列をソートする必要もありません。実際に動作させてみても分かる通り、非常に高速に動く事がわかります。工夫次第で、様々な用途に応用出来るのではないかと感じました。