前回からのつづき
前回、IndexedDBの基本動作の確認について説明を行ってきました。今回は、その続きとなります。
今回は、データ検索テストプログラムを中心に説明したいと思います。
データ検索テストプログラムによる検索動作の確認
下記のボタンを押下することで、テストプログラムの起動が可能です。
👉 本記事のテストプログラムを動かすことで、「IndexedDBの動き」が体感できます。
テストプログラムの概要(かんたん説明)
このプログラムが保存するデータは、売り上げのオブジェクトです。
例)
{
id: 1,
hizuke: 2026/05/01,
hinmoku: "えんぴつ",
kingaku: 1000
}
表示画面には、大きく分けて3つのエリアが存在します。
- テーブルエリア
- 検索項目エリア
- 検索条件エリア
テーブルエリア
IndexedDBに格納されているデータが一覧で表示されます。(最大10件)
検索項目エリア
検索対象とする項目を選択するものです。
検索条件エリア
値を入力することにより、一致検索、前方一致検索、下限検索、上限検索、範囲検索が可能です。検索されたデータは、テーブルエリアに反映されます。
全検索ボタン押下により、全件表示に戻ります。
構造はいたってシンプルです。
操作手順
手順①:本頁の「テストプログラム起動」を押す
テストプログラム起動 👉 起動時に、現在登録されているデータがテーブルエリアに表示されます。
手順②:一致検索
検索項目エリアにて、検索対象の項目を選択 検索条件エリアの1つ目の入力欄に値を入力し、「一致検索」ボタン押下 👉 入力欄への入力後、「一致検索」ボタンが押せるようになります。
手順③:前方一致検索
検索項目エリアにて、検索対象の項目を選択 検索条件エリアの1つ目の入力欄に値を入力し、「前方一致検索」ボタン押下 👉 入力欄への入力後、「前方一致検索」ボタンが押せるようになります。ただし、検索対象項目が文字列の場合のみ使用可能となります。
手順④:下限検索
検索項目エリアにて、検索対象の項目を選択 検索条件エリアの2つ目の入力欄に値を入力し、「下限検索」ボタン押下 👉 入力欄への入力後、「下限検索」ボタンが押せるようになります。 👉 「閾値含む」のチェックを外すことで、入力した値を含まない検索が可能です。
手順⑤:上限検索
検索項目エリアにて、検索対象の項目を選択 検索条件エリアの3つ目の入力欄に値を入力し、「上限検索」ボタン押下 👉 入力欄への入力後、「上限検索」ボタンが押せるようになります。 👉 「閾値含む」のチェックを外すことで、入力した値を含まない検索が可能です。
手順⑥:範囲検索
検索項目エリアにて、検索対象の項目を選択 検索条件エリアの2つ目と3つ目の入力欄の両方に値を入力し、「範囲検索」ボタン押下 👉 両方の入力欄への入力後、「範囲検索」ボタンが押せるようになります。 👉 「閾値含む」のチェックを外すことで、入力した値を含まない検索が可能です。
手順⑦:全検索
「全検索」ボタン押下 👉 登録されている全てのデータが読み込まれます。
動作確認を実施する
このテストで確認したいこと
このテストでは、次のことを確認していきたいと思います:
- IndexedDBのデータストアへの初期データ登録の動き
- Key値による一致検索の動き
- Key値による下限検索の動き
- Key値による上限検索の動き
- Key値による範囲検索の動き
- Indexによる一致検索の動き
- Indexによる前方一致検索の動き
- Indexによる下限検索の動き
- Indexによる上限検索の動き
- Indexによる範囲検索の動き
実際の操作手順(ここが重要)
- 手順:テストプログラムの起動。 初期データ10件が登録され、テーブルエリアに表示されることを確認します。
👉 IndexedDBのオープン、データ保存、読込を確認します。
- indexedDB.open()
- IDBOpenDBRequest.onupgradeneeded
- IDBOpenDBRequest.onsuccess
- IDBDatabase.createObjectStore()
- IDBDatabase.transaction()
- IDBTransaction.objectStore()
- IDBObjectStore.createIndex()
- IDBObjectStore.openCursor()
- IDBObjectStore.add()
- IDBCursor.continue()
- 手順:IDによる一致検索を行う。 指定されたIDのデータのみが読み込まれ、テーブルエリアに表示されることを確認します。
👉 IDBKeyRange.only()による一致検索の動きを確認します
- 手順:IDによる下限検索を行う。 指定されたID以降のデータが読み込まれ、テーブルエリアに表示されることを確認します。
👉 IDBKeyRange.lowerBound()による下限検索の動きを確認します
- 手順:IDによる上限検索を行う。 指定されたID以前のデータが読み込まれ、テーブルエリアに表示されることを確認します。
👉 IDBKeyRange.upperBound()による上限検索の動きを確認します
- 手順:IDによる範囲検索を行う。 指定されたID範囲のデータが読み込まれ、テーブルエリアに表示されることを確認します。
👉 IDBKeyRange.bound()による範囲検索の動きを確認します
- 手順:ID以外の項目による一致検索を行う。 指定された項目のデータのみが読み込まれ、テーブルエリアに表示されることを確認します。
👉 IDBKeyRange.only()による一致検索の動きを確認します
- 手順:ID以外の項目による前方一致検索を行う。 指定された項目のデータと前方一致するデータのみが読み込まれ、テーブルエリアに表示されることを確認します。
👉 IDBKeyRange.bound()による前方一致検索の動きを確認します
- 手順:ID以外の項目による下限検索を行う。 指定された項目以降のデータが読み込まれ、テーブルエリアに表示されることを確認します。
👉 IDBKeyRange.lowerBound()による下限検索の動きを確認します
- 手順:ID以外の項目による上限検索を行う。 指定された項目以前のデータが読み込まれ、テーブルエリアに表示されることを確認します。
👉 IDBKeyRange.upperBound()による上限検索の動きを確認します
- 手順:ID以外の項目による範囲検索を行う。 指定された項目範囲のデータが読み込まれ、テーブルエリアに表示されることを確認します。
👉 IDBKeyRange.bound()による範囲検索の動きを確認します
テスト結果まとめ
- IndexedDBの初期データ登録は、IDBOpenDBRequest.onupgradeneeded内で、IDBObjectStore.add()やIDBObjectStore.put()が使用可能
- IDBOpenDBRequest.onupgradeneededの中では、暗黙的に「versionchange トランザクション」が開いているため、新たなトランザクションの開始は不要
- 初期データの件数が多い場合(1000 件以上)は、 データ量が多すぎてトランザクションが閉じることを防ぐ為、IDBOpenDBRequest.onupgradeneededの中ではなく、IDBOpenDBRequest.onsuccess内で、分割登録の検討が必要
- IndexedDBの検索は、指定項目の「一致検索」「前方一致検索」「下限検索」「上限検索」「範囲検索」が基本
- Key以外の項目による検索は、IDBObjectStore.createIndex()で、事前にインデックスの作成が必須
- 「複合条件検索(ORの使用など)」は基本的にできない
- 「前方一致検索」は、範囲検索の応用で文字列の場合のみ実現可能
テストプログラムのコード
下記に、本テストで使用したプログラムのコードを記します。
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>
<fieldset class="field">
<legend>[ 検索項目 ]</legend>
<div>
<label>
<input type="radio" id="fid" name="field" value="id" checked>ID
</label>
<label>
<input type="radio" id="fhizuke" name="field" value="hizuke">日付
</label>
<label>
<input type="radio" id="fhinmoku" name="field" value="hinmoku">品目
</label>
<label>
<input type="radio" id="fkingaku" name="field" value="kingaku">金額
</label>
</div>
</fieldset>
<fieldset class="condition">
<legend>[ 検索条件 ]</legend>
<div>
<input type="number" class="value" id="input-value" disabled><br>
<input type="number" class="bound" id="input-lower-value" disabled> ~ <input type="number" class="bound" id="input-upper-value" disabled><br>
<label><input type="checkbox" id="input-lower-threshold" checked disabled> 閾値含む </label>
<label><input type="checkbox" id="input-upper-threshold" checked disabled> 閾値含む</label>
</div>
<div class="search-button">
<button id="btn-only" disabled>一致検索</button>
<button id="btn-prefix-match" disabled>前方一致検索</button><br>
<button id="btn-lower" disabled>下限検索</button>
<button id="btn-upper" disabled>上限検索</button>
<button id="btn-bound" disabled>範囲検索</button>
</div>
</fieldset>
<p>
<button id="btn-all" disabled>全検索</button>
</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: 12rem;
background-color: white;
overflow-y: auto;
}
#table-area table {
border-collapse: separate;
border-spacing: 0;
border: 1px solid;
}
#table-area thead th {
position: sticky;
top: 0;
z-index: 1;
border: 1px solid;
background-color: skyblue;
font-weight: bold;
height: 2rem;
}
#table-area tbody td {
border: 1px solid;
background-color: white;
height: 2rem;
}
#table-area tfoot td {
position: sticky;
bottom: 0;
z-index: 1;
border: 1px solid;
background-color: lightgray;
font-weight: bold;
height: 2rem;
}
#table-area .id {
width: 20rem;
}
#table-area .hizuke {
width: 30rem;
}
#table-area .hinmoku {
width: 50rem;
}
#table-area .kingaku {
width: 30rem;
}
.condition {
display: flex;
justify-content: center;
gap: 1rem;
}
.search-button {
text-align: left;
}
.value {
width: 19.5rem;
}
.bound {
width: 8rem;
}
#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", type: "number", calc: "合計", content: "ID" },
{ name: "hizuke", class: "hizuke", type: "date", calc: "", content: "日付" },
{ name: "hinmoku", class: "hinmoku", type: "string", calc: "", content: "品目" },
{ name: "kingaku", class: "kingaku", type: "number", calc: "sum", content: "金額" }
]);
// 画面要素取得
const input_fields = document.getElementsByName("field");
const input_value = document.getElementById("input-value");
const input_lower_value = document.getElementById("input-lower-value");
const input_upper_value = document.getElementById("input-upper-value");
const input_lower_threshold = document.getElementById("input-lower-threshold");
const input_upper_threshold = document.getElementById("input-upper-threshold");
const btn_only = document.getElementById("btn-only");
const btn_prefix_match = document.getElementById("btn-prefix-match");
const btn_lower = document.getElementById("btn-lower");
const btn_upper = document.getElementById("btn-upper");
const btn_bound = document.getElementById("btn-bound");
const btn_all = document.getElementById("btn-all");
const status = document.getElementById("popup-status");
// 固定値定義
const MAX_COUNT = 10;
const datas = [
{ id: 1, hizuke: new Date("2026-05-01"), hinmoku: "ノート", kingaku: 2000 },
{ id: 2, hizuke: new Date("2026-05-02"), hinmoku: "えんぴつ", kingaku: 1200 },
{ id: 3, hizuke: new Date("2026-05-02"), hinmoku: "消しゴム", kingaku: 500 },
{ id: 4, hizuke: new Date("2026-05-03"), hinmoku: "ボールペン", kingaku: 1500 },
{ id: 5, hizuke: new Date("2026-05-03"), hinmoku: "シャーペン", kingaku: 100 },
{ id: 6, hizuke: new Date("2026-05-04"), hinmoku: "定規", kingaku: 900 },
{ id: 7, hizuke: new Date("2026-05-04"), hinmoku: "コンパス", kingaku: 2500 },
{ id: 8, hizuke: new Date("2026-05-05"), hinmoku: "ノートA4", kingaku: 1000 },
{ id: 9, hizuke: new Date("2026-05-06"), hinmoku: "ノートB5横", kingaku: 6000 },
{ id: 10, hizuke: new Date("2026-05-07"), hinmoku: "ノートB5縦", kingaku: 2000 }
];
// 変数定義
let db = null; // IndexedDBのIDBDatabase(DB接続)オブジェクト
let threshold_open = false; // 閾値フラグ false:含まない/true:含む
// 検索項目に従い、入力欄の種別変更
input_fields.forEach(function (field) {
field.addEventListener('click', function(event) {
input_value.value = "";
input_lower_value.value = "";
input_upper_value.value = "";
btn_only.disabled = true;
btn_prefix_match.disabled = true;
btn_lower.disabled = true;
btn_upper.disabled = true;
btn_bound.disabled = true;
switch (field.value) {
case "id" :
input_value.type = "number";
input_lower_value.type = "number";
input_upper_value.type = "number";
break;
case "hizuke" :
input_value.type = "date";
input_lower_value.type = "date";
input_upper_value.type = "date";
break;
case "hinmoku" :
input_value.type = "text";
input_lower_value.type = "text";
input_upper_value.type = "text";
break;
case "kingaku" :
input_value.type = "number";
input_lower_value.type = "number";
input_upper_value.type = "number";
break;
default :
input_value.type = "text";
input_lower_value.type = "text";
input_upper_value.type = "text";
break;
}
});
});
// 一致検索、前方一致検索の入力欄の変更を監視して、入力があれば検索ボタンを有効化
['keyup', 'change'].forEach(function (eventType) {
input_value.addEventListener(eventType, function(event) {
input_lower_value.value = "";
input_upper_value.value = "";
input_lower_threshold.disabled =true;
input_upper_threshold.disabled =true;
btn_lower.disabled = true;
btn_upper.disabled = true;
btn_bound.disabled = true;
if (this.value) {
btn_only.disabled = false;
if (input_value.type == "text") btn_prefix_match.disabled = false;
} else {
btn_only.disabled = true;
btn_prefix_match.disabled = true;
}
status.textContent = "";
});
});
// 下限検索の入力欄の変更を監視して、入力があれば検索ボタンを有効化
['keyup', 'change'].forEach(function (eventType) {
input_lower_value.addEventListener(eventType, function(event) {
input_value.value = "";
btn_only.disabled = true;
btn_prefix_match.disabled = true;
if (this.value) {
input_lower_threshold.disabled =false;
btn_lower.disabled = false;
if (input_upper_value.value) btn_bound.disabled = false;
} else {
input_lower_threshold.disabled =true;
btn_lower.disabled = true;
btn_bound.disabled = true;
}
status.textContent = "";
});
});
// 下限検索、上限検索、範囲検索の入力欄の変更を監視して、入力があれば検索ボタンを有効化
['keyup', 'change'].forEach(function (eventType) {
input_upper_value.addEventListener(eventType, function(event) {
input_value.value = "";
btn_only.disabled = true;
btn_prefix_match.disabled = true;
if (this.value) {
input_upper_threshold.disabled =false;
btn_upper.disabled = false;
if (input_lower_value.value) btn_bound.disabled = false;
} else {
input_upper_threshold.disabled =true;
btn_upper.disabled = true;
btn_bound.disabled = true;
}
status.textContent = "";
});
});
// 一致検索、前方一致検索ボタン押下時に、IndexedDBから値を取得し、テーブルに表示
[btn_only, btn_prefix_match].forEach(function (button) {
button.addEventListener('click', function(event) {
const tx = db.transaction('sales', 'readonly');
const store = tx.objectStore('sales');
tableObj.setHeader();
const results = []
function query(value) {
switch (button.id) {
case "btn-only" :
return IDBKeyRange.only(value);
case "btn-prefix-match" :
return IDBKeyRange.bound(value, value+"\uffff");
default :
return IDBKeyRange.only(value);
}
}
let request;
input_fields.forEach((input_field) => {
if (input_field.checked) {
switch (input_field.value) {
case "id" :
request = store.openCursor(query(Number(input_value.value)));
break;
case "hizuke" :
request = store.index(input_field.value + "Index").openCursor(query(new Date(input_value.value)));
break;
case "hinmoku" :
request = store.index(input_field.value + "Index").openCursor(query(input_value.value));
break;
case "kingaku" :
request = store.index(input_field.value + "Index").openCursor(query(Number(input_value.value)));
break;
default :
request = store.openCursor(query(0));
break;
}
}
});
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
tableObj.setFooter();
if (!results.length) status.textContent = "指定されたデータがありません。";
return;
}
results.push(cursor.value)
tableObj.setData(cursor.value);
status.textContent = "完了"
if (results.length < MAX_COUNT) {
cursor.continue();
return;
}
tableObj.setFooter();
}
request.onerror = () => {
status.textContent = "エラー発生"
}
});
});
// 下限検索、上限検索、範囲検索ボタン押下時に、IndexedDBから値を取得し、テーブルに表示
[btn_lower, btn_upper, btn_bound].forEach(function (button) {
button.addEventListener('click', function(event) {
const tx = db.transaction('sales', 'readonly');
const store = tx.objectStore('sales');
tableObj.setHeader();
const results = []
function query(lowerValue, upperValue, lowerOpen, upperOpen) {
switch (button.id) {
case "btn-lower" :
return IDBKeyRange.lowerBound(lowerValue, lowerOpen);
case "btn-upper" :
return IDBKeyRange.upperBound(upperValue, upperOpen);
case "btn-bound" :
if (lowerValue == upperValue && lowerOpen != upperOpen) {
return IDBKeyRange.only(0);
} else if (lowerValue > upperValue) {
return IDBKeyRange.only(0);
} else {
return IDBKeyRange.bound(lowerValue, upperValue, lowerOpen, upperOpen);
}
default :
return IDBKeyRange.only(0);
}
}
let request;
input_fields.forEach((input_field) => {
if (input_field.checked) {
const lower_open = (input_lower_threshold.checked ? false : true);
const upper_open = (input_upper_threshold.checked ? false : true);
switch (input_field.value) {
case "id" :
request = store.openCursor(query(Number(input_lower_value.value), Number(input_upper_value.value), lower_open, upper_open));
break;
case "hizuke" :
request = store.index(input_field.value + "Index").openCursor(query(new Date(input_lower_value.value), new Date(input_upper_value.value), lower_open, upper_open));
break;
case "hinmoku" :
request = store.index(input_field.value + "Index").openCursor(query(input_lower_value.value, input_upper_value.value, lower_open, upper_open));
break;
case "kingaku" :
request = store.index(input_field.value + "Index").openCursor(query(Number(input_lower_value.value), Number(input_upper_value.value), lower_open, upper_open));
break;
default :
request = store.openCursor(query(0, 0, 0, 0));
break;
}
}
});
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
tableObj.setFooter();
if (!results.length) status.textContent = "指定されたデータがありません。";
return;
}
results.push(cursor.value)
tableObj.setData(cursor.value);
status.textContent = "完了"
if (results.length < MAX_COUNT) {
cursor.continue();
return;
}
tableObj.setFooter();
}
request.onerror = () => {
status.textContent = "エラー発生"
}
});
});
// 全検索ボタン押下時に、カスタムイベント発生
btn_all.addEventListener('click', function(event) {
document.dispatchEvent(new CustomEvent('reloadTable', {
detail: { storename: 'sales' }
}));
});
// カスタムイベント発生時、IndexedDBから全データ取得し、テーブルを再表示
document.addEventListener('reloadTable', (e) => {
const tx = db.transaction(e.detail.storename, 'readonly');
const store = tx.objectStore(e.detail.storename);
tableObj.setHeader();
const results = []
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
tableObj.setFooter();
return;
}
results.push(cursor.value)
tableObj.setData(cursor.value);
if (results.length < MAX_COUNT) {
cursor.continue();
return;
}
tableObj.setFooter();
}
});
// 初期処理
status.textContent = "データベースオープン中";
const request = indexedDB.open('SalesDatabase', 1);
request.onupgradeneeded = (event) => {
db = event.target.result;
if (!db.objectStoreNames.contains("sales")) {
const store = db.createObjectStore("sales", { keyPath: "id" });
store.createIndex("hizukeIndex", "hizuke");
store.createIndex("hinmokuIndex", "hinmoku");
store.createIndex("kingakuIndex", "kingaku");
datas.forEach(data => {
store.add(data);
});
}
};
request.onsuccess = (event) => {
db = event.target.result;
document.dispatchEvent(new CustomEvent('reloadTable', {
detail: { storename: 'sales' }
}));
input_value.disabled=false;
input_lower_value.disabled=false;
input_upper_value.disabled=false;
btn_all.disabled=false;
status.textContent = "";
};
});
4. JavaScript(テーブルクラス:table_class.js)
注)前回までのものとは、別物です。
//
// テーブル拡張クラス用JavaScriptコード
//
class tableClass {
// コンストラクター(テーブルを生成するdivタグとテーブルの定義をうけとる)
constructor(target, fields) {
this.target = target;
this.fields = fields;
this.table = document.createElement("table");
document.getElementById(target).append(this.table);
}
// テーブル定義に従い、テーブルのヘッダー部を作成
setHeader() {
this.table.innerHTML = "";
this.thead = document.createElement("thead");
this.tbody = document.createElement("tbody");
this.tfoot = document.createElement("tfoot");
this.table.appendChild(this.thead);
this.table.appendChild(this.tbody);
this.table.appendChild(this.tfoot);
const trec = document.createElement('tr');
this.fields.forEach((field) => {
const theader = document.createElement('th');
theader.classList.add(field.class);
theader.textContent = field.content;
trec.appendChild(theader);
});
this.thead.appendChild(trec);
}
// 1行分のデータを受け取り、テーブル定義に従ってテーブルのデータ部を作成
setData(data) {
const trec = document.createElement('tr');
this.fields.forEach((field) => {
const tdata = document.createElement('td');
tdata.textContent = (field.type == "date" ? data[field.name].toLocaleDateString() : data[field.name]);
trec.appendChild(tdata);
});
this.tbody.appendChild(trec);
}
// テーブル定義に従い、テーブルのフッター部を作成
setFooter() {
const trec = document.createElement('tr');
this.fields.forEach((field, columnIndex) => {
const tdata = document.createElement('td');
if (field.type == "number") {
switch (field.calc) {
case "sum" :
const tds = document.querySelectorAll(`#${this.target} table tbody tr td:nth-child(${columnIndex + 1})`);
let sum = 0;
tds.forEach(td => {
const value = Number(td.textContent.trim());
if (!isNaN(value)) sum += value;
});
tdata.textContent = sum;
break;
default :
tdata.textContent = field.calc;
break;
}
} else {
tdata.textContent = field.calc;
}
trec.appendChild(tdata);
});
this.tfoot.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 = (field.type == "date" ? data[field.name].toLocaleDateString() : data[field.name]);
trec.append(tdata);
});
this.table.appendChild(trec);
});
this.setFooter();
}
}
(function (root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
// Node.js環境
module.exports = factory();
} else {
// ブラウザ環境
root.tableClass = factory();
}
}(this, function () {
return tableClass;
}));