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

ようやくここまできましたあ。ラングオブジェクト(Range)を紹介します。

自分の備忘録として、いつものように検証しながら進行いたします。これまた長くなりそうだなあ。

IndexedDBにはまってます。(その10)の最後にも記述した通り、データベースでは複数のレコードを対象にする機能があり、それを実行するためにラングオブジェクトを利用します。

このオブジェクトにレコードの対象範囲をしっかり登録してからデータベースに渡して、複数のレコードを利用します。

ラングオブジェクトはインデックスキー(補キー)やカーソルを運用する際にも必要になり…、というかそっちが本番と言ってよいでしょう。でもー、それらを紹介をしてややこしくなる前にラングオブジェクトの準備を済ませてしまおうという考えです。

少し試してみたところ、数値と文字列、日付、配列が大丈夫で、真偽値や連想配列(オブジェクト)はダメでした。
空文字(”)やスペースを配列に入れても大丈夫でしたが、真偽値やnullは配列に入れてもダメでした。(IndexedDB APIキーで確認できます。IndexedDBにはまってます。(その3)でも紹介してます。)ほかは試してません。

そのキーには次のように記述があります。

キーは 文字列、date、浮動小数点数値、配列のいずれかの型を使用できます。配列では、キーは空の値から無限大までの範囲を使用できます。また、配列の中に配列を含めることができます。文字列または整数値のキーしか使用できないという条件はありません。

IndexedDBではキー値の大小のお約束に従い、自動的にレコードを並べ替えて格納してくれます。(ありがたや。ただし、私の確認の限り。)

プライマリキー(主キー)だけでなくインデックスキー(補キー)でもそうなると予想できます。(ほんとだろうか?それを頭で考えると脳汁が鼻から出そうです。データベースってすごいですね。)

ともかく、(その8)にある「多様なプライマリキー(主キー)のレコード」画像(以下に再掲載)を拡大してみてると、無造作に格納したレコードが、インデックスキー(補キー)の大小で並んでいることをみて取れます。

多様なプライマリキー(主キー)のレコード

以上のことを踏まえて、ラングオブジェクトの生成にかかります。

IDBKeyRangeにはラングオブジェクトの生成メソッドが四種類あって、それらを個々に紹介していくのは面倒に感じます。どうせまとめて利用することになりますので、ラングオブジェクトの生成を四種類まとめて確認できるようにと独自にnewRange関数(メソッド)を準備しました。

(どこかに不備があるやもしれませんが、とりあえずは大丈夫だと思います。)

つぎのコードをWebブラウザ(Google Chromeを利用)のコンソールにコピー&ペーストし、エンターキーで実行するとラングオブジェクトが生成されます。

newRange = function({start, end}) {
	let r = null;
	console.log(`ラング作ります_>>_はじめ_>${start.v}_${start.b}_おわり_>${end.v}_${end.b}`);
	//対象キーの値(start.vとend.v)が存在しない、もしくは連想配列(オブジェクト)であった場合は「null」を代入する。
	if(typeof(start.v)==='undefined') start.v = null;
	else if(!Array.isArray(start.v)) {
		if(typeof(start.v)==='object') start.v = null;
	}
	if(typeof(end.v)==='undefined') end.v = null;
	else if(!Array.isArray(end.v)) {
		if(typeof(end.v)==='object') end.v = null;
	}
	//開閉の真偽値(start.bとend.b)がそれぞれ有効であるかどうかを真偽の配列にしてflagに代入する。
	const flag = {s:typeof(start.b)==='boolean', e:typeof(end.b)==='boolean'};
	if(flag.s && flag.e) {
		//始端と終端の双方の開閉真偽値が有効である。
		if((start.v && end.v) && (start.v <= end.v)) {
			r = IDBKeyRange.bound(start.v, end.v, start.b, end.b);
		}
	} else if(flag.s && !flag.e) {
		//始端の開閉真偽値が有効である。
		if(start.v) r = IDBKeyRange.lowerBound(start.v, start.b);
	} else  if(!flag.s && flag.e) {
		//終端の開閉真偽値が有効である。
		if(end.v) r = IDBKeyRange.upperBound(end.v, end.b);
	} else {
		//始端と終端の双方の開閉真偽値が無効である。
		if(start.v && !end.v) r = IDBKeyRange.only(start.v);
		else if(!start.v && end.v) r = IDBKeyRange.only(end.v);
	}
	console.log(`RANGE_>${r}`);
	//ラングオブジェクトをコンソールに出力する。
	console.dir(r);
	return r;
}
コンソールで生成されたnewRange関数オブジェクト

試しにつぎのコードをコンソールで実行してみます。

rng = newRange({start:{v:3, b:false}, end:{v:9, b:false}});

ラングオブジェクトが生成されて変数rngに代入されました。

newRange関数の中でラングオブジェクトをコンソールに出力するように仕込んでますから、つぎのようにラングオブジェクトの中身を見ることができます。

コンソールに出力されたラングオブジェクト

それでは関数の説明をします。少しややこしいかもしれません。

newRange()関数に渡す引数(パラメータ)は連想配列オブジェクトが一つ、さらにその中にstartとendの連想配列オブジェクトが入ります。(明示的にしたかったので容赦ください。)

ラングオブジェクトの生成すなわち目的の範囲を作るには、始めと終りそれぞれの情報が必要です。IDBKeyRangeにあるメソッドの引数の数はまちまちです。よって、必要に応じて操作がまちまちになってしまいます。私はそれを嫌い、newRange()関数ではとりあえず始めと終りの情報をフルセットで受け取るようにしました。

引数のstartは生成する範囲の始端(範囲下限)で、endは終端(範囲上限)の指定になります。それぞれに{v:キー値, b:真偽値}と連想配列を入れます。(vはvalue、bはbooleanの略です。)

範囲の始端(範囲下限)の指定

  • start.vは始端キー(プライマリキーやインデックスキー)の値。
  • start.bは始端キー値において開くか閉じるかの真偽値。

終端(範囲上限)の指定

  • end.vは終端キー(プライマリキーやインデックスキー)の値。
  • end.bは終端キー値において開くか閉じるかの真偽値。

start.vとend.vはプライマリキー(主キー)の値です。IndexedDBではキー値の大小が自動的に判定され、レコードが選り分けられてますから、start.vよりend.vの方が大きな値でなくてはなりません。

start.bとend.bは指定キー値において開くか閉じるかの真偽になります。この概念がちょっと判りにくいのですが出力したラングオブジェクトを開いて確認するとなんとなーく理解できます。

なお、IDBKeyRangeにあるメソッドでは、この真偽値が省略可となっていて初期値は偽(false)になってます。でも、newRange()関数では明示的?に真偽値を指定しないと返り値はnullになります。(ご注意ください。)

引数を確認するコンソール出力行のつぎのコメントに「//対象キーの値(start.vとend.v)が存在しない、もしくは連想配列(オブジェクト)であった場合は「null」を代入する。」とあり、その下数行ではキー値として扱えない引数は問答無用でnullに差替えてしまいます。

始端と終端の両方がnullになると、やはり関数の返り値はnullになります。

これで余計な例外は排除できます。

その操作の後のコメントに「//開閉の真偽値(start.bとend.b)がそれぞれ有効であるかどうかを真偽の配列にしてflagに代入する。」とあり、start.bとend.bのデータ型が真偽値であるかどうかの真偽値を連想配列flagに仕込みます。(これいいアイデアだと思うのですよ。自画自賛。)

その後から、ラングオブジェクト生成の本番です。

生成の条件から外れると、処理がスルーされて関数の返り値がnullになる作りになってます。

条件文の第一ブロックのコメントに「//始端と終端の双方の開閉真偽値が有効である。」とあるように、両方の開閉指定が真偽値である場合です。

始端(範囲下限)と終端(範囲上限)双方のキー値を扱うのは本条件に限られます。双方の値が有効であり且つ大小が正しいという内包条件に合致した場合、IDBKeyRangeのbound()メソッドでラングオブジェクトを生成します。

bound()メソッドの詳細はMDNの「IDBKeyRange.bound()」で確認できます。(感謝)

条件に合わなければ関数の返り値はnullになります。

例えば、つぎのコードは両方の開閉指定を偽にした指定で、↑さきにコンソールで実行しました。

rng = newRange({start:{v:3, b:false}, end:{v:9, b:false}});

まだ、メモリ上の残っていますから、つぎのコードでラングオブジェクトを再度確認してみましょう。

console.dir(rng);
コンソールに出力されたラングオブジェクト

IDBKeyRangeオブジェクト内にlowerとupper(始端と終端)、そしてlowerOpenとupperOpen(各開閉)が引数の通りに指定されているのをみて取れます。

開閉指定を偽にした指定場合は、もし、プライマリキー(主キー)の値が連続していれば、「3, 4, 5, 6, 7, 8, 9」の値のレコードが対象となります。始端終端が閉じている時はキー値が含まれます。

生成されたラングオブジェクトに対して「IDBKeyRange.includes()」(感謝。)で範囲に含まれているかどうか真偽値で確認ができます。つぎのコードをコンソールで実行します。

rng.includes(3)
rng.includes(9)
範囲内なのでそれぞれ真(true)になる。

それぞれtrueが返ってきます。

つぎに両方の開閉指定を真にした指定で、コンソールで実行します。

rng = newRange({start:{v:3, b:true}, end:{v:9, b:true}});

プライマリキー(主キー)の値が連続していれば、「4, 5, 6, 7, 8」の値のレコードが対象となります。始端終端が開いている時はキー値が含まれません。

同じくincludes()メソッドで確認してみます。

rng.includes(3)
rng.includes(9)
範囲外なのでそれぞれ偽(false)になる。

それぞれfalseが返ってきます。

では「//始端の開閉真偽値が有効である。」の条件に引っかかる場合です。

ここではlowerBound()メソッドによってラングオブジェクトが生成されます。やはり、条件から外れれば関数の返り値はnullになります。

rng = newRange({start:{v:3, b:false}, end:{v:null, b:null}});
//もしくは
rng = newRange({start:{v:3, b:false}, end:{}});
コンソールに出力されたラングオブジェクト

始端は「//始端と終端の双方の開閉真偽値が有効である。」と同じですが、終端は開いています。プライマリキー(主キー)の値が連続していれば、「3, 4, 5, 6, 7, 8 …」と、プライマリキー(主キー)が「3」以上の値のレコードが全て範囲になります。

「開く」の意味がわかりやすいですよね。どうやら範囲内であろうとなかろうと指定の値から先を開くか閉じるという概念のようです。

(閑話)

堀に囲まれた城あり、堀の外側に検問所(キー値)がある情景を思い浮かべました。堀には検問所に向かって跳ね橋が架っていて、橋の上がっている時は堀が生き(真)ていて検問所は城外、橋がかかっている時は堀が死ん(偽)でいて検問所が機能するというものです。ちょっと変かな?

(閑話休題)

今度は終端「//終端の開閉真偽値が有効である。」の場合です。

ここではupperBound()メソッドによってラングオブジェクトが生成されます。今度は始端の場合と逆になり、「9」以下の値のレコードが全て範囲になります。

rng = newRange({start:{v:null, b:null}, end:{v:9, b:false}});
//もしくは
rng = newRange({start:{}, end:{v:9, b:false}});
コンソールに出力されたラングオブジェクト

最後に「//始端と終端の双方の開閉真偽値が無効である。」の場合です。

only()メソッドを利用する場合です。only()メソッドの引数は一つの引数になるのですが、newRange()関数では始端でも終端でもどちらでも指定が可能です。

rng = newRange({start:{v:3, b:null}, end:{v:null, b:null}});
//もしくは
rng = newRange({start:{v:3}, end:{}});
rng = newRange({start:{v:null, b:null}, end:{v:3, b:null}});
//もしくは
rng = newRange({start:{}, end:{v:3}});
コンソールに出力されたラングオブジェクト
コンソールに出力されたラングオブジェクト

始端と終端の値が同じで、閉じた状態であることから、次のように実行してbound()メソッドで生成しても同じ結果になります。

rng = newRange({start:{v:3, b:false}, end:{v:3, b:false}});
コンソールに出力されたラングオブジェクト

なお、どんなに省略しても「newRange({start:{}, end:{}});」としないと文法上のエラーになります。

この場合もnullが返ってきます。

なにはともあれラングオブジェクトなるものが生成できるようになりました。

つぎはこれの利用方法になります。ようやく、データベースの運用パラダイムを上がっていく準備ができたように感じます。

ところで、たびたびnewRange関数の返り値がnullになる確認をしましたが、それはそれで指定範囲がないという操作上の意味があります。

たとえば、get()メソッド以外にgetAll()というメソッドもあるのですが、引数はラングオブジェクトです。

もし、引数がnullとなるとすべてのレコードが対象になりますから、データベースの格納レコードを一気に取得するときに使えそうです。そかさ。