JavaScript and IndexedDB

JavaScript and IndexedDB

In this article, we will explain JavaScript and IndexedDB.

This tutorial provides a step-by-step explanation of JavaScript and IndexedDB, including practical sample code at each step to help deepen your understanding.

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 and IndexedDB

IndexedDB is an asynchronous key-value store database built into browsers. It offers features similar to relational databases and allows you to store and search large amounts of structured data on the client side. It is especially useful for offline-capable applications and PWAs (Progressive Web Apps).

Features of IndexedDB

  • Operates in an asynchronous and event-driven manner.
  • JavaScript objects can be stored in object stores.
  • Searching by queries or indexes is possible.
  • It has a large storage capacity (hundreds of MB or more), allowing you to store much more data than cookies or localStorage.

Opening and Creating a Database

To use IndexedDB, you must first open a database. If it does not exist, it is created automatically.

1const request = indexedDB.open('MyDatabase', 1); // Specify DB name and version
  • The open method opens the database asynchronously and triggers the following three events.

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};
  • The onsuccess event is triggered when the database opens successfully. Subsequent operations should be performed using request.result, which becomes available at this point.

onerror

1// Fired when database fails to open
2request.onerror = (event) => {
3    console.error('Failed to open database:', event.target.error);
4};
  • The onerror event is triggered when the database fails to open. Error logging and error handling should be performed here.

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 is triggered when the database is newly created or when the specified version is higher than the current version. Creating tables (object stores) and defining the schema should be done at this timing.

Creating an Object Store

First, create an 'object store' (equivalent to a table) within 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};

Here, the following settings are applied:.

  • createObjectStore createObjectStore is a method for creating a new object store in the database. You can store records in an object store and define how data is managed by specifying keyPath and other options. This process must be done inside the onupgradeneeded event.

  • keyPath: 'id' Set the keyPath to the id property, which uniquely identifies each record as a primary key. This allows id to be used automatically when adding, searching, or updating data.

  • createIndex Use the createIndex method to create an index based on the name property for searching. By setting unique: false, multiple records with the same name are allowed.

Handling Successful Database Connections

Assign processes to be executed upon a successful database connection to the onsuccess event. Within this process, you obtain the database instance and prepare for reading and writing data.

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};
  • This process executes when the IndexedDB database connection completes successfully.

  • event.target.result contains the opened database instance (IDBDatabase object), which is used to start transactions and access object stores.

  • Actual read and write operations such as adding, retrieving, updating, and deleting data are performed using the db object.

At this point, the database is ready, so you can safely start transactions.

Adding Data

Here is how to add new data to 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};
  • Create a transaction with db.transaction() and specify the object store to operate on and the mode (in this case, readwrite).
  • Add new data using the store.add() method.
  • Transactions are committed automatically, but if you want to group multiple operations, you can manage them using transaction end events.

Retrieving Data (Primary Key Search)

Here is how to retrieve specific data using the primary key.

 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};
  • Create a read-only transaction with db.transaction().
  • Use store.get(id) to retrieve the data corresponding to the specified primary key.
  • onsuccess is called when retrieval is successful, and the result is displayed if available. If there is no result, it is treated as 'no corresponding data.'.

Using Index Search

If you want to search by properties other than the primary key, use the index you created beforehand.

 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};
  • Access the pre-created name index using store.index('name').
  • index.get(value) retrieves the first record with the matching value. If multiple records have the same value, you can retrieve all of them with index.getAll(value).

Updating Data

To update existing data (overwrite), use the put() method.

If a record with a matching primary key exists, it is updated; if not, a new record is added.

 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() is a convenient method that supports both updating and adding.
  • If data with the same primary key already exists, it will be overwritten.
  • If you want to check for existence before updating, you can use get() to confirm beforehand.

Deleting Data

To delete data corresponding to a specified primary key, use the delete() method.

 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};
  • Use store.delete(id) to delete data with a matching primary key.
  • Note that even if the data does not exist, no error will occur and it will be considered a success.
  • Implementing error handling will result in more robust code.

Retrieving All Data

getAll()

To retrieve all records in the object store, use the getAll() method.

 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() retrieves all records in the specified object store as an array.
  • Even when retrieving large amounts of data at once, it is processed efficiently.
  • The results are stored as an array in request.result.
  • Always add error handling so you can deal with failures.

openCursor()

openCursor() is a method for sequentially traversing records in an object store or index. It is useful when you want to process data one item at a time instead of retrieving large amounts all at once.

 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};
  • By using openCursor(), you start a cursor and retrieve all records in the object store one by one.
  • Use cursor.value to get the data object of the current record.
  • Move the cursor to the next record with cursor.continue().
  • When cursor === null, all records have been traversed.

Example of an update process using openCursor()

For example, the process of changing the name of a user whose name is Alice to Alicia would look like this:.

 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) By using IDBKeyRange.only, you can target only the records whose key exactly matches oldName. This is useful when you want to access a specific value directly.

  • cursor.update() After updating cursor.value, calling update() will overwrite the corresponding record.

  • Handling Multiple Matches By calling cursor.continue(), you can move the cursor to the next matching record. This allows you to process multiple records that match the same key or condition in sequence.

  • Error Handling By outputting logs in onerror when a process fails, it becomes easier to investigate causes and troubleshoot during operation.

Example of a deletion process using openCursor()

For example, the process of deleting all users whose name is Bob would look like this:.

 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() With cursor.delete(), the record at the current cursor position is deleted. Since the result is returned asynchronously, you can check the process in onsuccess.

Notes on Transactions and Asynchronous Processing

IndexedDB is asynchronous and event-driven. All operations must be handled with onsuccess or onerror events. When you want to group multiple processes together, it is convenient to wrap them in a 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}
  • Wrapping functions like openDatabase or addUserAsync with Promise makes it possible to handle asynchronous processes intuitively with async/await.
  • This avoids callback hell and makes the code more readable.

Summary

IndexedDB is a very powerful feature when you want to perform advanced data management on the browser side. At first, you may be confused by event-based asynchronous processing, but once you understand the structure, you can perform full-scale data operations on the client side.

In particular, keeping the following points in mind will help you use it more smoothly:.

  • Perform initial setup with onupgradeneeded.
  • Pay attention to the read/write mode of transactions.
  • Indexes allow efficient searching.
  • Data can be stored as objects, making it highly compatible with JSON.

By mastering IndexedDB, data management for PWAs and offline apps becomes much easier.

You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.

YouTube Video