[HTML5] Indexed DBで検索結果をキャッシュする #3

Indexed DBで検索結果をキャッシュする #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_sample

ページ遷移とソート順の変更が凄まじく速いですね。

まとめ

お気づきかとは思いますが、今回作成したアプリケーションの機能は、Indexed DB を使用しないで、配列操作のみで実現する事も可能です。しかし、Indexed DB を活用すると、一旦データをオブジェクトストアに格納した後は、全てのデータの配列をメモリ内に変数として保持する必要も無くなりますし、ソート順を変更するたびに実際に配列をソートする必要もありません。実際に動作させてみても分かる通り、非常に高速に動く事がわかります。工夫次第で、様々な用途に応用出来るのではないかと感じました。