This is the first day of my participation in the More text Challenge. For details, see more text Challenge

preface

IndexedDB is a database for web browsers. It has a much larger storage space than localStorage. Sometimes we need to store a large amount of data to implement certain functions (such as chat). In addition to storing a large amount of data, IndexedDB also has the following characteristics: its data structure is the key type, except that the primary key must exist, other key values for each record can be defined at will; All of its operations are asynchronous.

Here’s a step-by-step guide to using IndexedDB and resolving some of the scenarios, using the chat scenario as an example.

Initializing the database

If you have a db.js class, create a db class and encapsulate init method. Init method will open the database with window.indexeddb.open (otherwise, it will be automatically created). This will return an object that has the following three callback apis: Onsuccess, which needs to be called back when the database connection is successful 2. Onupgradenneeded, which needs to be called back when the database version is upgraded 3. Onerror, which needs to be called back when the database connection fails

Use the following:

class DB {
  constructor() {
    this.db = null // Internal database objects
    this.tb_chat = 'chat' // This is the name of the table
    this.version = 1 / / version number
  }
  / / initialization
  init() {
    return new Promise((resolve, reject) = > {
      var that = this
    
      // Open database db_chat and specify database version number 1 (default: 1 if not specified)
      var request = window.indexedDB.open('db_chat'.this.version)
      
      // Callback when the database was successfully opened
      request.onsuccess = () = > {
        this.db = request.result
        resolve()
      }
      
      // When connecting to a database, the version number is larger than the existing version number or when the database is created for the first time
      request.onupgradeneeded = function (event) {
        this.db = event.target.result
        var objectStore
        // Create chat if there is no chat table (object repository)
        if (!this.db.objectStoreNames.contains(that.tb_chat)) {
          objectStore = this.db.createObjectStore(that.tb_chat, { keyPath: 'id' }) // Create table chat and specify ID as the primary key
          objectStore.createIndex('id'.'id', { unique: true }) // Create an id index that is unique
          objectStore.createIndex('uid'.'uid', { unique: false }) // Create the index of sender ID
          objectStore.createIndex('uid2'.'uid2', { unique: false }) // Create the receiver ID index
          objectStore.createIndex('sendTime'.'sendTime', { unique: true }) // Send time index
          objectStore.createIndex('uid_uid2'['uid'.'uid2'] and {unique: false }) // Send-receiver index}}// Callback when opening the database fails
      request.onerror = (error) = > {
        reject(error)
      }
    })
  }
}
Copy the code

Note that all operations related to IndexedDB are asynchronous, so the init method above uses a promise and writes code that needs to be performed immediately after the database is initialized to the promise’s then.

For ease of use, the DB class objects are directly exported here.

class DB {... }let db = new DB()
export default db
Copy the code

Where you need it, you can call it like this

// Import the above db.js and write the path 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 sections
})
Copy the code

If you need to set the primary key to increment, you can set it in either of the following ways. However, remember that the value of the primary key can only be controlled by IndexdDB itself when creating data after the increment is set. Otherwise, an error will be reported.

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

This is how it should be called after the auto-increment id is set

import db from db.js

db.init().then(() = > {
    / / operation
    // db.add({id: 1, content: 'hello'}
    db.add({content: 'hello'})
    // If the primary key is id, then the input data will be {id: 1, content: 'hello'}
    // If the primary key has no keyPath, then the incoming data will be {content: 'hello'}
})
Copy the code

The new operation

In IndexedDB, additions, deleutions, modifications, and searches are performed through transactions. If an exception occurs during an operation, the previous operation is rolled back and the data is returned to the previous operation.

ObjectStore (table name).add (JSON object) is used to add data to a table. In this transaction, you need to specify the table name (array) to be operated on and the processing mode (read-only or read-write).

class DB {...// Add data (return 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 the code

call

db.add(db.tb_chat, {id:1.uid:1.uid2:2.sendTime: 1622539418861.content:'Hello World'})
Copy the code

Delete operation

Delete data based on the primary key, similar to adding code.

class DB {...// Delete data based on 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 the code

call

Db.delete (db.tb_chat, 1)Copy the code

Modify the operating

Modify data based on the primary key

class DB {...// Modify the data based on 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 the code

call

Delete (db.tb_chat, {id:1, uid:1, uid2:2, sendTime: 1622539418861, content:'hi'}); // Delete (db.tb_chat, {id:1, UID :1, uid2:2, sendTime: 1622539418861, content:'hi'})Copy the code

Query operation

Query is the key difficulty of database operation, which usually involves various query scenarios. In initialneeded, you may have noticed that in the OnUpgradenneeded callback method, several indexes have been created at the same time as the table was created, this is because any query in IndexedDB must be an index or primary key, e.g. I need to query a chat record which has’ send time ‘equal to June 1, 2021, If the index of Send Time is not created, it cannot be queried.

Primary key queries and index queries

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

class DB {...// Query data by 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 the code

However, there are times when multiple indexes are required for a query. In this case, we can create a composite index composed of multiple indexes. Needed again to look at the Onupgradenneeded method, which has a composite “song-receiver” index composed of song-sender ID and receiver ID, which is needed to query “song-receiver” chat history.

objectStore.createIndex('uid_uid2'['uid'.'uid2'] and {unique: false }) // Send-receiver index
Copy the code

But, as we soon discovered, it’s not that simple, if we call it this way.

// Query the chat history between sender 1 and receiver 2
db.select(db.tb_chat, 'uid_uid2'[1.2]).then(res= > {
    console.log(res)
})
Copy the 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 that cursors are used when multiple records are queried. At this point we encapsulate another cursor query.

Cursor query

What is a cursor? A cursor is a pointer to a particular row in the data set. We can use the cursor to iterate through the result set of the query and put the data into the array and return the array.

// Query by index cursor
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)
      }
    })
}
Copy the code

call

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

The cursor query can also control the range of query conditions, for example, query “2019 ~2021” chat records. There are other cool queries you can do with the key code IDBKeyRange, but I won’t go into details here.

Modify the data table structure

Needed window. Indexeddb. open and onupgradenneeded methods are particularly important at times when we need to modify table structures, add, delete, change tables or indexes.

Suppose we add a new index “nickname” : nickname.

First, we need to change the database version number to be larger than the previous version number.

class DB {
  constructor() {
    this.db = null // Internal database objects
    this.tb_chat = 'chat' // This is the name of the table
    //this. Version = 1 //this
    this.version = 2 // New version number}... }Copy the code

And then modify the onUpgradenneeded methods

request.onupgradeneeded = function (event) {
    this.db = event.target.result
    var objectStore
    // Create chat if there is no chat table (object repository)
    if (!this.db.objectStoreNames.contains(that.tb_chat)) {
      objectStore = this.db.createObjectStore(that.tb_chat, { keyPath: 'id' }) // Create table chat and specify ID as the primary key
      objectStore.createIndex('id'.'id', { unique: true }) // Create an id index that is unique
      objectStore.createIndex('uid'.'uid', { unique: false }) // Create the index of sender ID
      objectStore.createIndex('uid2'.'uid2', { unique: false }) // Create the receiver ID index
      objectStore.createIndex('sendTime'.'sendTime', { unique: true }) // Send time index
      objectStore.createIndex('uid_uid2'['uid'.'uid2'] and {unique: false }) // Send-receiver index
    
      objectStore.createIndex('nickname'.'nickname', { unique: false }) // Index of sender nicknames}}Copy the code

After reinit database, we press F12, open application console, and find that IndexedDB chat table does not have the nickname index. The original structure of IndexedDB tables cannot be modified once they are created. We have to discard the old table and create a new one. In this case, we only need to change the table name of the constructor (which is why we specifically use a variable to store the table name).

class DB {
  constructor() {
    this.db = null // Internal database objects
    This. tb_chat = 'chat' // This is the old table name
    this.tb_chat = 'new_chat' / / the new name of the table
    //this. Version = 1 //this
    this.version = 3 // Change the version number to 3 because it has already been init once}... }Copy the code

Another possible concern here is that the old table data needs to be moved to the new table if it is useful. Therefore, it is important to be thoughtful when designing tables for IndexedDB.

other

If native code encapsulation isn’t a hassle, you can use the Dexie library.

The complete code

db.js

class DB {
      constructor() {
        this.db = null // Internal database objects
        this.tb_chat = 'chat' // This is the name of the table
        this.version = 1 / / version number
      }
      / / initialization
      init() {
        return new Promise((resolve, reject) = > {
          var that = this

          // Open database db_chat and specify database version number 1 (default: 1 if not specified)
          var request = window.indexedDB.open('db_chat'.this.version)

          // Callback when the database was successfully opened
          request.onsuccess = () = > {
            this.db = request.result
            resolve()
          }

          // When connecting to a database, the version number is larger than the existing version number or when the database is created for the first time
          request.onupgradeneeded = function (event) {
            this.db = event.target.result
            var objectStore
            // Create chat if there is no chat table (object repository)
            if (!this.db.objectStoreNames.contains(that.tb_chat)) {
              objectStore = this.db.createObjectStore(that.tb_chat, {
                keyPath: 'id'
              }) // Create table chat and specify ID as the primary key
              objectStore.createIndex('id'.'id', {
                unique: true
              }) // Create an id index that is unique
              objectStore.createIndex('uid'.'uid', {
                unique: false
              }) // Create the index of sender ID
              objectStore.createIndex('uid2'.'uid2', {
                unique: false
              }) // Create the receiver ID index
              objectStore.createIndex('sendTime'.'sendTime', {
                unique: true
              }) // Send time index
              objectStore.createIndex('uid_uid2'['uid'.'uid2'] and {unique: false
              }) // Send-receiver index}}// Callback when opening the database fails
          request.onerror = (error) = > {
            reject(error)
          }
        })
      }

      // Add data (return 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 based on 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 based on 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 by 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 by 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)
          }
        })
      }
}

let db = new DB()
export default db
Copy the code

call

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 the code