Aprender a usar o IndexedDB: armazenamento de dados em JavaScript

Aprender a usar o IndexedDB: armazenamento de dados em JavaScript

O IndexedDB é uma das soluções de armazenamento que os navegadores foram introduzindo ao longo dos anos.

Trata-se de uma base de dados chave/valor (uma base de dados noSQL) considerada a solução definitiva para guardar dados no navegador.

É uma API assíncrona, o que significa que a execução do código não bloqueia o resto do teu programa, com o ganho de experiência que isso traz aos utilizadores. Pode guardar uma quantidade indefinida de dados, embora, acima de um certo limiar, o utilizador seja convidado a autorizar o site a guardar mais.

Há outros métodos, os cookies e o Web Storage ( localStorage e sessionStorage ). O armazenamento local/de sessão tem o inconveniente de estar limitado a um tamanho pequeno: os navegadores dão de 2 MB a 10 MB de espaço por site.

Embora tecnicamente possas criar várias bases de dados por site, o habitual é criar uma só e, dentro dela, criar vários object stores.

Uma base de dados é privada de um domínio, pelo que nenhum outro site consegue aceder a uma base IndexedDB que não seja a sua.

Cada object store contém normalmente um conjunto de coisas, que podem ser:

  • objects
  • arrays
  • strings
  • numbers
  • dates

Por exemplo, podes ter um store que contém mensagens e outro que contém comentários.

Um store contém um certo número de objetos que possuem uma chave única, que representa a forma como um objeto pode ser identificado.

Podes modificar esses objetos através de transações, executando operações de adição, alteração, remoção e iteração sobre os elementos que contêm.

Desde a chegada das Promises no ES6, e da passagem subsequente das APIs para as promessas, a API IndexedDB parece um pouco da velha guarda.

Não há mal nenhum nisso, mas em todos os exemplos que vou explicar uso a biblioteca de promessas IndexedDB do Jake Archibald, uma camada fina sobre a API IndexedDB que a torna mais fácil de usar.

Esta biblioteca é também a usada em todos os exemplos sobre IndexedDB no site Google Developers


Criar uma base de dados IndexedDB

A forma mais simples é usar o unpkg, acrescentando-o ao cabeçalho da página:

javascript
<script type="module">
import { openDB, deleteDB } from 'https://unpkg.com/idb?module'
</script>

Antes de usares a API IndexedDB, verifica sempre se o navegador é compatível. Mesmo estando amplamente disponível, nunca sabes que navegador o utilizador tem:

javascript
(async () => {
  'use strict'

if (!('indexedDB' in window)) {
    console.warn('IndexedDB not supported')
    return
  }
})()

Como criar uma base de dados IndexedDB

Usa o openDB():

javascript
(async () => {
  //...

const dbName="mydbname"
  const storeName="store1"
  const version = 1
  const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
    }
  })
})()

Os 2 primeiros parâmetros são o nome da base de dados e a versão. O terceiro parâmetro, facultativo, é um objeto que contém uma função chamada apenas se o número de versão for superior ao da base de dados instalada. No corpo dessa função podes atualizar a estrutura (stores e índices) da base de dados.

Adicionar dados a um store

Adicionar dados na criação do store, ao inicializá-lo

Para adicionar dados usa-se o método put do object store, mas primeiro é preciso inicializá-lo com db.createObjectStore() no momento em que o criamos.

Com o put, o valor é o primeiro argumento e a chave é o segundo. Isto acontece porque, se indicares a keyPath ao criar o object store, deixas de precisar de escrever o nome da chave em cada put(): basta o valor.

Isto preenche o store0 logo depois de o termos criado:

javascript
(async () => {
  //...
  const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(dbName, version,{
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
      store.put('Hello world!', 'Hello')
    }
  })
})()

Adicionar dados a um store já criado, com transações

Para acrescentar elementos mais tarde, tens de criar uma transação de leitura/escrita, que garante a integridade da base de dados (se uma operação falhar, todas as operações da transação são anuladas e o estado volta ao inicial).

Para isso, usa uma referência ao objeto dbPromise que obtivemos ao chamar o openDB e executa:

javascript
(async () => {
  //...
  const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(/* ... */)

const tx = db.transaction(storeName, 'readwrite')
  const store = await tx.objectStore(storeName)

const val="hey!"
  const key = 'Hello again'
  const value = await store.put(val, key)
  await tx.done
})()

Obter dados de um store

Obter um item de um object store: get()

javascript
const key = 'Hello again'
const item = await db.transaction(storeName).objectStore(storeName).get(key)

Obter todos os itens de um object store: getAll()

Obter todas as chaves guardadas

javascript
const items = await db.transaction(storeName).objectStore(storeName).getAllKeys()

Obter todos os valores guardados

javascript
const items = await db.transaction(storeName).objectStore(storeName).getAll()

Apagar dados do IndexedDB

Apagar a base de dados, um object store e dados

Apagar por completo uma base de dados IndexedDB

javascript
const dbName="mydbname"
await deleteDB(dbName)

Apagar dados dentro de um object store

Usamos uma transação:

javascript
(async () => {
  //...

const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
    }
  })

const tx = await db.transaction(storeName, 'readwrite')
  const store = await tx.objectStore(storeName)

const key = 'Hello again'
  await store.delete(key)
  await tx.done
})()

Migrar a partir de uma versão anterior da base de dados

O terceiro parâmetro (facultativo) da função openDB() é um objeto que pode conter uma função de atualização chamada apenas se o número de versão for superior ao da base de dados instalada. No corpo dessa função podes atualizar a estrutura (stores e índices) da base de dados:

javascript
const name="mydbname"
const version = 1
openDB(name, version, {
  upgrade(db, oldVersion, newVersion, transaction) {
    console.log(oldVersion)
  }
})

Neste callback podes verificar a partir de que versão o utilizador está a atualizar e executar operações em função disso.

Podes fazer uma migração a partir de uma versão anterior da base de dados com esta sintaxe

javascript
(async () => {
  //...
  const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      switch (oldVersion) {
        case 0:
          // um store introduzido na versão 1
          db.createObjectStore('store1')
        case 1:
          // apagar o antigo store na versão 2 e criar um novo
          db.createObjectStore('store2', { keyPath: 'name' })
      }
      db.createObjectStore(storeName)
    }
  })
})()

Chave única

O createObjectStore(), como vês no caso 1, aceita um segundo parâmetro que indica a chave de índice da base de dados. É muito útil quando guardas objetos: as chamadas a put() não precisam de um segundo parâmetro, podem receber apenas o valor (um objeto) e a chave será mapeada para a propriedade do objeto que tem esse nome.

O índice dá-te uma forma de recuperar um valor mais tarde por essa chave específica, e tem de ser única (cada elemento tem de ter uma chave diferente).

Uma chave pode ficar em incremento automático, o que te dispensa de a gerir no código do cliente:

javascript
db.createObjectStore('notes', { autoIncrement: true })

Usa o incremento automático se os teus valores ainda não contiverem uma chave única.


Verificar se um object store existe

Podes verificar se um object store já existe chamando o método objectStoreNames():

javascript
const storeName="store1"

if (!db.objectStoreNames.contains(storeName)) {
  db.createObjectStore(storeName)
}

Apagar do IndexedDB

Apagar a base de dados, um object store e dados

Apagar uma base de dados

javascript
await deleteDB('mydb')

Apagar um object store

Um object store só pode ser apagado no callback de abertura de uma db, e esse callback só é chamado se indicares uma versão superior à que está instalada:

javascript
const db = await openDB('dogsdb', 2, {
  upgrade(db, oldVersion, newVersion, transaction) {
    switch (oldVersion) {
      case 0:
        // um store introduzido na versão 1
        db.createObjectStore('store1')
      case 1:
        // apagar o antigo store na versão 2 e criar um novo
        db.deleteObjectStore('store1')
        db.createObjectStore('store2')
    }
  }
})

Para apagar dados de um object store, usa uma transação

javascript
const key = 232

const db = await openDB(/*...*/)
const tx = await db.transaction('store', 'readwrite')
const store = await tx.objectStore('store')
await store.delete(key)
await tx.complete

JavaScript

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.