IndexedDB-How to use

IndexedDB-How to use

This is the first day I participated in the Wenwen Challenge. For details of the event, please view: Wenwen Challenge

Preface

IndexedDB is a database of a web browser. Its storage space is much larger than localStorage. Sometimes we need to store a large amount of data when we implement certain functions (such as chat). At this time, IndexedDB can be used.
In addition to storing large amounts of data, IndexedDB also has these features. Its data structure is a key value type. In addition to the primary key, which must exist, the other key values of each record can be defined at will; all its operations are asynchronous .

The following will introduce how to use IndexedDB and solve some scene problems one by one, taking the chat scene as an example.

Initialize the database

Suppose there is a db.js, create a DB class in it, encapsulate the init method, in the init method first use window.indexedDB.open to open the corresponding database (if not, it will be created automatically), this time will return an object, this object There are mainly the following three callback APIs:
1. onsuccess, callback when the database connection is successful
2. onupgradeneeded, callback when the database version is upgraded
3. onerror, callback when the database connection fails

Use as follows:

class DB { constructor () { The this .db = null //database objects inside the this .tb_chat = 'Chat' //This is the name of the table the this .version = . 1 //version number } //Initialize init () { return new Promise ( ( resolve, reject ) => { var that = this //Open the database db_chat, and specify the database version number as 1 (if not specified, the default is 1) var request = window .indexedDB.open( 'db_chat' , this .version) // Call back when the database is successfully opened request.onsuccess = () => { this .db = request.result resolve() } //When connecting to the database, the version number is greater than the existing version number or when the database is created for the first time, it will also be called back. The data structure can be changed by using the version number request.onupgradeneeded = function ( event ) { this .db = event. target.result var objectStore //If there is no chat table (object store), create a chat if (! this .db.objectStoreNames.contains(that.tb_chat)) { objectStore = this .db.createObjectStore(that.tb_chat, { keyPath : 'id' }) //Create a table chat, and specify id as the primary key objectStore.createIndex( 'id' , 'id' , { unique : true }) //Create id index with uniqueness objectStore.createIndex( 'uid' , 'uid' , { unique : false }) //Create sender id index objectStore.createIndex( 'uid2' , 'uid2' , { unique : false } ) //Create receiver id index objectStore.createIndex( 'sendTime' , 'sendTime' , { unique : true }) //send time index objectStore.createIndex( 'uid_uid2' , [ 'uid' , 'uid2' ], { unique : false }) //send Recipient-recipient index } } // Call back when the database fails to open request.onerror = ( error ) => { reject(error) } }) } } Copy code

Note that all related operations of IndexedDB are asynchronous, so the init method above uses a promise, which allows some code that needs to be operated immediately after the database is initialized to be written into the then of the promise.

For ease of use, here directly export the object of the DB class.

class DB { ... } the let DB = new new DB () Export default DB duplicated code

Where you need to use it, you can call it like this

//Import the above db.js, the path is written as your own import db from db.js db.init().then( () => { //Operation db.add({ id : 1 , content : 'hello' }) //Combine the new code in the following chapters }) Copy code

If you need to set the primary key to auto-increment, you can set it in the following two ways, but remember, after setting auto-increment, when creating new data, the value of the primary key can only be controlled by IndexdDB itself, and custom values cannot be passed in, otherwise an error will be reported.

  1. The primary key has keyPath. When adding data, the id attribute will be automatically added to the data
= objectStore the this .db.createObjectStore (that.tb_chat, { keyPath : 'ID' , autoIncrement : to true }) //when creating the table, add autoIncrement copy the code
  1. The primary key has no keyPath, the primary key will only have the concept of value, and will not add primary key information to the data
= objectStore the this .db.createObjectStore (that.tb_chat, { autoIncrement : to true }) //when creating the table, add autoIncrement copy the code

After setting the auto-increment id, it should be called like this

import db from db.js db.init().then( () => { //Operation //db.add({id: 1, content:'hello'})//If the id is set as an auto-increment primary key, it cannot be called like this, it will Error db.add({ content : 'hello' }) //If the keyPath of the primary key is id, then the stored data will be {id: 1, content:'hello'} //If the primary key does not have a keyPath, then enter the database The data will be {content:'hello'} }) Copy code

New operation

In IndexedDB, the addition, deletion, modification, and query are all operated through transactions. The so-called transaction is that if an operation is abnormal in the middle, the previous operation will be rolled back, and the data will return to the way it was before the operation.

Before adding data, create a transaction. The transaction must specify the table name (array) and processing method (read-only or read-write) that needs to be operated. After the transaction object is created, use the transaction object to call objectStore (table name) .add (json object) can add data.

class DB { ... //Add data (return to primary key) add ( tb, data ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb], 'readwrite' ).objectStore(tb) .add(data) request.onsuccess = ( event ) => { resolve(event.target.result) } request.onerror = ( event ) => { reject(event) } }) } } Copy code

transfer

db.add (db.tb_chat, { ID : . 1 , UID : . 1 , UID2 : 2 , sendTime : 1,622,539,418,861 , Content : 'the Hello World' }) copy the code

Delete operation

Deleting data based on the primary key is similar to adding code.

class DB { ... //Delete data according to the primary key delete ( tb, key ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb], 'readwrite' ) .objectStore(tb) .delete(key) request.onsuccess = ( event ) => { resolve(event) } request.onerror = ( error ) => { reject(error) } }) } } Copy code

transfer

//Delete the data whose primary key is 1 db.delete(db.tb_chat, 1) Copy code

Modify operation

Modify the data according to the primary key (it seems to modify the data can only be modified through the primary key, not through other conditions)

class DB { ... //Modify the data according to the primary key update ( tb, data ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb], 'readwrite' ) .objectStore(tb) .put(data) request.onsuccess = ( event ) => { resolve(event) } request.onerror = ( error ) => { reject(error) } }) } } Copy code

transfer

//Modify the data whose primary key is 1, and modify the content to hi db.delete(db.tb_chat, {id:1, uid:1, uid2:2, sendTime: 1622539418861, content:'hi'}) Copy code

Query operation

Query is the key and difficult point of database operation, and it usually involves various scenarios of query.
In the initialization operation, you may notice that in the onupgradeneeded callback method, when the table is created, several indexes are also created at the same time. This is because in IndexedDB, any query condition must be an index or a primary key. For example, I want to query " "Send time" is equal to June 1, 2021" chat records, if the "send time" index is not created, it cannot be queried.

Primary key query and index query

The code for primary key query and index query is posted below.

class DB { ... //Query data based on the primary key selectById ( tb, key ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb]) .objectStore(tb) .get(key) request.onsuccess = ( event ) => { if (request.result) { resolve(request.result) } else { resolve() } } request.onerror = ( error ) => { reject(error) } }) } //Query by index select ( tb, index, content ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb]) .objectStore(tb) .index(index) .get(content) request.onsuccess = ( event ) => { if (request.result) { resolve(request.result) } else { resolve() } } request.onerror = ( error ) => { reject(error) } }) } } Copy code

However, sometimes we will encounter the need for multiple indexes as a condition for query. At this time, we can build a composite index composed of multiple indexes. Looking back at the onupgradeneeded method, is there a "sender-receiver" composite index that is a combination of the sender id and the receiver id. This is for querying the chat history of the "sender-receiver".

objectStore.createIndex ( 'uid_uid2' , [ 'UID' , 'UID2' ], { UNIQUE : to false }) //sender - receiver index duplicated code

However, we soon discovered that it is not so simple if we call like this.

//Query the chat history of the sender with id 1 and the receiver with id 2 db.select(db.tb_chat, 'uid_uid2' , [ 1 , 2 ]).then( res => { console .log(res) }) Copy code

Even if there are multiple chat records, we will find that only one record can be queried, which obviously cannot meet our business needs. Therefore, we need to use cursors. Remember to use cursors in scenarios where multiple records need to be queried. At this time, we encapsulate a cursor query.

Cursor query

What is a cursor? The cursor is a pointer to a specific row of the data set. We can traverse the result set of the query through the cursor and put the data in the array, and return the array.

//Query selectList according to the index cursor ( tb, index, content ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb]).objectStore(tb).index(index) //Create a cursor var c = request.openCursor(IDBKeyRange.only(content)) var arr = [] c.onsuccess = ( event ) => { var cursor = event.target.result if (cursor) { arr.push(cursor.value) cursor.continue() } else { resolve(arr) } } c.onerror = ( error ) => { reject(error) } }) } Copy code

transfer

//Query the chat history of the sender with id 1 and the receiver with id 2 db.selectList(db.tb_chat, 'uid_uid2' , [ 1 , 2 ]).then( res => { console .log(res) }) Copy code

Cursor query can also control the range of query conditions, for example, query the chat history of "2019~2021". As long as you use the key code IDBKeyRange method, you can also do other cool queries. I won't elaborate on the query methods one by one here.

Modify the data table structure

Sometimes, we need to modify the data table structure, add, delete, change the data table or index, this time window.indexedDB.open and onupgradeneeded methods are especially important.

Suppose we add a new index "sender nickname": nickname.

First of all, we must first modify the version number of the database to be larger than the previous version number.

class DB { constructor () { The this the .db = null //database objects inside the this .tb_chat = 'Chat' //This is the table name //this.version = 1//old version the this .version = 2 //new version number } ... } Copy code

Then modify the onupgradeneeded method

request.onupgradeneeded = function ( event ) { this .db = event.target.result var objectStore //If there is no chat table (object store), create a chat if (! this .db.objectStoreNames.contains(that.tb_chat) ) { objectStore = this .db.createObjectStore(that.tb_chat, { keyPath : 'id' }) //Create a table chat, and specify id as the primary key objectStore.createIndex( 'id' , 'id' , { unique : true }) //Create id index with uniqueness objectStore.createIndex( 'uid' , 'uid' , { unique : false }) //Create sender id index objectStore.createIndex( 'uid2' , 'uid2' , { unique : false } )//Create receiver id index objectStore.createIndex( 'sendTime' , 'sendTime' , { unique : true }) //send time index objectStore.createIndex( 'uid_uid2' , [ 'uid' , 'uid2' ], { unique : false }) //send Recipient-recipient index objectStore.createIndex( 'nickname' , 'nickname' , { unique : false }) //sender's nickname index } } Copy code

After re-init the database, we press F12 and open the application of the console, we can find that the chat table of IndexedDB does not have a nickname index. The original IndexedDB table cannot be modified once it is created. We had to abandon the old table and create a new one. Here, we only need to change the table name of the constructor (this is why a variable is used to store the table name).

class DB { constructor () { this .db = null //Internal database object// this.tb_chat ='chat'//This is the old table name this .tb_chat = 'new_chat' //New table name //this.version = 1//The old version number this .version = 3 //The new version number, the version number is changed to 3, because the above has been init once } ... } Copy code

There may also be a problem involved here, that is, if the old table data is useful, it needs to be transferred to the new table. Therefore, when designing IndexedDB tables, we must be considerate.

other

If you think it is troublesome to encapsulate the native code, you can use the dexie library directly.

Complete code

db.js

class DB { constructor () { The this .db = null //database objects inside the this .tb_chat = 'Chat' //This is the name of the table the this .version = . 1 //version number } //Initialize init () { return new Promise ( ( resolve, reject ) => { var that = this //Open the database db_chat, and specify the database version number as 1 (if not specified, the default is 1) var request = window .indexedDB.open( 'db_chat' , this .version) // Call back when the database is successfully opened request.onsuccess = () => { this .db = request.result resolve() } //When connecting to the database, the version number is greater than the existing version number or when the database is created for the first time, it will also be called back. You can use the version number to change the data structure request.onupgradeneeded = function ( event ) { this .db = event. target.result var objectStore //If there is no chat table (object store), create a chat if (! this .db.objectStoreNames.contains(that.tb_chat)) { objectStore = this .db.createObjectStore(that.tb_chat, { keyPath : unique : false 'id' }) //Create a table chat, and specify id as the primary key objectStore.createIndex( 'id' , 'id' , { unique : true }) //Create id index, unique objectStore.createIndex( 'uid' , 'uid' , { unique : false }) //Create sender id index objectStore.createIndex( 'uid2' , 'uid2', { }) //Create receiver id index objectStore.createIndex( 'sendTime' , 'sendTime' , { unique : true }) //Send time index objectStore.createIndex( 'uid_uid2' , [ 'uid' , 'uid2' ], { unique : false }) //sender-receiver index } } // Call back when the database fails to open request.onerror = ( error ) => { reject(error) } }) } //Add data (return to primary key) add ( tb, data ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb], 'readwrite' ).objectStore(tb) .add(data) request.onsuccess = ( event ) => { resolve(event.target.result) } request.onerror = ( event ) => { reject(event) } }) } //Delete data according to the primary key delete ( tb, key ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb], 'readwrite' ) .objectStore(tb) .delete(key) request.onsuccess = ( event ) => { resolve(event) } request.onerror = (error ) => { reject(error) } }) } //Modify the data according to the primary key update ( tb, data ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb], 'readwrite' ) .objectStore(tb) .put(data) request.onsuccess = ( event ) => { resolve(event) } request.onerror = ( error ) => { reject(error) } }) } //Query data based on the primary key selectById ( tb, key ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb]) .objectStore(tb) .get(key) request.onsuccess = ( event ) => { if (request.result) { resolve(request.result) } else { resolve() } } request.onerror = ( error ) => { reject(error) } }) } //Query by index select ( tb, index, content ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb]) .objectStore(tb) .index(index) .get(content) request.onsuccess = ( event ) => { if (request.result) { resolve(request.result) } else { resolve() } } request.onerror = ( error ) => { reject(error) } }) } //Query the list according to the index selectList ( tb, index, content ) { return new Promise ( ( resolve, reject ) => { var request = this .db.transaction([tb]).objectStore(tb).index(index) //Create a cursor var c = request.openCursor(IDBKeyRange.only(content)) var arr = [] c.onsuccess = ( event ) => { var cursor = event.target.result if (cursor) { arr.push(cursor.value) cursor.continue() } else { resolve(arr) } } c.onerror = ( error ) => { reject(error) } }) } } the let DB = new new DB () Export default DB duplicated code

transfer

import db from db.js db.init().then(() => { db.add(db.tb_chat, {id:1, uid:1, uid2:2, sendTime: 1622539418861, content:'hi'}) db.selectList(db.tb_chat,'uid_uid2', [1, 2]).then(res => { console.log(res) }) db.delete(db.tb_chat, 1) }) Copy code