IndexedDBの動きを確認する
IndexedDBはLocalStorageやSessionStorageと異なり、データベースである為、アクセスには、手順が必要です。検索機能も持ち合わせており、多機能ですが、基本通り使えば、とても簡単に扱えます。 ここでは、下記の3つのテストプログラムを使って、実際の挙動を確認していきたいと思います。
- マスターメンテナンス風テストプログラム(getAll版)
- マスターメンテナンス風テストプログラム(cursor版)
- データ検索テストプログラム
「マスターメンテナンス風テストプログラム(getAll版)」と「マスターメンテナンス風テストプログラム(cursor版)」は、動作的には、同じものです。データベースの読み込みに、getAll()を使用しているか、cursorを使用しているかが異なるだけです。 どちらもIndexedDBの基本動作(読み、書き、削除等)を確認します。
「データ検索テストプログラム」では、検索の肝となる、IDBKeyRangeオブジェクトを使用したquery検索を確認します。
マスターメンテナンス風テストプログラム(getAll版)による基本動作の確認
下記のボタンを押下することで、テストプログラムの起動が可能です。
👉 本記事のテストプログラムを動かすことで、「IndexedDBの動き」が体感できます。
テストプログラムの概要(かんたん説明)
このプログラムが保存するデータは、住所録のオブジェクトです。
例)
{
id: 1,
name: "山田こん太郎",
address: "東京都品川区羽田が丘2-10-1",
tel: "090-1234-5678",
email: "yamada_kontaro@konkitsune.com"
}
表示画面には、大きく分けて3つのエリアが存在します。
- テーブルエリア
- 検索条件エリア
- 入力欄エリア
テーブルエリア
IndexedDBに格納されているデータ全てが一覧で表示されます。(最大5件) ただし、表示されるのは、(id, name, addressのみ)
検索条件エリア
IDやnameによりIndexedDBに格納されているデータの検索が可能です。検索されたデータは、入力欄に表示され、変更後の上書きが可能です。また、新規データの格納も可能です。
入力欄エリア
データである住所録オブジェクトの各項目を表示したり、書き換え、新規入力を行うものです。
構造はいたってシンプルです。
注)IDはIndexedDBのkeyとして使用します。本プログラムでは、半角数字のみに制限。INDEXは名前のみに付加。
操作手順
手順①:本頁の「テストプログラム起動」を押す
テストプログラム起動 👉 起動時に、保存されたデータがある場合、テーブルエリアに表示されます。
手順②:データの保存
検索条件エリアの「新規」ボタンを押下し、入力欄エリアの名前、住所、TEL、MAILに文字を入力 入力後、「書込」ボタン押下でIndexedDBに保存。 👉 「新規」ボタンを押下後、入力欄が入力可能になります。入力すると「書込」ボタンが押せるようになります。 👉 新規入力を取りやめたい時は、「解除」ボタン押下で、中止可能です。
手順③:ID指定による読込
検索条件エリアのIDに読込したいデータのIDを指定し、「読込」ボタンを押下 👉 入力すると「読込」ボタンが押せるようになります。
手順④:名前による検索
検索条件エリアの名前に検索したいデータの名前を指定し、「検索」ボタンを押下 👉 入力すると「検索」ボタンが押せるようになります。
手順⑤:データの削除
検索条件エリアのIDに削除したいデータのIDを指定し、「削除」ボタンを押下 👉 入力すると「削除」ボタンが押せるようになります。 👉 「ID指定による読込」や「名前による検索」後もIDに値が入るので、削除可能です。
手順⑥:全データの削除
画面最下部の「全削除」ボタンを押下 👉 IndexedDBに保存されている全てのデータが削除されます。
動作確認を実施する
このテストで確認したいこと
このテストでは、次のことを確認していきたいと思います:
- データがブラウザに保存される仕組み
- ページをリロードしてもデータが残ること
- Keyによるデータの読込の動き
- Indexによるデータの読込の動き
- 上書き保存の動き
- 削除・全削除の違い
実際の操作手順(ここが重要)
- 手順:「新規」->「保存」で、データを2件以上、保存する。
👉 IndexedDBのオープン、データ保存、読込を確認します
- indexedDB.open()
- IDBOpenDBRequest.onupgradeneeded
- IDBOpenDBRequest.onsuccess
- IDBDatabase.createObjectStore()
- IDBDatabase.transaction()
- IDBTransaction.objectStore()
- IDBObjectStore.createIndex()
- IDBObjectStore.getAll()
- IDBObjectStore.add()
- 手順:F5や再読み込みを実行した後、テストプログラムを再起動する。
保存したデータが消えずに残っていることを確認します。
- 手順:IDによる読込を行う。 指定されたIDのデータが入力欄に表示されることを確認します。
👉 IDBObjectStore.get()の動作を確認します
- 手順:名前による検索を行う。 指定された名前のデータが入力欄に表示されることを確認します。
👉 IndexedDBのindex動作を確認します
- IDBObjectStore.index()
- IDBIndex.get()
- 手順:入力欄の住所を書き換え、「保存」を押す
書き換えた住所で上書きされることを確認します。 👉 IDBObjectStore.put()の上書き動作を確認します
- 手順:「削除」を押す
対象のデータだけ削除されることを確認します。 👉 IDBObjectStore.delete()の動作を確認します
- 手順:「全削除」を押す
すべてのデータが削除されることを確認します。 👉 IDBObjectStore.clear()の動作を確認します
テスト結果まとめ
- IndexedDBのアクセスは、オブジェクトのイベントドリブンが基本
- ページをリロードしてもデータは残る
- 同じキーは上書きされる
- 削除しない限りデータは残り続ける
- IDBOpenDBRequest.onupgradeneededは初回か、バージョンアップのopen時だけ発生 (IDBOpenDBRequest.onupgradeneeded → IDBOpenDBRequest.onsuccessの順で発生)
- 下記のAPIは、IDBOpenDBRequest.onupgradeneededイベント内でのみ実行可能
- IDBDatabase.createObjectStore()
- IDBDatabase.deleteObjectStore()
- IDBObjectStore.createIndex()
- IDBObjectStore.deleteIndex()
- Index作成で、unique指定した場合、書き込み時にduplicateがあるとエラーになる
- IDBObjectStore.get(),IDBObjectStore.getAll(),IDBIndex.get()は対象データがなくてもエラーにならない
- IDBObjectStore.delete()やIDBObjectStore.clear()は対象データがなくてもエラーにならない
- Edgeでは、ファイル指定(
file:///C:/Users/~/index.html)でも動作 - 開発者ツールのコンソールで保存データが直接確認可能
テストプログラムのコード
下記に、本テストで使用したプログラムのコードを記します。
1. HTML(index.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Session Storage Test</title>
<link rel="stylesheet" href="./style.css">
<script type="text/javascript" src="./script.js"></script>
<script type="text/javascript" src="./table_class.js"></script>
</head>
<body>
<div id="table-area"></div>
<p>
<fieldset>
<legend>[ 検索条件 ]</legend>
<div>
<label for="id">ID:</label>
<input type="text" id="id" disabled>
<button id="new" disabled>新規</button>
<button id="get" disabled>読込</button>
<button id="put" disabled>書込</button>
<button id="del" disabled>削除</button>
<button id="rel" disabled>解除</button>
</div>
<div>
<label for="s-name">名前:</label>
<input type="text" id="s-name">
<button id="sch" disabled>検索</button>
</div>
</fieldset>
</p>
<p>
<div id="input-area">
<div>
<label for="name">名前:</label>
<input type="text" id="name" disabled>
</div>
<div>
<label for="address">住所:</label>
<input type="text" id="address" disabled>
</div>
<div>
<label for="tel">電話:</label>
<input type="text" id="tel" disabled>
</div>
<div>
<label for="email">Mail:</label>
<input type="text" id="email" disabled>
</div>
<div>
</p>
<p>
<div>
<button id="all-del">全削除</button>
</div>
</p>
<p>
<div id="popup-status"></div>
</p>
</body>
</html>
2. CSS(style.css)
body {
padding: 1rem;
background-color: rgb(253, 191, 76);
text-align: center;
overflow: hidden;
}
#table-area {
height: 10rem;
background-color: white;
overflow-y: auto;
}
#input-area {
display: block;
justify-content: left;
align-items: left;
}
table {
border-collapse: separate;
border-spacing: 0;
border: 1px solid;
}
th {
position: sticky;
top: 0;
z-index: 1;
border: 1px solid;
background-color: skyblue;
height: 2rem;
}
td {
border: 1px solid;
background-color: white;
height: 2rem;
}
.id {
width: 20rem;
}
.name {
width: 50rem;
}
.address {
width: 100rem;
}
input {
width: 30rem;
}
#id {
width: 15.5rem;
}
#s-name {
width: 27rem;
}
#popup-status {
font-size: 1.25rem;
font-weight: bold;
color: red;
margin-bottom: 1rem;
}
3. JavaScript(本体:script.js)
//
// IndexedDBの動作確認用JavaScriptコード
//
window.addEventListener("load", function() {
// 表示テーブル作成
const tableObj = new tableClass("table-area", [
{ name: "id", class: "id", content: "ID" },
{ name: "name", class: "name", content: "名前" },
{ name: "address", class: "address", content: "住所" }
]);
// 画面要素取得
const input_id = document.getElementById("id");
const input_s_name = document.getElementById("s-name");
const input_name = document.getElementById("name");
const input_address = document.getElementById("address");
const input_tel = document.getElementById("tel");
const input_email = document.getElementById("email");
const btn_new = document.getElementById("new");
const btn_get = document.getElementById("get");
const btn_put = document.getElementById("put");
const btn_del = document.getElementById("del");
const btn_rel = document.getElementById("rel");
const btn_sch = document.getElementById("sch");
const btn_all_del = document.getElementById("all-del");
const status = document.getElementById("popup-status");
// 固定値定義
const MAX_COUNT = 5;
// 変数定義
let db = null; // IndexedDBのIDBDatabase(DB接続)オブジェクト
let lastId = 0; // 最新のKey値
let totalCount = 0; // 登録件数
// ID入力欄の変更を監視して、入力があれば新規ボタンを有効化
input_id.addEventListener('keyup', function(event) {
if (this.value) {
if (!Number(input_id.value)) {
status.textContent = "IDには、半角数字を入力してください。";
input_id.focus();
return;
}
input_s_name.value = "";
input_name.value = "";
input_address.value = "";
input_tel.value = "";
input_email.value = "";
input_name.disabled = true;
input_address.disabled = true;
input_tel.disabled = true;
input_email.disabled = true;
btn_new.disabled = true;
btn_get.disabled = false;
btn_put.disabled = true;
btn_del.disabled = false;
btn_rel.disabled = true;
btn_sch.disabled = true;
} else {
input_s_name.value = "";
input_name.value = "";
input_address.value = "";
input_tel.value = "";
input_email.value = "";
btn_new.disabled = false;
btn_get.disabled = true;
btn_put.disabled = true;
btn_del.disabled = true;
btn_rel.disabled = true;
btn_sch.disabled = true;
}
status.textContent = "";
});
// 検索用MAIL入力欄の変更を監視して、入力があれば検索ボタンを有効化
input_s_name.addEventListener('keyup', function(event) {
if (this.value) {
input_id.value = "";
input_name.value = "";
input_address.value = "";
input_tel.value = "";
input_email.value = "";
input_name.disabled = true;
input_address.disabled = true;
input_tel.disabled = true;
input_email.disabled = true;
btn_new.disabled = true;
btn_get.disabled = true;
btn_put.disabled = true;
btn_del.disabled = true;
btn_rel.disabled = true;
btn_sch.disabled = false;
} else {
input_id.value = "";
input_name.value = "";
input_address.value = "";
input_tel.value = "";
input_email.value = "";
btn_new.disabled = false;
btn_get.disabled = true;
btn_put.disabled = true;
btn_del.disabled = true;
btn_rel.disabled = true;
btn_sch.disabled = true;
}
status.textContent = "";
});
// ID以外の入力欄の変更を監視して、変更があれば、書込ボタンを有効化
[input_name, input_address, input_tel, input_email].forEach(function (input) {
input.addEventListener('change', function(event) {
btn_put.disabled = false;
status.textContent = "";
});
});
// 新規ボタン押下時に、ID以外の入力欄を有効化する
btn_new.addEventListener('click', function(event) {
input_id.value = "";
input_s_name.value = "";
input_name.value = "";
input_address.value = "";
input_tel.value = "";
input_email.value = "";
input_id.disabled = true;
input_s_name.disabled = true;
input_name.disabled = false;
input_address.disabled = false;
input_tel.disabled = false;
input_email.disabled = false;
btn_new.disabled = true;
btn_get.disabled = true;
btn_put.disabled = true;
btn_del.disabled = true;
btn_rel.disabled = false;
btn_sch.disabled = true;
status.textContent = "";
});
// 取得ボタン押下時に、IndexedDBから値を取得し、入力欄を有効化
btn_get.addEventListener('click', function(event) {
const tx = db.transaction('friends', 'readonly');
const store = tx.objectStore('friends');
//const request = store.getAll(IDBKeyRange.only(input_id.value), 1);
const request = store.get(input_id.value);
request.onsuccess = () => {
if (!request.result) {
status.textContent = "指定されたデータがありません。";
return;
}
input_name.value = request.result.name;
input_address.value = request.result.address;
input_tel.value = request.result.tel;
input_email.value = request.result.email;
input_name.disabled = false;
input_address.disabled = false;
input_tel.disabled = false;
input_email.disabled = false;
btn_put.disabled = true;
status.textContent = "読込完了"
}
request.onerror = () => {
status.textContent = "エラー発生"
}
});
// 書込ボタン押下時に、IndexedDBに値を保存する
btn_put.addEventListener('click', function(event) {
if (totalCount >= MAX_COUNT) {
status.textContent = `登録件数が最大値(${MAX_COUNT}件)を超えているので、登録できません。`;
return;
}
if (!input_id.value && !input_name.value && !input_address.value && !input_tel.value && !input_email.value) {
input_id.focus();
status.textContent = "値が入力されていません。";
return;
}
const phoneRegex = /^0\d{1,4}-?\d{1,4}-?\d{3,4}$/;
if (!phoneRegex.test(input_tel.value)) {
status.textContent = "電話番号の形式が正しくありません";
input_tel.focus();
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(input_email.value)) {
status.textContent = "MAILの形式が正しくありません";
input_email.focus();
return;
}
const tx = db.transaction('friends', 'readwrite');
const store = tx.objectStore('friends');
const data = {
id: input_id.value,
name: input_name.value,
address: input_address.value,
tel: input_tel.value,
email: input_email.value
};
// IDが存在すれば、上書き、なければ、新たにIDを付与し追加
if (input_id.value) {
store.put(data);
} else {
lastId++;
data.id = lastId.toString();
store.add(data);
}
tx.oncomplete = () => {
status.textContent = "保存完了";
document.dispatchEvent(new CustomEvent('reloadTable', {
detail: { storename: 'friends' }
}));
};
tx.onerror = () => {
status.textContent = "エラー発生";
lastId--;
};
input_id.value = "";
input_name.value = "";
input_address.value = "";
input_tel.value = "";
input_email.value = "";
input_id.disabled = false;
input_s_name.disabled = false;
input_name.disabled = true;
input_address.disabled = true;
input_tel.disabled = true;
input_email.disabled = true;
btn_new.disabled = false;
btn_get.disabled = true;
btn_put.disabled = true;
btn_del.disabled = true;
btn_rel.disabled = true;
});
// 削除ボタン押下時に、IndexedDBから値を削除
document.getElementById("del").addEventListener('click', function(event) {
const tx = db.transaction('friends', 'readwrite');
const store = tx.objectStore('friends');
store.delete(input_id.value);
tx.oncomplete = () => {
status.textContent = "削除完了";
document.dispatchEvent(new CustomEvent('reloadTable', {
detail: { storename: 'friends' }
}));
};
tx.onerror = () => {
status.textContent = "エラー発生";
};
input_id.value = "";
input_name.value = "";
input_address.value = "";
input_tel.value = "";
input_email.value = "";
input_id.disabled = false;
input_name.disabled = true;
input_address.disabled = true;
input_tel.disabled = true;
input_email.disabled = true;
btn_new.disabled = false;
btn_get.disabled = true;
btn_put.disabled = true;
btn_del.disabled = true;
btn_rel.disabled = true;
});
// 解除ボタン押下時に、入力欄をクリアし、新規ボタン押下前に戻す。
document.getElementById("rel").addEventListener('click', function(event) {
input_id.value = "";
input_name.value = "";
input_address.value = "";
input_tel.value = "";
input_email.value = "";
input_id.disabled = false;
input_s_name.disabled = false;
input_name.disabled = true;
input_address.disabled = true;
input_tel.disabled = true;
input_email.disabled = true;
btn_new.disabled = false;
btn_get.disabled = true;
btn_put.disabled = true;
btn_del.disabled = true;
btn_rel.disabled = true;
});
// 検索ボタン押下時に、入力欄をクリアし、新規ボタン押下前に戻す。
document.getElementById("sch").addEventListener('click', function(event) {
const tx = db.transaction('friends', 'readonly');
const store = tx.objectStore('friends');
const index = store.index("nameIndex");
const request = index.get(input_s_name.value);
request.onsuccess = () => {
if (!request.result) {
status.textContent = "指定されたデータがありません。";
return;
}
input_id.value = request.result.id;
input_name.value = request.result.name;
input_address.value = request.result.address;
input_tel.value = request.result.tel;
input_email.value = request.result.email;
input_name.disabled = false;
input_address.disabled = false;
input_tel.disabled = false;
input_email.disabled = false;
btn_put.disabled = true;
btn_del.disabled = false;
status.textContent = "読込完了"
}
request.onerror = () => {
status.textContent = "エラー発生"
}
});
// 全削除ボタン押下時に、IndexedDBから全値を削除
document.getElementById("all-del").addEventListener('click', function(event) {
const tx = db.transaction('friends', 'readwrite');
const store = tx.objectStore('friends');
store.clear();
tx.oncomplete = () => {
status.textContent = "全削除完了";
document.dispatchEvent(new CustomEvent('reloadTable', {
detail: { storename: 'friends' }
}));
};
tx.onerror = () => {
status.textContent = "エラー発生";
};
input_id.value = "";
input_s_name.value = "";
input_name.value = "";
input_address.value = "";
input_tel.value = "";
input_email.value = "";
input_id.disabled = false;
input_name.disabled = true;
input_address.disabled = true;
input_tel.disabled = true;
input_email.disabled = true;
btn_new.disabled = false;
btn_get.disabled = true;
btn_put.disabled = true;
btn_del.disabled = true;
btn_rel.disabled = true;
btn_sch.disabled = true;
});
// カスタムイベント発生時、IndexedDBから全データ取得し、テーブルを再表示
document.addEventListener('reloadTable', (e) => {
const tx = db.transaction(e.detail.storename, 'readonly');
const store = tx.objectStore(e.detail.storename);
const request = store.getAll(null, MAX_COUNT);
request.onsuccess = () => {
tableObj.resetDatas(request.result);
if (request.result.length) {
lastId = request.result[request.result.length - 1].id;
totalCount = request.result.length;
} else {
lastId = 0;
totalCount = 0;
}
}
});
// 初期処理
status.textContent = "データベースオープン中";
const request = indexedDB.open('MyDatabase', 1);
request.onupgradeneeded = (event) => {
db = event.target.result;
const store = db.createObjectStore('friends', { keyPath: 'id' });
store.createIndex("nameIndex", "name", { unique: true });
};
request.onsuccess = (event) => {
db = event.target.result;
document.dispatchEvent(new CustomEvent('reloadTable', {
detail: { storename: 'friends' }
}));
input_id.disabled=false;
btn_new.disabled=false;
status.textContent = "";
};
});
4. JavaScript(テーブルクラス:table_class.js)
//
// テーブル拡張クラス用JavaScriptコード
//
class tableClass {
// コンストラクター(テーブルを生成するdivタグとテーブルの定義をうけとる)
constructor(target, fields) {
this.fields = fields;
this.table = document.createElement("table");
document.getElementById(target).append(this.table);
}
// テーブル定義に従い、テーブルのヘッダー部を作成
setHeader() {
this.table.innerHTML = "";
const trec = document.createElement('tr');
this.fields.forEach((field) => {
const theader = document.createElement('th');
theader.classList.add(field.class);
theader.textContent = field.content;
trec.append(theader);
});
this.table.appendChild(trec);
}
// 1行分のデータを受け取り、テーブル定義に従ってテーブルのデータ部を作成
setData(data) {
const trec = document.createElement('tr');
this.fields.forEach((field) => {
const tdata = document.createElement('td');
tdata.textContent = data[field.name];
trec.append(tdata);
});
this.table.appendChild(trec);
}
// データを配列で受け取り、テーブル定義に従ってテーブルを作成
resetDatas(datas) {
this.setHeader();
datas.forEach((data) => {
const trec = document.createElement('tr');
this.fields.forEach((field) => {
const tdata = document.createElement('td');
tdata.textContent = data[field.name];
trec.append(tdata);
});
this.table.appendChild(trec);
});
}
}
(function (root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
// Node.js環境
module.exports = factory();
} else {
// ブラウザ環境
root.tableClass = factory();
}
}(this, function () {
return tableClass;
}));
(つづく)