IndexedDBにはまってます。(その15)

カーソルとはなんぞや?

ざっくり申しますと、カーソルはラングオブジェクトで指定された領域のレコードを順番に参照して行くための仕組みです。

はて?と思いますよね。

今までだって、ラングオブジェクトの範囲に合わせてレコードを参照できましたよね?

そうですね。プライマリキー(主キー)だけで運用して、且つ、個人だけで使うような小さなデータベースだったら、いままで紹介した内容で済むかもしれません。でも、その程度の利用でよければスプレッドシートで事足りてしまいます。

たとえ個人の情報といえども、この情報化社会にあってはリレーショナルデータベース級に運用したくなりそうなものです。

そのためには、個別のレコードに動的にコミットできる仕組みが必要であると思うのです。

いままでのオブジェクトストアでは、ラングオブジェクトの領域を一気に処理してましたが、同じくオブジェクトストアの機能であるカーソルはレコードを順番に参照していきます。

重要です。カーソルはレコードを個別に順番に処理します。よって、個別に操作が可能になり、レコードの更新などに有効です。

また、今後紹介するインデックスキー(補キー)もカーソルによる利用ができます。

補キーが加わって、さらに強力に、複合的なデータベースの運用が可能になると期待できます。(脳漿にかかる負荷も期待できます。)

ということで、さてはカーソルです。

まずは操作するレコードを準備しましょう。

(その14)の「オブジェクトストアStore_name生成」、プライマリキー(主キー)を配列(しかも階層付き)にて、のコードをWebブラウザ(Google Chromeを利用)のコンソールにコピー&ペーストし、エンターキーで実行します。

コンソール画面の入力と結果表示

つづいてIndexedDBにはまってます。(その13)の「オブジェクトストアStore_name生成」コードの次にある「配列」コードをコンソールにコピー&ペーストして配列オブジェクトを準備します。

コンソール画面で配列を確認

さらに(その14)の「オブジェクトストアStore_name生成」コードのつぎの「レコード格納」コード(トランザクションがreadwriteモードのやつです。)をコンソールにコピー&ペーストし…、addメソッドの第2パラメータを削除したやつ。できますよね?

コンソール画面の入力と結果表示
オブジェクトストアに格納されたレコード

というわけで、レコード格納までは(その14)と同じでした。

そして、ラングオブジェクトを生成するメソッドをIndexedDBにはまってます。(その11)からコピー&ペーストしてコンソールに登録してください。

ところで、このメソッドは今投稿では問題なく動作してくれます。(難しい指定をしなければ…、不等号と等号のセットで行けてしまいます。)

でも、将来のために改善?をいたします。現在は検証中につき、ここでは、まま利用してください。

コンソール画面でメソッドを登録

では、カーソルを使ってレコードを操作してみます。

この投稿のずっと下の方を確認ください。コードの冒頭からラングオブジェクト生成までは、(その14)のままです。その後のtry文のセットがごっそり入れ替わります。

個人的な印象ですが、これまではオブジェクトストアへのリクエストでしたが、ここからはオブジェクトストアに生成されるカーソルへのリクエストになって、さらに一階層潜る気分です。

手順は似ていても、さらにカーソルが移動するためのパラメータが絡んでくるので複雑になります。

まずはシンプルに、カーソルの移動方向を指定するパラメータに絞って、基本的な操作によるレコード取得をします。

カーソルの移動方向を指定するプロパティ「direction」のキーワードには、つぎの4つがあります。

direction(読み取り専用)のキーワード

  • ‘next’は、ラングオブジェクトの範囲の先頭からアクセスして末尾に向かって参照します。
  • ‘prev’は、ラングオブジェクトの範囲の末尾からアクセスして先頭に向かって参照します。
  • ‘nextunique’は’next’と同順に操作しますが、重複した値を持つインデックスキー(補キー)については、先頭から最初にアクセスしたレコードだけを参照します。
  • ‘prevunique’は’prev’と同順に操作しますが、重複した値を持つインデックスキー(補キー)については、末尾から最初にアクセスしたレコードだけを参照します。

ここまではプライマリキー(主キー)だけを紹介しており、主キーは一意の値を持ってオブジェクトストアの中でソートされています。前後に移動しても必ずユニークなレコードが参照されます。

よって、今のところは’next’と’prev’だけを対象に検証します。

いまは余談ですが、’nextunique’と’prevunique’については一意とは限らないインデックスキー(補キー)を運用する場合に意味があるようです。私の確認の限りでは、これらを主キーに対して使っても’next’と’prev’と同じ動作になります。

まずは基本的な’next’キーワードの指定で動作させます。コードの記述を確認します。

//カーソル移動方向キーワード
const direction = 'next';

移動方向だけに絞ると申しましたが、後で改めるのが面倒なので、カーソル移動速度について「カーソル移動ステップ」を記述しておきます。

//カーソル移動ステップ(整数)
const cursorStep = 1;

この後ろが、挿げ替った新しいtry文です。

オブジェクトストアへのリクエストと同様に、カーソルへのリクエスト生成には例外が発生するのでtry文を使います。

まずは、操作目的に合わせて、openCursoropenKeyCursorメソッドを使ってカーソルリクエストを生成します。

openCursorメソッドから紹介します。メソッドのパラメータには前出の、ラングオブジェクトとカーソル移動方向キーワードを指定します。

//オブジェクトストアのカーソルを生成する。
const cursorRequest = objectStore.openCursor(range, direction);

そのままtry文の中で記述を続けることもできるのですが、生成されたカーソル生成の成功と失敗イベントそれぞれの発火を受ける枠を記述することになります。且つ、カーソル動作の中で利用されるメソッドでも例外が発生し、今回はありませんが、さらに成功と失敗イベントそれぞれの発火を受けることになります。(うへーって思いますよね。)

可能な限り入れ子表記を少なくしたいので、つぎのように記述してカーソルリクエストが存在を確認して、操作を仕切り直します。

if(cursorRequest!==null) {

そして「カーソルによるオブジェクトストア操作箇所」を記述します。

とりま、参照レコード情報を格納する配列と、動作確認用にカーソル操作回数のカウンターを準備します。

//カーソルによるオブジェクトストア操作箇所の始まり
let r = new Array();
let count = 0;

つぎは、カーソルリクエスト移動の成功イベント発火を受けての処理を行う枠のコードです。(その都度生成されるのかもしれませんが、挙動としては「移動」です。)

cursorRequest.addEventListener('success', (e)=>{
	const cursor = e.target.result;
	if(cursor===null){
		console.log(`カーソルが終了しました。`);
		//カーソルイベントが終端になると返り値がnullになる。
		if(count===0) console.error(`対象となるレコードがありませんでした。`);
		else {
			console.log(`カーソルの移動が終了しました。_全移動回数_>${count}`);
			console.dir(e.target.source);
			console.dir(r);
		}
	} else {
		console.log(`カーソルの移動は_>${count}回めです。`);
		console.log(`カーソルリクエストを参照_>${cursor.request}`);
		r.push(cursor.value);
		try {
			cursor.advance(cursorStep);
		} catch(e) {
			console.error(`カーソルの移動に失敗しました。`);
			console.dir(e);
		}
		count++;
	}
}, false);
cursorRequest.addEventListener('error', (e)=>{
	console.error(`カーソルの移動の_>${count}回めを失敗しました。`);
}, false);
//カーソルによるオブジェクトストア操作箇所の終わり

上のコードの説明です。

今までのはまってシリーズで紹介してきたイベントリスナーと挙動が異なります。

冒頭の方で、ラングオブジェクトの範囲にて順番にレコードを参照できる旨を申し述べましたよね。ご丁寧にも、ひとつひとつのレコードを参照できるようになる度に、その移動の成功と失敗のイベントが発火するのです。

ですから、成功と失敗イベント枠のセットで、for文の中身のようになっていると理解してください。

(長くなりますが、セットになる失敗イベント枠のコードも下に記述してあります。)

カーソルがクルクル回り(移動)続けるとその度に成功イベントが発火して、受け取り枠にイベントが返ってきます。
最初にそのイベントの中の成果(この定数cursorからレコードを参照します。)を判りやすいように取得します。

const cursor = e.target.result;

クルクル回って、ラングオブジェクトの範囲を越えてしまうと、定数cursorに入ってくる値がnullになります。それ以降は成功イベントの発火は止まるようです。

定数cursorのプロパティーにはレコードオブジェクトが入っているので、nullであれば何もしようがありません。ということで、つぎのように定数cursorがnullだったら移動終了の処理、そうでなければ移動中のレコードの処理を行い、カウンターをインクリメントします。

if(cursor===null){
	//カーソルの移動終了の処理
} else {
	//カーソルが移動中のレコード処理
	count++;//カウンターをインクリメントする。
}

「カーソルの移動終了の処理」の中身です。

if(cursor===null){
	console.log(`カーソルの移動が終了しました。`);
	if(count===0) console.error(`対象となるレコードがありませんでした。`);
	else {
		console.log(`カーソルの移動が終了しました。_全移動回数_>${count}`);
		console.dir(e.target.source);
		console.dir(r);
	}
} else {

冒頭で「if(count===0)」と条件しています。

対象になるレコードが存在しなくてもカーソルリクエストの移動成功イベントは発火しますので、初っ端から定数cursorにnullが入ってきます。もちろん、カウンターは動きません。

また、たとえ対象になるレコードがあっても、任意で処理せず、且つ、カウンターを進めないことも可能です。どうするかは開発要件次第になりますのでご留意ください。(←ちと解りにくいですね。)

ここでは一応、エラーをコンソールに出力します。

カウンター「0」でなければ、何かしら成果があったということでそれらをコンソールに出力します。

そして、「カーソルが移動中のレコード処理」の中身です。

} else {
	console.log(`カーソルの移動は_>${count}回めです。`);
	console.log(`カーソルリクエストを参照_>${cursor.request}`);
	r.push(cursor.value);
	try {
		cursor.advance(cursorStep);
	} catch(e) {
		console.error(`カーソルの移動に失敗しました。`);
		console.dir(e);
	}
	count++;
}

冒頭で定数cursorに入ってきたカーソルリクエストをコンソールに出力して確認してます。単なる確認です。

その次の行で同じく定数cursorに入ってきたレコードを、準備した配列にpush()して確保します。

操作が終わったので、advanceメソッドを使ってカーソルを移動させます。

advanceメソッドにはプロパティの指定が必須です。整数を指定できて移動のステップになります。

ここではcursorStepに代入しておいた「1」が入り、連続して移動します。ご想像の通り、「2」であれば一つ置き、「3」であれば二つ置きになります。試してみてください。

そして、advanceメソッドには例外も発生します。ここはtry文で包みます。

最後になりますが、成功イベント発火とセットになる、カーソルリクエストの移動の失敗イベント発火の枠のコードについては説明しなくても大丈夫ですよね。

でも一つだけ注意です。一度でもエラーが発生すれば、トランザクションが稼働してますから全ての操作が無かったことになることをお忘れなく。

説明は十分だったろうか。これを読む方は、相当解っていると期待します。(自分は棚に上げて…と)

それでは、つぎのコードをWebブラウザ(Google Chromeを利用)のコンソールにコピー&ペーストし、エンターキーで実行します。

const openRequest = indexedDB.open('DB_name');
openRequest.addEventListener('success', (event)=>{
	const db = event.target.result;
	console.log(`データベースを開始しました。`);
	if(db.objectStoreNames.contains('Store_name')) {
		const tx = db.transaction('Store_name', 'readonly');
		tx.addEventListener('complete', (event)=>{console.log(`トランザクションが完遂しました。`);}, false);
		tx.addEventListener('error', (event)=>{console.error(`トランザクションでエラーが発生しました。`);}, false);
		tx.addEventListener('abort', (event)=>{console.error(`トランザクションが中断しました。`);}, false);
		const objectStore = tx.objectStore('Store_name');
		//ラングオブジェクト生成
		const range = newRange({start:{v:[2, 'SN0001'], b:false}, end:{v:[5, 'SN0005'], b:false}});
		//カーソル移動方向キーワード
		const direction = 'next';
		//カーソル移動ステップ(整数)
		const cursorStep = 1;
		let cursorRequest;
		try {
			//オブジェクトストアのカーソルを生成する。
			cursorRequest = objectStore.openCursor(range, direction);
		} catch(e) {
			console.error(`カーソルの生成に失敗しました。`);
		}
		if(cursorRequest!==null) {
			//カーソルによるオブジェクトストア操作箇所の始まり
			let r = new Array();
			let count = 0;
			cursorRequest.addEventListener('success', (e)=>{
				const cursor = e.target.result;
				if(cursor===null){
					console.log(`カーソルの移動が終了しました。`);
					if(count===0) console.error(`対象となるレコードがありませんでした。`);
					else {
						console.log(`カーソルの移動が終了しました。_全移動回数_>${count}`);
						console.dir(e.target.source);
						console.dir(r);
					}
				} else {
					console.log(`カーソルの移動は_>${count}回めです。`);
					console.log(`カーソルリクエストを参照_>${cursor.request}`);
					r.push(cursor.value);
					try {
						cursor.advance(cursorStep);
					} catch(e) {
						console.error(`カーソルの移動に失敗しました。`);
						console.dir(e);
					}
					count++;
				}
			}, false);
			cursorRequest.addEventListener('error', (e)=>{
				console.error(`カーソルの移動の_>${count}回めを失敗しました。`);
			}, false);
			//カーソルによるオブジェクトストア操作箇所の終わり
		}
	} else console.error(`目的のオブジェクトストアはありませんでした。`);
	db.close();
	db.addEventListener('close', ()=>{console.log(`データベースが閉鎖されました。`);}, false);
}, false);
openRequest.addEventListener('error', (event)=>{console.error(`データベースの開始に失敗しました。`);}, false);
コンソール画面の入力と結果表示

個別に操作がなされ、ラングオブジェクト範囲の先頭からレコードが参照できました。

それでは、カーソルの移動方向を’prev’に指定してみます。

const direction = 'prev';
コンソール画面の入力と結果表示

ラングオブジェクト範囲の末尾からレコードが参照できました。(よね。)

最後にもうひとつ。

「オブジェクトストアのカーソルを生成する。」のopenCursorメソッドをopenKeyCursorメソッドに変更します

//オブジェクトストアのカーソルを生成する。
const cursorRequest = objectStore.openKeyCursor(range, direction);

そして、受け取れるものが変わります。

前述した「レコードを、準備した配列にpush()して確保」の部分「r.push(cursor.value);」ではレコードを確保してます。でも、openKeyCursorメソッドでは「value」には何も入ってません。

ここでは「key」に主キーが入ってきますので、「value」を「key」に変更します。つぎの一番下の行です。

console.log(`カーソルの移動は_>${count}回めです。`);
console.log(`カーソルリクエストを参照_>${cursor.request}`);
r.push(cursor.key);
コンソール画面の入力と結果表示

レコードのプライマリキー(主キー)が取得できた思います。

オブジェクトストアのカーソルを生成するメソッドは、openCursorとopenKeyCursorメソッドだけなのですが、openKeyCursorメソッドは今ここではプライマリキー(主キー)を取得するためにしかつかえません。

よって、このあとはopenCursorメソッドによる紹介ばかりになります。

ようやくカーソルの紹介が始められました。

いままでもそうなのですが、いざ投稿記事を起こしてみると曖昧なままに済ませていた事象に出会いまくります。

それも都合の悪い奴柄ばかりで、それらを確認していくと、さらに問題に巡り合ってしまい。予想以上に先まで確認をしなくてはならなくなってしまいます。

学習にはなるのですが、はまってシリーズの遥か先の題目まで担保しなくてはならないのは負担を感じてしまいます。

特にカーソルからは、精神的にも来ちゃいそうです。

投稿に間違いがあるやもしれません。後々、投稿の中に「訂正」を入れることが頻発するのではないかと憂慮してます。そかさ。