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

IndexedDBにはまってます。(その7)では「次回はキージェネレータをオフにした操作を紹介します。」と締めましたが、厄介なことに気づきました。

オブジェクトストアのオプションの指定において、プライマリキー(主キー)とキージェネレータの指定には相互に関係があり、ごちゃごちゃ?してます。

不明なこともあり整理といえるレベルではありませんが、検証して備忘録にいたします。

本稿だけで一応のまとまりをつけるため長くなります。すみません。

(その7)でもそうですが、はまってシリーズでは、オブジェクトストアの生成に際して、オプションを準備してから、改めて生成するという流れで、次のように記述してます。

//オブジェクトストアのオプション指定を変数に代入する。
const Store_option = {keyPath:'member', autoIncrement:true};
//オブジェクトストアの生成メソッドのパラメータとする。
const objectStore = db.createObjectStore('Store_name', Store_option);

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

const openRequest = indexedDB.open('DB_name');
openRequest.addEventListener('upgradeneeded', (event) => {
	const db = event.target.result;
	//const tx = event.target.transaction;
	console.log(`UPGRADE_DB_VER_>${db.version}_OLD_>${event.oldVersion}_NEW_>${event.newVersion}`);
	//今回の主題は次の行になります。
	//オブジェクトストアのオプション指定を変数に代入する。
	const Store_option = {keyPath:'member', autoIncrement:true};
	try {
		//オブジェクトストア操作箇所
		//Store_optionをオブジェクトストアの生成メソッドのオプションパラメータとする。
		const objectStore = db.createObjectStore('Store_name', Store_option);
		objectStore.transaction.addEventListener('complete', (event)=>{
			if(event.target.objectStoreNames.contains('Store_name')) {
				console.log(`オブジェクトストアを生成しました。`);
			}
		}, false);
		objectStore.transaction.addEventListener('abort', (event)=>{console.error(`トランザクションが中断しました。`);}, false);
		objectStore.transaction.addEventListener('error', (event)=>{console.error(`トランザクションでエラーが発生しました。`);}, false);
	} catch(exception) {
		console.error(`例外が発生しました。_>${exception.message}`);
	}
}, false);
openRequest.addEventListener('success', (event)=>{
	const db = event.target.result;
	console.log(`データベースを開始しました。`);
	//db.close();
	db.addEventListener('close', ()=>{console.log(`データベースが閉鎖されました。`);}, false);
}, false);
openRequest.addEventListener('error', (event)=>{console.error(`データベースの開始に失敗しました。`);}, false);

このオブジェクトストアは、キージェネレータがオンでプライマリキー(主キー)を単一キーワードにした運用ができます。このパターンについては、いままで紹介してきたとおりです。

このあとは、このコードを変更しながら続くオブジェクトストアの生成テストをしていきます。

ゴミの残る心配もないし、オブジェクトストアを削除するよりデータベースを削除する方が簡単です。その都度、データベースを削除しながらテストすることをお勧めします。

(追記)

ここでは単一キーワードにだけ言及してますが、キーワードをピリオド区切で指定して、プライマリキー(主キー)をオブジェクトの階層概念でも運用できます。

後述(↓)の「キージェネレータがオフの場合」の記載と同様に利用できます。

ただし、キージェネレータを利用してレコードを格納する限りでは単一キーワードによる運用と変わりなく動作します。って、判りにくいですね。(追記終り)

アプリケーション画面の「データベースを削除」ボタン

キージェネレータがオン「autoIncrement:true」の場合

まず、プライマリキー(主キー)には前述(↑)の単一キーワード以外に、空文字(”)とキーワードの配列の指定が可能です。

でもキージェネレータがオンの場合は、例外が発生して蹴られます。これ仕様です。

//プライマリキー(主キー)が空文字の場合
const Store_option = {keyPath:'', autoIncrement:true};
//プライマリキー(主キー)がスペースの場合
const Store_option = {keyPath:' ', autoIncrement:true};
//プライマリキー(主キー)が配列の場合
const Store_option = {keyPath:[], autoIncrement:true};
const Store_option = {keyPath:['member', ''], autoIncrement:true};
const Store_option = {keyPath:['member', ' '], autoIncrement:true};
const Store_option = {keyPath:['member', 'id'], autoIncrement:true};
オブジェクトストアは生成できない(例外発生)

それぞれをテストする都度にデータベースを削除することをお忘れ無く。バージョンアップが発火しないので、データベースを開くだけになります。

また、ライマリキー(主キー)がスペースの場合と、配列にスペースがある場合では、例外メッセージが「有効なキーパスじゃないよ」とあります。オブジェクトストアの生成以前に、キーにはスペースを指定できません。

このあとは、必要を感じない限りスペースなど意味のなさそうな指定のテストは省きます。

プライマリキー(主キー)が有効でない例外

空文字(”)はダメなのですが「null」ではオブジェクトストアが生成されます。

これはプライマリキー(主キー)の指定がないことになります。

「//オブジェクトストアのオプション指定を変数に代入する。」の次行をつぎのコードに差し替えて実行します。

//プライマリキー(主キー)がnullの場合
const Store_option = {keyPath:null, autoIncrement:true};
//もしくは、
const Store_option = {autoIncrement:true};
プライマリキー(主キー)がnullのストアオブジェクト

プライマリキー(主キー)を指定しない場合は「アウトオブラインキー(out-of-line keys)」といってオブジェクトストアの外側に、勝手にプライマリキー(主キー)が定義されて運用されるモードになります。

逆になんらかの指定をした場合は「インラインキー(in-line keys)」といって、オブジェクトストアの内側に、任意にプライマリキー(主キー)を定義して運用するモードになります。

この外側と内側の概念は少々判りにくいです。後述(↓)の第二パラメータを利用するかしないかの違いぐらいに思っておいた方が良さそうです。

オブジェクトストアが生成されたら、試しにデータレコード(以後レコード)を格納してみます。

つぎのコードをWebブラウザのコンソールにコピー&ペーストし、エンターキーで実行します。

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', 'readwrite');
		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');
		try {
			//レコード操作箇所
			const storeRequest = objectStore.add({index:{en:1, sn:'SN0003'}, date:Date.now(), title:'ZINC', author:'hustlemouse', tag:['red','blue','yellow'], note:'Zn', member:2});

			storeRequest.addEventListener('success', (event)=>{console.log(`レコードの書き込みに成功しました。`);}, false);	
			storeRequest.addEventListener('error', (event)=>{console.error(`レコードの書き込みに失敗しました。`);}, false);
		} catch(exception) {
			console.error(`例外が発生しました。_>${exception.message}`);
		}
	} else console.error(`目的のオブジェクトストアはありませんでした。`);
	//db.close();
	db.addEventListener('close', ()=>{console.log(`データベースが閉鎖されました。`);}, false);
}, false);
openRequest.addEventListener('error', (event)=>{console.error(`データベースの開始に失敗しました。`);}, false);
コードを実行したコンソール画面
アプリケーション画面の格納レコード

レコードが格納されました。

いままで、紹介したメソッドでプライマリキー(主キー)を指定したのはdelete()メソッドだけでした。キーを指定して操作してみます。

アプリケーション画面でプライマリキー(主キー)が「1」になっていると思われるので、それをパラメータにして「//レコード操作箇所」の次行をつぎのように差し替えて実行します。

const storeRequest = objectStore. delete(1);
コードを実行したコンソール画面

削除されました。(レコードが空になったことをアプリケーション画面で確認します。)

材料の少ない推測になりますが、名前のないプライマリキー(主キー)で運用できる様子です。

では、コンソール画面にもどり↑カーソルキーを2回叩いて、もう一度レコードを追加(add)してみます。

レコードが格納されます。アプリケーション画面でレコードを確認するとプライマリキー(主キー)が「2」になってます。キージェネレータがちゃんと動いてくれてます。

ここでadd()メソッドの第二パラメータを設定してみましょう。はまってシリーズでは初めてです。レコードの後ろにカンマ切りして「5」と入れてみます。

const storeRequest = objectStore.add({index:{en:1, sn:'SN0003'}, date:Date.now(), title:'ZINC', author:'hustlemouse', tag:['red','blue','yellow'], note:'Zn', member:2}, 5);
コードを実行したコンソール画面(第二パラメータ付き)

追加を実行すると、プライマリキー(主キー)「5」のレコードが格納されます。再度、第二パラメータを付けずに追加を実行すると「6」になるはずです。随意に「1」や「3」も実行できるはずです。

アプリケーション画面の格納レコード

第二パラメータには、プライマリキー(主キー)やインデックスキー(補キー)、いわゆるキーに割り当てることができる値が使えます。

少し試してみたところ、数値と文字列、日付、配列が大丈夫で、真偽値や連想配列(オブジェクト)はダメでした。

空文字(”)やスペースを配列に入れても大丈夫でしたが、真偽値やnullは配列に入れてもダメでした。(IndexedDB APIキーで確認できます。IndexedDBにはまってます。(その3)でも紹介してます。)ほかは試してません。

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

ま、実際の運用で多様な値を使うことは、あまり想像できませんね。

気が済んだら、データベースを削除します。

キージェネレータがオフ「autoIncrement:false」の場合

ここでようやく、キージェネレータがオフの場合になります。

前述(↑)に続いて、プライマリキー(主キー)を指定しない「アウトオブラインキー(out-of-line keys)」のモードから始めて、単一キーワードに遡り、キーワードの配列まで検証します。

「keyPath:null」指定からいきます。

//オブジェクトストアのオプション指定を変数に代入する。
const Store_option = {keyPath:null, autoIncrement:false};
//オブジェクトストアの生成メソッドのパラメータとする。
const objectStore = db.createObjectStore('Store_name', Store_option);

autoIncrementの既定値は偽(false)ですから、つぎのオプションを指定しない記述だけでも生成できます。

const objectStore = db.createObjectStore('Store_name');
コンソール画面で実行されたコード

オブジェクトストアが生成されます。

レコードの格納をしてみます。

キージェネレータはオフですから、add()メソッドやput()メソッドの第二パラメータに任意プライマリキー(主キー)の値を必ず入れます。

第二パラメータの値については前述(↑)と同様です。

次のコードを使って、順次、第二パラメータを差し替えて格納の実行テストをしてみると良いでしょう。ごちゃごちゃと遊べます。

const storeRequest = objectStore.add({index:{en:1, sn:'SN0003'}, date:Date.now(), title:'ZINC', author:'hustlemouse', tag:['red','blue','yellow'], note:'Zn', member:2}, 2);

いろいろテストしてみるとアウトオブラインキーとインラインキーの違いでadd()メソッドの動作が異なることに気がついたりします。(本当かなあ。めんどいなあ。)

本題から外れそうになっちゃうし、ここで追っかけてられないので、つぎのプライマリキー(主キー)指定に進みます。

まずは、空文字(”)を指定してみます。nullと同じ性格のストアオブジェクトが生成されるかと思ったのですが、違いました。(私の検証の限りでは…)

//プライマリキー(主キー)が空の場合
const Store_option = {keyPath:'', autoIncrement:false};
アプリケーション画面の格納レコード

空文字でもオブジェクトストアは生成されます。たしかにIndexedDB APIのキーパスでは「空文字列」も有効とあります。

でも困ったことに、第二パラメータを指定してもレコードを格納できません。

プライマリキー(主キー)が空文字にして運用するようなことは想像できませんが、どうして有効なのでしょう。トラブルに気がついたときには後の祭りです。(キージェネレータがオンでは例外発生するんですよ。なんで微妙に違うかな。)

現況では、つぎのように値が空文字(”)だったらnullに差し替えるように対処しておくしかなさそうです。

if(Store_option.keyPath === '') Store_option.keyPath = null;

ようやく、オプション指定とオブジェクトストア生成を分けて記述してきた理由(ワケがあったのです。)に至った次第です。

プライマリキー(主キー)の指定条件をこの行間で検査することにより、対処する例外処理を削減できると考えます。

そして帰ってきました、プライマリキー(主キー)に単一キーワードを指定します。

//プライマリキー(主キー)が単一キーワードの場合
const Store_option = {keyPath:'member', autoIncrement:false};

キージェネレータがオフなだけで、単一キーワードの指定では特別なことはありません。

そこで、ついでに(その3)でも紹介したピリオドで区切った場合も指定してみます。

//プライマリキー(主キー)がキーワードをピリオド区切りの場合
const Store_option = {keyPath:'member.id', autoIncrement:false};
コードを実行したコンソール画面(ピリオド区切り)

(その3)と後述(↓)にもある通りオブジェクトの階層になりますので、「member{id:2}」と連想配列で指定してレコードの格納ができます。

単独配列では単一キーワードの指定とあまり違いを感じません。キーワードの配列を指定をする場合に効果がありそうです。

const storeRequest = objectStore.add({index:{en:1, sn:'SN0003'}, date:Date.now(), title:'ZINC', author:'hustlemouse', tag:['red','blue','yellow'], note:'Zn', member:{id:2}});
アプリケーション画面の格納レコード(連想配列)

最後に、プライマリキー(主キー)にキーワードの配列を指定します。

//プライマリキー(主キー)がキーワードの配列の場合
const Store_option = {keyPath:['member', 'id'], autoIncrement:false};
コンソール画面で実行されたコード(配列)

この場合、プライマリキー(主キー)はセットで運用されます。つぎの「member:2, id:3」ように必ず一緒に指定、そして運用されます。

const storeRequest = objectStore.add({index:{en:1, sn:'SN0003'}, date:Date.now(), title:'ZINC', author:'hustlemouse', tag:['red','blue','yellow'], note:'Zn', member:2, id:3});
アプリケーション画面の格納レコード(配列)

試しに配列の片方をnullとしてみたら、Google Chromeでは文字列’null’という名前のプライマリキー(主キー)として扱われるようになりました。(我ながら余計なことを…)

const Store_option = {keyPath:['member', null], autoIncrement:false};

そして、さらに配列に指定するキーワードをピリオドで区切って登録します。

const Store_option = {keyPath:['member.id', 'member.name'], autoIncrement:false};
コンソール画面で実行されたコード(ピリオド区切)

個別に登録されるのでは無くて、次のようなオブジェクトの階層概念になります。

{
	member:{
		id:2,
		name:'hustle'
	}
}

というわけで、たとえば「member:{id:2, name:’hustle’}」と連想配列で指定します。

const storeRequest = objectStore.add({index:{en:1, sn:'SN0003'}, date:Date.now(), title:'ZINC', author:'hustlemouse', tag:['red','blue','yellow'], note:'Zn', member:{id:2, name:'hustle'}});
アプリケーション画面の格納レコード(連想配列)

一通り済んだと思います。

これで、プライマリキー(主キー)とキージェネレータの指定に関しては少し解ったような気になりました。が、失念していこともあるやもしれません。

取り止めもなく、まじ長大な投稿になってしまいました。

これ以上長くしなたくないので、総括のような「まとめ」はつぎの(その8)に回します。

QiitaIndexedDBの概念がわかりづらい件について(感謝。)では、キーと値についてkeyPathとautoIncrementの指定を表にして解説してあります。

助けにはなるのですが、自分なりに検証してみると表だけでは理解が片付かないように感じました。

中途半端な印象ですが、自らのチャレンジ精神は良しとします。

不明快なこともありますから、再度(訂正?言い訳?)投稿をすることになるじゃないかと憂慮する次第です。そかさ。