JavaScriptとIndexedDB

JavaScriptとIndexedDB

この記事ではJavaScriptとIndexedDBについて説明します。

JavaScriptとIndexedDBについて、ステップバイステップで解説し、理解を深めるために、各ステップには実用的なサンプルコードも含めています。

YouTube Video

javascript-indexed-db.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args
119                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
120                    .join(" ");
121                output.appendChild(message);
122            };
123
124            // Override console.error
125            const originalError = console.error;
126            console.error = function (...args) {
127                originalError.apply(console, args);
128                const message = document.createElement('div');
129                message.textContent = args
130                    .map(arg => (typeof arg === "object" && arg !== null ? JSON.stringify(arg) : String(arg)))
131                    .join(" ");
132                message.style.color = 'red'; // Color error messages red
133                output.appendChild(message);
134            };
135        })();
136
137        document.getElementById('executeBtn').addEventListener('click', () => {
138            // Prevent multiple loads
139            if (document.getElementById('externalScript')) return;
140
141            const script = document.createElement('script');
142            script.src = 'javascript-indexed-db.js';
143            script.id = 'externalScript';
144            //script.onload = () => console.log('javascript-indexed-db.js loaded and executed.');
145            //script.onerror = () => console.log('Failed to load javascript-indexed-db.js.');
146            document.body.appendChild(script);
147        });
148    </script>
149</body>
150</html>

JavaScriptとIndexedDB

IndexedDBは、ブラウザに組み込まれている非同期のキーバリューストア型データベースです。リレーショナルデータベースのような機能を持ち、クライアントサイドで大量の構造化データを保存・検索できます。特に、オフライン対応アプリケーションやPWA(Progressive Web App)で重宝されます。

IndexedDBの特徴

  • 非同期・イベント駆動で動作します。
  • オブジェクトストアにJavaScriptオブジェクトを保存できます。
  • クエリやインデックスによる検索が可能です。
  • 数百MB以上と保存容量が大きく、CookieやlocalStorageよりも遥かに多くのデータを保持できます。

データベースを開く・作成する

IndexedDBを使うには、まずデータベースを開きます。存在しない場合は自動的に作成されます。

1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
  • open メソッドは、非同期でデータベースを開き、以下の3つのイベントを発生させます。

onsuccess

1// Fired when database is successfully opened
2request.onsuccess = (event) => {
3    const db = event.target.result; // Database instance
4    console.log('Database opened successfully:', db.name);
5};
  • onsuccessイベントは、データベースのオープンに成功したときに発生します。以降の操作はこのタイミングで取得できる request.result を使って行います。

onerror

1// Fired when database fails to open
2request.onerror = (event) => {
3    console.error('Failed to open database:', event.target.error);
4};
  • onerrorイベントは、データベースのオープンに失敗したときに発生します。エラーログの出力やエラーハンドリングをここで行います。

onupgradeneeded

 1// Fired when database is newly created or upgraded
 2request.onupgradeneeded = (event) => {
 3    const db = event.target.result;
 4    console.log('Database upgrade needed (or newly created):', db.name);
 5
 6    // Example: Create an object store (like a table) if it doesn’t exist
 7    if (!db.objectStoreNames.contains('users')) {
 8        db.createObjectStore('users', { keyPath: 'id' });
 9        console.log('Object store "users" created');
10    }
11};
  • onupgradeneededは、データベースが新規に作成されたとき、または指定したバージョンが現在のバージョンより新しい場合に発生します。テーブル(オブジェクトストア)の作成やスキーマの定義はこのタイミングで行います。

オブジェクトストアの作成

まず、onupgradeneeded内で、テーブルに相当する「オブジェクトストア」を作成します。

1request.onupgradeneeded = function (event) {
2    const db = event.target.result;
3    console.log("onupgradeneeded triggered. Database version:", db.version);
4    if (!db.objectStoreNames.contains('users')) {
5        console.log("Creating object store: users");
6        const store = db.createObjectStore('users', { keyPath: 'id' });
7        store.createIndex('name', 'name', { unique: false });
8    }
9};

ここでは以下の設定を行っています。

  • createObjectStore createObjectStore は、データベース内に新しいオブジェクトストアを作成するメソッドです。オブジェクトストアにはレコードを保存でき、keyPath やオプションを指定することでデータの管理方法を定義できます。この処理は onupgradeneeded イベント内で行う必要があります。

  • keyPath: 'id' keyPath には、各レコードを一意に識別する主キーとして id プロパティを指定します。これにより、データを追加・検索・更新する際に自動的に id が使われます。

  • createIndex createIndexメソッドで、検索用にnameプロパティをもとにしたインデックスを作成します。unique: falseとしているため、同じnameを持つ複数のレコードが存在することを許容します。

データベースへの接続成功時の処理

onsuccessには、データベースへの接続が正常に完了した際に実行される処理を設定します。この処理内で取得したデータベースインスタンスを使い、データの読み書きを行う準備が整います。

1request.onsuccess = function (event) {
2    const db = event.target.result;
3    console.log('Database opened successfully');
4    // Use db for reading and writing in subsequent operations
5};
  • この処理は、IndexedDBのデータベース接続が正常に完了したときに実行されます。

  • event.target.result には、開かれたデータベースインスタンス(IDBDatabase オブジェクト)が格納されており、この db を通じてトランザクションの開始やオブジェクトストアへのアクセスを行います。

  • データの追加、取得、更新、削除など、実際の読み書き操作は、db オブジェクトを使って行います。

この時点でデータベースの準備が整ったため、安全にトランザクションを開始できます。

データを追加する

IndexedDBに新しいデータを追加する方法は次のようになります。

 1function addUser(db, user) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.add(user);
 5
 6    request.onsuccess = () => {
 7        console.log('User added:', user);
 8    };
 9
10    request.onerror = () => {
11        console.error('Add failed:', request.error);
12    };
13}
14
15// Example: Add a user
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    addUser(db, { id: 1, name: 'Alice' });
19    addUser(db, { id: 2, name: 'Bob' });
20    addUser(db, { id: 3, name: 'John' });
21};
  • db.transaction() でトランザクションを作成し、操作対象のオブジェクトストアとモード(ここではreadwrite)を指定します。
  • store.add() メソッドで新規データを追加します。
  • トランザクションは自動的にコミットされますが、複数操作をまとめたい場合はトランザクションの終了イベントなどを使って管理できます。

データを取得する(主キー検索)

主キーを使って特定のデータを取得する方法は次のようになります。

 1function getUserById(db, id) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.get(id);
 5
 6    request.onsuccess = () => {
 7        if (request.result) {
 8            console.log('User found:', request.result);
 9        } else {
10            console.log('User not found');
11        }
12    };
13
14    request.onerror = () => {
15        console.error('Error retrieving user:', request.error);
16    };
17}
18
19// Example: Get a user by id
20request.onsuccess = function (event) {
21    const db = event.target.result;
22    getUserById(db, 1);
23};
  • db.transaction() で読み取り専用のトランザクションを作成します。
  • store.get(id) で指定した主キーに対応するデータを取得します。
  • 取得成功時は onsuccess が呼ばれ、結果があれば表示します。結果がない場合は該当データなしと判断します。

インデックス検索を使う

主キー以外のプロパティでデータを検索したい場合は、あらかじめ作成したインデックスを利用します。

 1function getUserByName(db, name) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.get(name);
 6
 7    request.onsuccess = () => {
 8        if (request.result) {
 9            console.log('User by name:', request.result);
10        } else {
11            console.log('No user found with that name');
12        }
13    };
14
15    request.onerror = () => {
16        console.error('Error retrieving user by name:', request.error);
17    };
18}
19
20// Example: Get a user by id
21request.onsuccess = function (event) {
22    const db = event.target.result;
23    getUserByName(db, 'Alice');
24};
  • store.index('name') によって、あらかじめ作成しておいた name インデックスにアクセスします。
  • index.get(value) は、該当する値を持つ最初の1件のレコードを取得します。 同じ値のレコードが複数存在する可能性がある場合は、index.getAll(value) を使うことで全件取得が可能です。

データの更新

既存のデータを上書きして更新するには、put() メソッドを使用します。

主キーが一致するレコードが存在すれば更新され、存在しない場合は新規追加されます。

 1function updateUser(db, updatedUser) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.put(updatedUser);
 5
 6    request.onsuccess = () => {
 7        console.log('User updated:', updatedUser);
 8    };
 9
10    request.onerror = () => {
11        console.error('Update failed:', request.error);
12    };
13}
14
15// Example: Update user
16request.onsuccess = async (event) => {
17    const db = event.target.result;
18
19    // Test : update existing user
20    updateUser(db, { id: 3, name: 'John Updated' });
21
22    // Test : insert new user
23    updateUser(db, { id: 4, name: 'Charlie' });
24};
  • put() は更新と追加の両方に対応する便利なメソッドです。
  • 主キーが一致するデータがすでに存在する場合、それが上書きされます。
  • 更新前に存在チェックをしたい場合は、先に get() を使って確認する方法もあります。

データの削除

指定した主キーに対応するデータを削除するには、delete() メソッドを使用します。

 1function deleteUser(db, id) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const request = store.delete(id);
 5
 6    request.onsuccess = () => {
 7        console.log(`User with id=${id} deleted successfully`);
 8    };
 9
10    request.onerror = () => {
11        console.error(`Failed to delete user with id=${id}:`, request.error);
12    };
13}
14
15// Example: Delete a user by id
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    deleteUser(db, 4);
19};
  • store.delete(id) を使って、主キーが一致するデータを削除します。
  • 対象のデータが存在しない場合でも、エラーにはならず成功とみなされる点に注意が必要です。
  • エラーハンドリングも忘れずに実装することで、より堅牢なコードになります。

すべてのデータを取得する

getAll()

オブジェクトストア内のすべてのレコードを取得するには、getAll() メソッドを使用します。

 1function getAllUsers(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.getAll();
 5
 6    request.onsuccess = () => {
 7        console.log('All users:', request.result);
 8    };
 9
10    request.onerror = () => {
11        console.error('Failed to retrieve users:', request.error);
12    };
13}
14
15// Example: Get all users
16request.onsuccess = function (event) {
17    const db = event.target.result;
18    getAllUsers(db);
19};
  • getAll() は、指定されたオブジェクトストア内の全レコードを配列として取得します。
  • 多くのデータを一度に取得する場合でも効率的に処理されます。
  • 結果は request.result に配列として格納されます。
  • 必ずエラーハンドリングを加えることで、失敗時の対応が可能になります。

openCursor()

openCursor() は、オブジェクトストアやインデックス内のレコードを 逐次的に走査 するためのメソッドです。大量のデータを一括で取得せず、1件ずつ処理したい場合に有効です。

 1function getAllUsersWithCursor(db) {
 2    const transaction = db.transaction('users', 'readonly');
 3    const store = transaction.objectStore('users');
 4    const request = store.openCursor();
 5
 6    request.onsuccess = () => {
 7        const cursor = request.result;
 8        if (cursor) {
 9            console.log('User:', cursor.value); // Process the current record
10            cursor.continue(); // Move to the next record
11        } else {
12            console.log('All users have been processed.');
13        }
14    };
15
16    request.onerror = () => {
17        console.error('Failed to open cursor:', request.error);
18    };
19}
20
21// Example: Get all users
22request.onsuccess = function (event) {
23    const db = event.target.result;
24    getAllUsersWithCursor(db);
25};
  • **openCursor()**で、カーソルを開始し、オブジェクトストア内のすべてのレコードを1件ずつ取得します。
  • **cursor.value**で、現在のレコードのデータオブジェクトを取得できます。
  • **cursor.continue()**で、カーソルを次のレコードに進めます。
  • cursor === null のときは、全レコードの走査が完了しています。

openCursor() を使った更新処理の例

たとえば、「nameAlice のユーザーの名前を Alicia に変更する」処理では次のようになります。

 1function updateUserName(db, oldName, newName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name'); // Use the 'name' index
 5    const request = index.openCursor(IDBKeyRange.only(oldName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const user = cursor.value;
11            user.name = newName; // Update the name
12            const updateRequest = cursor.update(user);
13
14            updateRequest.onsuccess = () => {
15                console.log('Updated user:', user);
16            };
17
18            updateRequest.onerror = () => {
19                console.error('Failed to update user:', updateRequest.error);
20            };
21
22            cursor.continue();
23        } else {
24            console.log('All matching users have been updated.');
25        }
26    };
27
28    request.onerror = () => {
29        console.error('Cursor error:', request.error);
30    };
31}
32
33// Example: Update user name
34request.onsuccess = function (event) {
35    const db = event.target.result;
36    updateUserName(db, 'Alice', 'Alicia');
37};
  • IDBKeyRange.only(oldName) IDBKeyRange.only を使うと、キーが oldName と完全に一致するレコードだけを検索対象にできます。特定の値にピンポイントでアクセスしたい場合に利用します。

  • cursor.update() cursor.value を更新した後に update() を呼び出すことで、該当レコードが上書きされます。

  • 複数マッチ対応 cursor.continue() を呼び出すことで、カーソルを次の一致するレコードへ進められます。これにより、同じキーや条件に一致する複数のレコードを順番に処理できます。

  • エラーハンドリング 処理が失敗した場合に onerror でログを出力するようにしておくと、原因調査や運用時のトラブルシューティングが容易になります。

openCursor() を使った削除処理の例

たとえば、「nameBob のすべてのユーザーを削除する」処理は次のようになります。

 1function deleteUsersByName(db, targetName) {
 2    const transaction = db.transaction('users', 'readwrite');
 3    const store = transaction.objectStore('users');
 4    const index = store.index('name');
 5    const request = index.openCursor(IDBKeyRange.only(targetName));
 6
 7    request.onsuccess = () => {
 8        const cursor = request.result;
 9        if (cursor) {
10            const deleteRequest = cursor.delete();
11
12            deleteRequest.onsuccess = () => {
13                console.log('Deleted user:', cursor.value);
14            };
15
16            deleteRequest.onerror = () => {
17                console.error('Failed to delete user:', deleteRequest.error);
18            };
19
20            cursor.continue();
21        } else {
22            console.log('All matching users have been deleted.');
23        }
24    };
25
26    request.onerror = () => {
27        console.error('Cursor error:', request.error);
28    };
29}
30
31// Example: Delete user by name
32request.onsuccess = function (event) {
33    const db = event.target.result;
34    deleteUsersByName(db, 'Bob');
35};
  • cursor.delete() cursor.delete()によって、現在のカーソル位置のレコードを削除します。結果は非同期で返されるため、onsuccess で処理を確認できます。

トランザクションと非同期処理の注意点

IndexedDBは非同期イベント駆動型です。すべての操作はonsuccessonerrorイベントでハンドリングする必要があります。複数の処理をまとめたい場合は、Promiseでラップするのが便利です。

 1function openDatabase() {
 2    return new Promise((resolve, reject) => {
 3        const request = indexedDB.open('MyDatabase', 1);
 4
 5        request.onupgradeneeded = function (event) {
 6            const db = event.target.result;
 7
 8            // Initialization process (creating object stores and indexes, etc.)
 9            const store = db.createObjectStore('users', { keyPath: 'id' });
10            store.createIndex('name', 'name', { unique: false });
11        };
12
13        request.onsuccess = function (event) {
14            const db = event.target.result;
15            resolve(db);
16        };
17
18        request.onerror = function () {
19            reject(request.error);
20        };
21    });
22}
23
24function addUserAsync(db, user) {
25    return new Promise((resolve, reject) => {
26        const transaction = db.transaction('users', 'readwrite');
27        const store = transaction.objectStore('users');
28        const request = store.add(user);
29
30        request.onsuccess = () => resolve();
31        request.onerror = () => reject(request.error);
32    });
33}
34
35async function main() {
36    try {
37        const db = await openDatabase();
38        await addUserAsync(db, { id: 1, name: 'Alice' });
39        console.log('User added successfully');
40    } catch (error) {
41        console.error('Error:', error);
42    }
43}
  • openDatabaseaddUserAsync のように Promise でラップすると、async/await で直感的に非同期処理を扱えるようになります。
  • これによりコールバック地獄を避け、コードが読みやすくなります。

まとめ

IndexedDBは、ブラウザ側で高度なデータ管理を行いたい場面で非常に強力な機能です。使い始めはイベントベースの非同期処理に戸惑うかもしれませんが、構造を理解すれば、クライアントサイドで本格的なデータ操作が可能になります。

特に以下の点を押さえておくと、よりスムーズに活用できます。

  • onupgradeneededで初期設定を行います。
  • トランザクションは読み取り・書き込みモードに注意します。
  • インデックスで効率的に検索できます。
  • データはオブジェクトとして保存でき、JSONと親和性が高いです。

IndexedDBをマスターすることで、PWAやオフラインアプリのデータ管理が格段にしやすくなります。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video