IndexedDB
Storing Browser Data: IndexedDB
What is IndexedDB?
View Answer:
How does IndexedDB differ from LocalStorage?
View Answer:
What are the key components of IndexedDB?
View Answer:
1. Database: This is the actual container for data storage. An IndexedDB database has a version and a set of object stores.
2. Object Stores: These are the IndexedDB equivalent of tables. Each database can have multiple object stores, and each object store can contain different types of data.
3. Keys: These are used to identify records in an object store. Every record in an object store must have a unique key. Keys can be simple types like integers, dates, and strings, or they can be arrays.
4. Indexes: Indexes are essentially object stores that store a key for every record in another object store. An index on a specific field in an object store allows efficient lookup of records based on that field, even if it's not the key.
5. Transactions: All interactions with data in IndexedDB happen in the context of a transaction. A transaction is a wrapper around an operation, or group of operations, with three possible modes: readonly, readwrite, and versionchange.
6. Versioning: IndexedDB uses a versioning model. Whenever the structure of the database changes (e.g., when object stores or indexes are created or removed), the version number of the database is increased.
7. Cursors: Cursors are used to iterate over multiple records in database. They offer a way to retrieve and update data in a specific sequence.
8. Requests: Almost every operation in IndexedDB is asynchronous and returns a request object. These requests are not the actual data but act as a placeholder for the data.
9. Queries: These are requests for data that meet certain criteria. Queries can retrieve data from both object stores and indexes.
10. Events: IndexedDB operations communicate success or failure by firing events at request objects. There are three types of events: success events, error events, and blocked events.
Can you explain the concept of "Object Stores" in IndexedDB?
View Answer:
let openRequest = indexedDB.open("myDatabase", 1);
openRequest.onupgradeneeded = function(e) {
let db = e.target.result;
// Create an object store named "books", with a key path of "isbn"
let store = db.createObjectStore("books", {keyPath: "isbn"});
// Create an index on the "author" property of the objects in the store
store.createIndex("author", "author", {unique: false});
};
openRequest.onsuccess = function(e) {
console.log("Success! Got a handle on the database.");
};
openRequest.onerror = function(e) {
console.error("Unable to open database: ", e.target.errorCode);
};
What is the importance of "Keys" in IndexedDB?
View Answer:
How does indexing work in IndexedDB?
View Answer:
let openRequest = indexedDB.open("myDatabase", 1);
openRequest.onupgradeneeded = function(e) {
let db = e.target.result;
// Create an object store named "books", with a key path of "isbn"
let store = db.createObjectStore("books", {keyPath: "isbn"});
// Create an index on the "author" property of the objects in the store.
// The third parameter is an options object, and here we're saying we
// want the "author" index to allow non-unique keys.
store.createIndex("author", "author", {unique: false});
};
openRequest.onsuccess = function(e) {
console.log("Success! Got a handle on the database.");
};
openRequest.onerror = function(e) {
console.error("Unable to open database: ", e.target.errorCode);
};
How do "Cursors" work in IndexedDB?
View Answer:
let openRequest = indexedDB.open("myDatabase", 1);
openRequest.onsuccess = function(e) {
let db = e.target.result;
let transaction = db.transaction("books", "readonly");
let objectStore = transaction.objectStore("books");
let request = objectStore.openCursor();
request.onsuccess = function(e) {
let cursor = e.target.result;
if (cursor) {
console.log("Key: ", cursor.key);
console.log("Value: ", cursor.value);
// Continue to the next item
cursor.continue();
} else {
console.log("End of data");
}
};
request.onerror = function(e) {
console.error("Error opening cursor: ", e.target.errorCode);
};
};
openRequest.onerror = function(e) {
console.error("Unable to open database: ", e.target.errorCode);
};
Can you perform join operations in IndexedDB like SQL?
View Answer:
Sure, here's a simple example of how you might manually implement relationships in IndexedDB:
Suppose you have two object stores, authors
and books
. Each book
object has an authorId
field, which is the key of the author in the authors
object store.
let db; // Assuming db is the opened IndexedDB database
// Get the transaction
let transaction = db.transaction(["books", "authors"]);
// Get the object stores
let bookStore = transaction.objectStore("books");
let authorStore = transaction.objectStore("authors");
// Let's find the details of the book and its author with bookId=1
let bookId = 1;
// Request to get the book
let bookRequest = bookStore.get(bookId);
bookRequest.onsuccess = function(e) {
let book = e.target.result;
console.log("Book: ", book.title);
// Request to get the author using the authorId from the book
let authorRequest = authorStore.get(book.authorId);
authorRequest.onsuccess = function(e) {
let author = e.target.result;
console.log("Author: ", author.name);
};
authorRequest.onerror = function(e) {
console.error("Error fetching author: ", e.target.error);
};
};
bookRequest.onerror = function(e) {
console.error("Error fetching book: ", e.target.error);
};
This code gets a book and its corresponding author from the database. First, it fetches the book from the "books" object store. Then, using the authorId
from the book, it fetches the author from the "authors" object store. This simulates a join operation.
How do you handle errors in IndexedDB?
View Answer:
authorRequest.onerror = function(e) {
console.error("Error fetching author: ", e.target.error);
};
What are the storage limits of IndexedDB?
View Answer:
Browser | Max. Storage Size | Max. Number of Databases | Max. Size per Database |
---|---|---|---|
Chrome | No limit | No limit | No limit |
Firefox | No limit | No limit | No limit |
Safari (Desktop) | 50 MB | 1 | 50 MB |
Safari (iOS) | 50 MB | 1 | 50 MB |
Microsoft Edge | No limit | No limit | No limit |
Internet Explorer 11 | 250 MB | 10 | 250 MB |
Opera | No limit | No limit | No limit |
Please note that these limitations are based on the information available up until March 2021. Browser versions and storage limitations may have changed since then. It's always recommended to refer to the latest browser documentation for the most up-to-date information on IndexedDB storage limitations.
What does "versioning" mean in IndexedDB?
View Answer:
// Open the database
const dBOpenRequest = window.indexedDB.open("toDoList", 4);
dBOpenRequest.onupgradeneeded = (event) => {
const db = event.target.result;
console.log(`Upgrading to version ${db.version}`);
// Create an objectStore for this database
const objectStore = db.createObjectStore("toDoList", {
keyPath: "taskTitle",
});
// define what data items the objectStore will contain
objectStore.createIndex("hours", "hours", { unique: false });
objectStore.createIndex("minutes", "minutes", { unique: false });
objectStore.createIndex("day", "day", { unique: false });
objectStore.createIndex("month", "month", { unique: false });
objectStore.createIndex("year", "year", { unique: false });
};
Overall, this code example demonstrates how to open an IndexedDB database, handle the upgrade event, create an object store, and define indexes on the object store.
Note: It's important to handle versioning properly to ensure smooth database upgrades and migrations in real-world scenarios.
Can IndexedDB be used synchronously or asynchronously?
View Answer:
// this is an example of deprecrated openDatabaseSync()
let db = window.openDatabaseSync("myDatabase", "1.0", "My Database", 2 * 1024 * 1024);
db.transaction(function(tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS books (id INTEGER PRIMARY KEY, title TEXT)');
tx.executeSql('INSERT INTO books (id, title) VALUES (?, ?)', [1, 'Book 1']);
tx.executeSql('INSERT INTO books (id, title) VALUES (?, ?)', [2, 'Book 2']);
});
let results = db.readTransaction(function(tx) {
let resultSet = tx.executeSql('SELECT * FROM books');
return resultSet.rows;
});
console.log(results);
Please note that openDatabaseSync() is a deprecated API, and it's recommended to use the asynchronous IndexedDB API for most modern applications.
Is IndexedDB transactional? Why is it important?
View Answer:
Can IndexedDB be used across different browsers?
View Answer:
Can IndexedDB handle concurrent transactions?
View Answer:
Here's an example demonstrating concurrent transactions in IndexedDB.
let openRequest = indexedDB.open("myDatabase", 1);
openRequest.onupgradeneeded = function(e) {
let db = e.target.result;
// Create object stores
db.createObjectStore("books");
db.createObjectStore("authors");
};
openRequest.onsuccess = function(e) {
let db = e.target.result;
// Start the first transaction (readwrite)
let transaction1 = db.transaction("books", "readwrite");
let store1 = transaction1.objectStore("books");
// Start the second transaction (readonly)
let transaction2 = db.transaction("authors", "readonly");
let store2 = transaction2.objectStore("authors");
// Perform operations on store1
let request1 = store1.put({ id: 1, title: "Book 1" });
request1.onsuccess = function() {
console.log("Added book to store1");
};
request1.onerror = function() {
console.error("Error adding book to store1");
};
// Perform operations on store2
let request2 = store2.get(1);
request2.onsuccess = function() {
console.log("Retrieved author from store2");
};
request2.onerror = function() {
console.error("Error retrieving author from store2");
};
// Commit the transactions
transaction1.oncomplete = function() {
console.log("Transaction 1 completed");
};
transaction2.oncomplete = function() {
console.log("Transaction 2 completed");
};
};
openRequest.onerror = function(e) {
console.error("Unable to open database: ", e.target.errorCode);
};
In this example, we open a database called "myDatabase" with version 1. We create two object stores, "books" and "authors".
We then start two transactions: transaction1 (readwrite) on the "books" object store and transaction2 (readonly) on the "authors" object store. Within each transaction, we perform separate operations on their respective object stores.
The example adds a book to store1 using put()
and retrieves an author from store2 using get()
. These operations are performed concurrently within their respective transactions.
After the operations, the transactions complete, and the corresponding oncomplete
event handlers are triggered.
Please note that concurrent transactions should be used with caution to avoid conflicts and ensure data consistency.
Is IndexedDB secure? Can its data be encrypted?
View Answer:
IndexedDB itself does not provide built-in encryption or security features. The data stored in IndexedDB is typically stored locally on the client-side and can be accessed by scripts running within the same origin. Therefore, it's important to be cautious when storing sensitive or confidential data in IndexedDB.
To secure the data stored in IndexedDB, you can employ encryption techniques yourself. One approach is to encrypt the data before storing it in IndexedDB and decrypt it when retrieving it. This way, the data remains encrypted while it's stored and can only be accessed with the appropriate encryption keys.
Encryption libraries or frameworks can be used in conjunction with IndexedDB to implement data encryption. Common cryptographic algorithms like AES or RSA can be utilized for encryption and decryption operations.
It's worth noting that encryption implementation and key management are critical aspects of securing data in IndexedDB, and proper security practices should be followed to protect sensitive information.
What is the onupgradeneeded event in IndexedDB?
View Answer:
let openRequest = indexedDB.open("myDatabase", 1);
openRequest.onupgradeneeded = function(event) {
let db = event.target.result;
// Perform database schema changes or initialization
// Example: Creating object stores, indexes, etc.
// Access the transaction associated with the upgrade
let upgradeTransaction = event.target.transaction;
// Do any necessary database modifications
// For example, creating object stores or indexes
let store = db.createObjectStore("books", { keyPath: "id" });
store.createIndex("titleIndex", "title", { unique: false });
};
openRequest.onsuccess = function(event) {
// Database opened or upgraded successfully
let db = event.target.result;
// Continue with other database operations
// For example, reading or writing data
};
openRequest.onerror = function(event) {
// Error occurred while opening or upgrading the database
console.error("Error opening database:", event.target.errorCode);
};
Can IndexedDB be used in a Service Worker?
View Answer:
// Open IndexedDB in the Service Worker
let openRequest = indexedDB.open("myDatabase", 1);
openRequest.onupgradeneeded = function(event) {
let db = event.target.result;
// Perform database schema changes or initialization
// Example: Creating object stores, indexes, etc.
};
openRequest.onsuccess = function(event) {
let db = event.target.result;
// Perform database operations
// Example: Store and retrieve data
// Close the IndexedDB connection
db.close();
};
openRequest.onerror = function(event) {
console.error("Error opening database:", event.target.errorCode);
};
You will have to wrap this code in your service worker. Here is a link if you would like to see a complete implemenation of the code. Integrating IndexedDB into site with a Service Worker
It's important to note that IndexedDB in a Service Worker operates in a different context compared to IndexedDB in a web page. The Service Worker has its own global scope, separate from web pages, and can persistently store data even when web pages are closed or not actively in use.
Where is data in the IndexedDB stored?
View Answer:
How do you initially open an IndexedDB database?
View Answer:
Syntax: indexedDB.open(name, version);
let openRequest = indexedDB.open('store', 1);
openRequest.onupgradeneeded = function () {
// triggers if the client had no database
// ...perform initialization...
};
openRequest.onerror = function () {
console.error('Error', openRequest.error);
};
openRequest.onsuccess = function () {
let db = openRequest.result;
// continue working with database using db object
};
What are the cross-domain rules that govern IndexedDB?
View Answer:
How do we delete an IndexedDB database using JavaScript?
View Answer:
let deleteRequest = indexedDB.deleteDatabase("myDatabase");
deleteRequest.onsuccess = function() {
console.log("Database deleted successfully");
};
deleteRequest.onerror = function(event) {
console.error("Error deleting database:", event.target.errorCode);
};
It's important to note that deleting a database is a permanent action, and the data within the database will be permanently removed. Therefore, exercise caution when deleting databases and ensure it's the desired outcome.
What is the reason a user cannot open an IndexedDB database based on versioning?
View Answer:
Is there a way to handle potential versioning issues with IndexedDB?
View Answer:
let openRequest = indexedDB.open("store", 2);
openRequest.onupgradeneeded = ...;
openRequest.onerror = ...;
openRequest.onsuccess = function() {
let db = openRequest.result;
db.onversionchange = function() {
db.close();
console.log("Database is outdated, please reload the page.")
};
// ...the db is ready, use it...
};
openRequest.onblocked = function() {
// this event shouldn't trigger if we handle onversionchange correctly
// it means that there's another open connection to same database
// and it wasn't closed after db.onversionchange triggered for it
};
What components do we need to store data in an IndexedDB database?
View Answer:
let openRequest = indexedDB.open("myDatabase", 1);
openRequest.onupgradeneeded = function(e) {
let db = e.target.result;
// Create an object store named "books" with "isbn" as the key path
let store = db.createObjectStore("books", { keyPath: "isbn" });
};
openRequest.onsuccess = function(e) {
let db = e.target.result;
// Start a transaction on the "books" object store
let transaction = db.transaction("books", "readwrite");
let store = transaction.objectStore("books");
// Add a book to the object store
let book = { isbn: "9781234567890", title: "Sample Book" };
let request = store.add(book);
request.onsuccess = function() {
console.log("Book added successfully");
};
request.onerror = function(event) {
console.error("Error adding book:", event.target.errorCode);
};
// Commit the transaction
transaction.oncomplete = function() {
console.log("Transaction completed");
};
transaction.onerror = function(event) {
console.error("Transaction error:", event.target.errorCode);
};
// Close the database connection
db.close();
};
openRequest.onerror = function(e) {
console.error("Unable to open database:", e.target.errorCode);
};
Please note that this is a simplified example, and in a real-world scenario, you may need to handle asynchronous operations and consider error handling, versioning, and more complex data manipulation.
What types of values can an IndexedDB database store?
View Answer:
Can you give an example of an Object that can’t be stored in IndexedDB?
View Answer:
Is there a specific type of key that we must use in IndexedDB?
View Answer:
How are keys implemented in an IndexedDB when we store objects?
View Answer:
Syntax: db.createObjectStore(name, options);
Here's an example showing different ways of using keys in IndexedDB:
// Open a database
let request = indexedDB.open("myDB", 1);
request.onupgradeneeded = function(e) {
let db = e.target.result;
// Using autoIncrement for keys
let autoIncrementStore = db.createObjectStore("AutoIncrementStore", { autoIncrement : true });
// Using keyPath for keys
let keyPathStore = db.createObjectStore("KeyPathStore", { keyPath: "id" });
// No keyPath specified, so keys will be provided manually during add/put operations
let manualKeyStore = db.createObjectStore("ManualKeyStore");
};
// Adding data to the stores
request.onsuccess = function(e) {
let db = e.target.result;
let tx = db.transaction(["AutoIncrementStore", "KeyPathStore", "ManualKeyStore"], "readwrite");
// Adding to autoIncrementStore, key will be auto-generated
tx.objectStore("AutoIncrementStore").add({name: "Alice"});
// Adding to keyPathStore, key is included in the object
tx.objectStore("KeyPathStore").add({id: 1, name: "Bob"});
// Adding to manualKeyStore, key is provided manually
tx.objectStore("ManualKeyStore").add({name: "Charlie"}, 1);
};
In this example, keys for "AutoIncrementStore" are automatically generated, keys for "KeyPathStore" are specified in the objects themselves (using "id" field), and for "ManualKeyStore" keys are provided manually during add
operation.
Can you explain the function of the createObjectStore JavaScript method?
View Answer:
Syntax: db.createObjectStore(name, options);
// Create an objectStore for this database
let objectStore = db.createObjectStore('toDoList', { keyPath: 'taskTitle' });
When can Object stores be created or modified in IndexedDB?
View Answer:
What are the two basic methods for upgrading an IndexedDB version?
View Answer:
We can implement per-version upgrade functions: from 1 to 2, from 2 to 3, from 3 to 4, and onwards. Then, in upgradeneeded we can compare versions (e.g., old 2, now 4) and run per-version upgrades step by step, for every intermediate version (2 to 3, then 3 to 4).
Or we can examine the database: retrieve a list of existing object stores as db.objectStoreNames. The object is a DOMStringList that provides contains(name) method to check for the existence of the objects, and then we execute updates depending on what exists and what does not.
For small databases, the second variant may be simpler.
let openRequest = indexedDB.open('db', 2);
// create/upgrade the database without version checks
openRequest.onupgradeneeded = function () {
let db = openRequest.result;
if (!db.objectStoreNames.contains('books')) {
// if there's no "books" store
db.createObjectStore('books', { keyPath: 'id' }); // create it
}
};
Please note that setVersion() is deprecated, and it's recommended to use the onupgradeneeded event instead to handle versioning and schema changes in IndexedDB.
What is a transaction in IndexedDB?
View Answer:
Can you explain the function of the transaction method?
View Answer:
Syntax: IDBDatabase.transaction(storeNames, mode, options);
let transaction = db.transaction('books', 'readwrite'); // (1)
// get an object store to operate on it
let books = transaction.objectStore('books'); // (2)
let book = {
id: 'js',
price: 10,
created: new Date(),
};
let request = books.add(book); // (3)
request.onsuccess = function () {
// (4)
console.log('Book added to the store', request.result);
};
request.onerror = function () {
console.log('Error', request.error);
};
What are the different types of IndexedDB transactions?
View Answer:
Why are there different types of IndexedDB transactions?
View Answer:
let transaction = db.transaction('books', 'readwrite'); // (1)
What are the two methods used for data storage in an Object Store?
View Answer:
Syntax: let request = books.add(book);
How do we mark an IndexedDB transaction as finished, with no more requests to come?
View Answer:
What is one of the side-effects of the transaction auto-commit principle?
View Answer:
let request1 = books.add(book);
request1.onsuccess = function () {
fetch('/').then((response) => {
let request2 = books.add(anotherBook); // (*)
request2.onerror = function () {
console.log(request2.error.name); // TransactionInactiveError
};
});
};
Do we need onerror/onsuccess for every request?
View Answer:
request.onerror = function (event) {
if (request.error.name == 'ConstraintError') {
console.log('Book with such id already exists'); // handle the error
event.preventDefault(); // don't abort the transaction
event.stopPropagation(); // don't bubble error up, "chew" it
} else {
// do nothing
// transaction will be aborted
// we can take care of error in transaction.onabort
}
};
What are the two main types of searches in an Object store?
View Answer:
Here's an example of both types of searches:
let request = indexedDB.open("myDB");
request.onsuccess = function(e) {
let db = e.target.result;
let tx = db.transaction("ObjectStore");
let store = tx.objectStore("ObjectStore");
// Get operation
let getRequest = store.get(1);
getRequest.onsuccess = function() {
console.log("Get operation:", getRequest.result);
};
// Cursor operation
let cursorRequest = store.openCursor();
cursorRequest.onsuccess = function() {
let cursor = cursorRequest.result;
if (cursor) {
console.log("Cursor operation:", cursor.value);
cursor.continue();
}
};
};
In this example, the "get" operation retrieves the object with key 1, and the "cursor" operation iterates over all the objects in the store.
Can you describe how a key range or value search works?
View Answer:
// get one book
books.get('js');
// get books with 'css' <= id <= 'html'
books.getAll(IDBKeyRange.bound('css', 'html'));
// get books with id < 'html'
books.getAll(IDBKeyRange.upperBound('html', true));
// get all books
books.getAll();
// get all keys, where id > 'js'
books.getAllKeys(IDBKeyRange.lowerBound('js', true));
By default, how does a IndexedDB Object store sort values?
View Answer:
How do you delete values in an IndexedDB Object store?
View Answer:
// find the key where price = 5
let request = priceIndex.getKey(5);
request.onsuccess = function () {
let id = request.result;
let deleteRequest = books.delete(id);
};
books.clear(); // clear the storage.
Can you explain what a cursor is and its relation to the IndexedDB database?
View Answer:
It also contains a few pieces of additional metadata and a couple of methods, like continue or primaryKey. As an object store is sorted internally by key, a cursor walks the store in key order (ascending by default). The cursor also has two optional arguments, including the range and direction. The range query is a key or a key range, same as for getAll. The direction sets the order to use and includes two parameters prev, and nextunique or prevunique. The prev parameter is the reverse order: down from the record with the biggest key. The nextunique and prevunique are similar, but the skip records with the same key (only for cursors over indexes, e.g., for multiple books with price=5 only the first one returns). The main difference of the cursor is that request.onsuccess triggers multiple times: once for each result.
Here's an example demonstrating the usage of a cursor in IndexedDB:
let request = indexedDB.open("myDB");
request.onsuccess = function(e) {
let db = e.target.result;
let tx = db.transaction("ObjectStore");
let store = tx.objectStore("ObjectStore");
let cursorRequest = store.openCursor();
cursorRequest.onsuccess = function() {
let cursor = cursorRequest.result;
if (cursor) {
// Access the current record using cursor.value
console.log("Cursor value:", cursor.value);
// Update the record
cursor.value.updatedField = "New value";
let updateRequest = cursor.update(cursor.value);
updateRequest.onsuccess = function() {
console.log("Record updated successfully");
};
// Delete the record
let deleteRequest = cursor.delete();
deleteRequest.onsuccess = function() {
console.log("Record deleted successfully");
};
cursor.continue(); // Move to the next record
} else {
console.log("Cursor iteration complete");
}
};
};
In this example, the cursor iterates over all records in the "ObjectStore". It accesses the current record using cursor.value
, updates the record using cursor.update()
, and deletes the record using cursor.delete()
. The cursor moves to the next record using cursor.continue()
.