Como trabalhar com o IndexedDB

Um guia para os fundamentos do IndexedDB.

Este guia aborda os conceitos básicos da API IndexedDB. Estamos usando a biblioteca IndexedDB Promised de Jake Archibald, que é muito semelhante à API IndexedDB, mas usa promessas (que você pode usar await para ter uma sintaxe mais sucinta). Isso simplifica a API e mantém a estrutura.

O que é IndexedDB?

O IndexedDB é um sistema de armazenamento NoSQL em grande escala que permite armazenar praticamente qualquer coisa no navegador do usuário. Além das ações comuns de pesquisa, get e put, o IndexedDB também suporta transações. Esta é a definição de IndexedDB no MDN:

IndexedDB é uma API de baixo nível para armazenamento de quantidades significativas de dados estruturados do lado do cliente, incluindo arquivos/blobs. Essa API usa índices para permitir pesquisas de alto desempenho desses dados. Embora o DOM Storage seja útil para armazenar quantidades menores de dados, ele é menos útil para armazenar grandes quantidades de dados estruturados. O IndexedDB oferece uma solução.

Cada banco de dados IndexedDB é exclusivo de uma origin (normalmente, o domínio ou subdomínio do site), o que significa que ele não pode acessar ou ser acessado por qualquer outra origem. Os limites de armazenamento de dados costumam ser muito grandes, se existirem, mas diferentes navegadores lidam com os limites e a remoção de dados de maneiras diferentes. Consulte a seção Leitura adicional para mais informações.

Termos do IndexedDB

Banco de dados
O nível mais alto de IndexedDB. Ela contém os armazenamentos de objetos, que, por sua vez, contêm os dados que você quer manter. É possível criar vários bancos de dados com qualquer nome.
Armazenamento de objetos
Um bucket individual para armazenar dados. Pense nos armazenamentos de objetos como sendo semelhantes a tabelas em bancos de dados relacionais tradicionais. Normalmente, há um armazenamento de objetos para cada tipo de dados (não JavaScript) de dados que você armazena. Por exemplo, considerando um aplicativo que mantém postagens de blog e perfis de usuário, é possível imaginar dois repositórios de objetos. Ao contrário das tabelas em bancos de dados tradicionais, os tipos de dados JavaScript reais do repositório não precisam ser consistentes. Por exemplo, se houver três pessoas no armazenamento de objetos people, as propriedades de idade delas poderão ser 53, 'twenty-five' e unknown.
Índice
Um tipo de armazenamento de objetos para organizar dados em outro repositório (chamado de repositório de objetos de referência) por uma propriedade individual dos dados. O índice é usado para recuperar registros no armazenamento de objetos por esta propriedade. Por exemplo, se você armazenar pessoas, é possível fazer buscas mais tarde pelo nome, idade ou animal favorito.
Operação
Uma interação com o banco de dados.
Transação
Um wrapper em torno de uma operação ou grupo de operações que garante a integridade do banco de dados. Se uma das ações de uma transação falhar, nenhuma delas será aplicada e o banco de dados retornará ao estado em que estava antes do início da transação. Todas as operações de leitura ou gravação no IndexedDB precisam fazer parte de uma transação. Isso permite operações atômicas de leitura-modificação-gravação sem precisar se preocupar com outras linhas de execução agindo no banco de dados ao mesmo tempo.
Cursor
Um mecanismo para iterar vários registros em um banco de dados.

Como verificar a compatibilidade com IndexedDB

O IndexedDB é quase compatível com todos. No entanto, se você estiver trabalhando com navegadores mais antigos, por precaução, não é uma má ideia detectar o suporte de recursos. A maneira mais fácil é verificar o objeto window:

function indexedDBStuff () {
  // Check for IndexedDB support:
  if (!('indexedDB' in window)) {
    // Can't use IndexedDB
    console.log("This browser doesn't support IndexedDB");
    return;
  } else {
    // Do IndexedDB stuff here:
    // ...
  }
}

// Run IndexedDB code:
indexedDBStuff();

Como abrir um banco de dados

Com o IndexedDB, você pode criar vários bancos de dados com qualquer nome. Se um banco de dados não existir ao tentar abri-lo, ele será criado automaticamente. Para abrir um banco de dados, use o método openDB() com a biblioteca idb:

import {openDB} from 'idb';

async function useDB () {
  // Returns a promise, which makes `idb` usable with async/await.
  const dbPromise = await openDB('example-database', version, events);
}

useDB();

Esse método retorna uma promessa que é resolvida em um objeto de banco de dados. Ao usar o openDB(), o método fornece um nome, número de versão e um objeto de eventos para configurar o banco de dados.

Confira um exemplo do método openDB() em contexto:

import {openDB} from 'idb';

async function useDB () {
  // Opens the first version of the 'test-db1' database.
  // If the database does not exist, it will be created.
  const dbPromise = await openDB('test-db1', 1);
}

useDB();

A verificação do suporte ao IndexedDB é feita na parte superior da função anônima. A função sairá se o navegador não oferecer suporte ao IndexedDB. Em seguida, chame o método openDB() para abrir um banco de dados chamado 'test-db1'. Neste exemplo, o objeto de eventos opcionais foi deixado de fora para simplificar, mas você vai precisar especificá-lo para fazer qualquer trabalho significativo com o IndexedDB.

Como trabalhar com repositórios de objetos

Um banco de dados IndexedDB contém um ou mais armazenamentos de objetos. O conceito de armazenamento de objetos é semelhante ao de uma tabela em um banco de dados SQL. Como as tabelas SQL, um armazenamento de objetos contém linhas e colunas, mas no IndexedDB, há menos flexibilidade no número de colunas, porque um armazenamento de objetos IndexedDB contém uma coluna para uma chave e outra para os dados associados a essa chave.

Criar repositórios de objetos

Considere, por exemplo, um site que mantém perfis e notas de usuário. Imagine um armazenamento de objetos people contendo objetos person e um armazenamento de objetos notes. Um banco de dados IndexedDB bem estruturado precisa ter um armazenamento de objetos para cada tipo de dado que precisa ser mantido.

Para garantir a integridade do banco de dados, os armazenamentos de objetos só podem ser criados e removidos no objeto de eventos em uma chamada openDB(). O objeto de eventos expõe um método upgrade() que oferece uma maneira de criar armazenamentos de objetos. Chame o método createObjectStore() no método upgrade() para criar o armazenamento de objetos:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('example-database', 1, {
    upgrade (db) {
      // Creates an object store:
      db.createObjectStore('storeName', options);
    }
  });
}

createStoreInDB();

Esse método usa o nome do armazenamento de objetos, bem como um objeto de configuração opcional que permite definir várias propriedades do armazenamento de objetos.

Confira abaixo um exemplo de como o método createObjectStore() é usado:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db1', 1, {
    upgrade (db) {
      console.log('Creating a new object store...');

      // Checks if the object store exists:
      if (!db.objectStoreNames.contains('people')) {
        // If the object store does not exist, create it:
        db.createObjectStore('people');
      }
    }
  });
}

createStoreInDB();

Neste exemplo, um objeto de eventos é transmitido ao método openDB() para criar o armazenamento de objetos e, como antes, o trabalho de criação do armazenamento de objetos é feito no método upgrade() do objeto de evento. No entanto, o navegador vai gerar um erro se você tentar criar um armazenamento de objetos que já existe. Por isso, envolva o método createObjectStore() em uma instrução if que verifica se esse armazenamento existe. No bloco if, chame createObjectStore() para criar um armazenamento de objetos chamado 'firstOS'.

Como definir chaves primárias

Ao definir repositórios de objetos, você pode definir como os dados são identificados exclusivamente no armazenamento usando uma chave primária. Você pode definir uma chave primária definindo um caminho de chave ou usando um gerador de chaves.

Um caminho de chave é uma propriedade que sempre existe e contém um valor exclusivo. Por exemplo, no caso de um armazenamento de objetos people, é possível escolher o endereço de e-mail como o caminho da chave:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }
    }
  });
}

createStoreInDB();

Este exemplo cria um armazenamento de objetos chamado 'people' e atribui a propriedade email como a chave primária na opção keyPath.

Também é possível usar um gerador de chaves, como autoIncrement. O gerador de chaves cria um valor exclusivo para cada objeto adicionado ao armazenamento de objetos. Por padrão, se você não especificar uma chave, o IndexedDB criará uma chave e a armazenará separadamente dos dados.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

Este exemplo cria um armazenamento de objetos chamado 'notes' e define a chave primária a ser atribuída automaticamente como um número de incremento automático.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

Este exemplo é semelhante ao anterior, mas desta vez o valor de incremento automático é explicitamente atribuído a uma propriedade chamada 'id'.

A escolha do método para definir a chave depende dos seus dados. Se os dados tiverem uma propriedade que é sempre exclusiva, você pode torná-la keyPath para aplicar essa exclusividade. Caso contrário, é recomendável usar um valor de incremento automático.

O código a seguir cria três repositórios de objetos, demonstrando as várias maneiras de definir chaves primárias em armazenamentos de objetos:

import {openDB} from 'idb';

async function createStoresInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }

      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }

      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoresInDB();

Como definir índices

Índices são um tipo de armazenamento de objetos usado para recuperar dados do armazenamento de objetos de referência por uma propriedade especificada. Um índice reside no repositório de objetos de referência e contém os mesmos dados, mas usa a propriedade especificada como caminho de chave, em vez de a chave primária do repositório de referência. Os índices precisam ser criados quando você cria seus armazenamentos de objetos e também podem ser usados para definir uma restrição exclusiva nos dados.

Para criar um índice, chame o método createIndex() em uma instância de repositório de objetos:

import {openDB} from 'idb';

async function createIndexInStore() {
  const dbPromise = await openDB('storeName', 1, {
    upgrade (db) {
      const objectStore = db.createObjectStore('storeName');

      objectStore.createIndex('indexName', 'property', options);
    }
  });
}

createIndexInStore();

Esse método cria e retorna um objeto de índice. O método createIndex() na instância do armazenamento de objetos usa o nome do novo índice como o primeiro argumento, e o segundo argumento se refere à propriedade nos dados que você quer indexar. O argumento final permite definir duas opções que determinam como o índice funciona: unique e multiEntry. Se unique for definido como true, o índice não vai permitir valores duplicados para uma única chave. Em seguida, multiEntry determina como createIndex() se comporta quando a propriedade indexada é uma matriz. Se ele for definido como true, o createIndex() vai adicionar uma entrada para cada elemento da matriz no índice. Caso contrário, ele vai adicionar uma única entrada contendo a matriz.

Confira um exemplo:

import {openDB} from 'idb';

async function createIndexesInStores () {
  const dbPromise = await openDB('test-db3', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        const peopleObjectStore = db.createObjectStore('people', { keyPath: 'email' });

        peopleObjectStore.createIndex('gender', 'gender', { unique: false });
        peopleObjectStore.createIndex('ssn', 'ssn', { unique: true });
      }

      if (!db.objectStoreNames.contains('notes')) {
        const notesObjectStore = db.createObjectStore('notes', { autoIncrement: true });

        notesObjectStore.createIndex('title', 'title', { unique: false });
      }

      if (!db.objectStoreNames.contains('logs')) {
        const logsObjectStore = db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createIndexesInStores();

Neste exemplo, os armazenamentos de objetos 'people' e 'notes' têm índices. Para criar os índices, primeiro atribua o resultado de createObjectStore() (que é um objeto de armazenamento de objetos) a uma variável para que você possa chamar createIndex() nele.

Como trabalhar com dados

Esta seção descreve como criar, ler, atualizar e excluir dados. Todas essas operações são assíncronas, usando promessas em que a API IndexedDB usa solicitações. Isso simplifica a API. Em vez de detectar eventos acionados pela solicitação, você pode chamar .then() no objeto do banco de dados retornado do método openDB() para iniciar interações com o banco de dados ou await a criação dele.

Todas as operações de dados no IndexedDB são realizadas dentro de uma transação. Cada operação tem este formato:

  1. Recebe o objeto do banco de dados.
  2. Abrir transação no banco de dados.
  3. Abrir repositório de objetos na transação.
  4. Executar operação no armazenamento de objetos.

Uma transação pode ser considerada um wrapper seguro em torno de uma operação ou grupo de operações. Se uma das ações de uma transação falhar, todas serão revertidas. As transações são específicas de um ou mais armazenamentos de objetos, que você define quando abre a transação. Eles podem ser somente leitura ou leitura e gravação. Isso significa se as operações dentro da transação leem os dados ou fazem uma alteração no banco de dados.

Criar dados

Para criar dados, chame o método add() na instância do banco de dados e transmita os dados que você quer adicionar. O primeiro argumento do método add() é o armazenamento de objetos em que você quer adicionar os dados. O segundo argumento é um objeto que contém os campos e dados associados a serem adicionados. Confira o exemplo mais simples, em que uma única linha de dados é adicionada:

import {openDB} from 'idb';

async function addItemToStore () {
  const db = await openDB('example-database', 1);

  await db.add('storeName', {
    field: 'data'
  });
}

addItemToStore();

Cada chamada add() ocorre dentro de uma transação. Portanto, mesmo que a promessa seja resolvida com sucesso, isso não significa necessariamente que a operação funcionou. Lembre-se, se uma das ações da transação falhar, todas as operações da transação serão revertidas.

Para garantir que a operação de adição foi realizada, verifique se toda a transação foi concluída usando o método transaction.done(). Essa é uma promessa que é resolvida quando a transação é concluída e é rejeitada em caso de erros na transação. Esse método não fecha a transação. A transação é concluída sozinha. Você deve executar essa verificação para todas as operações de "gravação", porque é sua única maneira de saber se as alterações no banco de dados foram realmente realizadas.

O código a seguir mostra o uso do método add(), mas usando uma transação desta vez:

import {openDB} from 'idb';

async function addItemsToStore () {
  const db = await openDB('test-db4', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('foods')) {
        db.createObjectStore('foods', { keyPath: 'name' });
      }
    }
  });
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Add multiple items to the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.add({
      name: 'Sandwich',
      price: 4.99,
      description: 'A very tasty sandwich!',
      created: new Date().getTime(),
    }),
    tx.store.add({
      name: 'Eggs',
      price: 2.99,
      description: 'Some nice eggs you can cook up!',
      created: new Date().getTime(),
    }),
    tx.done
  ]);
}

addItemsToStore();

Depois de abrir o banco de dados e criar um armazenamento de objetos, se necessário, abra uma transação chamando o método transaction(). Esse método usa um argumento para a loja em que você quer fazer transações, bem como o modo. Nesse caso, estamos interessados em gravar no repositório, então 'readwrite' é especificado no exemplo anterior.

A próxima etapa é começar a adicionar itens à loja como parte da transação. No exemplo anterior, estamos lidando com três operações no armazenamento 'foods', cada uma retornando uma promessa:

  1. Adicionando um registro de um sanduíche saboroso.
  2. Adicionando um registro para alguns ovos.
  3. Indicação de que a transação foi concluída (tx.done).

Como todas essas ações são baseadas em promessas, precisamos esperar a conclusão de todas. Transmitir essas promessas para Promise.all é uma forma legal e ergonômica de fazer isso. Promise.all aceita uma matriz de promessas e vai terminar quando todas as promessas transmitidas para ele forem resolvidas.

Para os dois registros que estão sendo adicionados, a interface store da instância de transação tem um método add que pode ser chamado, e os dados são transmitidos para cada um. A própria chamada Promise.all pode ser await e será concluída quando a transação for concluída.

Ler dados

Para ler dados, chame o método get() na instância do banco de dados recuperada usando o método openDB(). get() usa o nome do armazenamento, bem como o valor da chave primária do objeto que você quer recuperar dele. Este é um exemplo básico:

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('example-database', 1);

  // Get a value from the object store by its primary key value:
  const value = await db.get('storeName', 'unique-primary-key-value');
}

getItemFromStore();

Assim como acontece com add(), o método get() retorna uma promessa, para que você possa fazer await, se preferir, ou usar o callback .then() que todas as promessas oferecem, caso você não queira fazer isso.

O exemplo a seguir usa o método get() no armazenamento de objetos 'foods' do banco de dados 'test-db4' para receber uma única linha pela chave primária 'name':

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('test-db4', 1);
  const value = await db.get('foods', 'Sandwich');

  console.dir(value);
}

getItemFromStore();

Recuperar uma única linha do banco de dados é bastante simples: abra o banco de dados e especifique o armazenamento de objetos e o valor da chave primária da linha da qual você quer receber dados. Como o método get() retorna uma promessa, você pode usar await nela.

Atualizar dados

Para atualizar dados, chame o método put() no armazenamento de objetos. O método put() é semelhante ao método add() e também pode ser usado em vez de add() para criar dados no armazenamento de objetos. Este é o exemplo mais simples de como usar put() para atualizar uma linha em um armazenamento de objetos pelo valor da chave primária:

import {openDB} from 'idb';

async function updateItemInStore () {
  const db = await openDB('example-database', 1);

  // Update a value from in an object store with an in-line key:
  await db.put('storeName', { inlineKeyName: 'newValue' });

  // Update a value from in an object store with an out-of-line key.
  // In this case, the out-of-line key value is 1, which is the
  // auto-incremented value.
  await db.put('otherStoreName', { field: 'value' }, 1);
}

updateItemInStore();

Como outros métodos, esse método retorna uma promessa. Também é possível usar put() como parte de uma transação, da mesma forma que você faria com o método add(). Confira um exemplo que usa a loja 'foods', com a exceção de que atualizamos o preço do sanduíche e dos ovos:

import {openDB} from 'idb';

async function updateItemsInStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Update multiple items in the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.put({
      name: 'Sandwich',
      price: 5.99,
      description: 'A MORE tasty sandwich!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.store.put({
      name: 'Eggs',
      price: 3.99,
      description: 'Some even NICER eggs you can cook up!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.done
  ]);
}

updateItemsInStore();

A forma de atualização dos itens depende de como você define uma chave. Se você definir um keyPath, cada linha no armazenamento de objetos será associada ao que é conhecido como chave em linha. O exemplo anterior atualiza linhas com base nessa chave, e quando você atualiza linhas nessa situação, precisa especificar essa chave para que o item adequado no armazenamento de objetos seja realmente atualizado. Uma chave fora de linha é criada ao definir um autoIncrement como a chave primária.

Excluir dados

Para excluir dados, chame o método delete() no armazenamento de objetos:

import {openDB} from 'idb';

async function deleteItemFromStore () {
  const db = await openDB('example-database', 1);

  // Delete a value 
  await db.delete('storeName', 'primary-key-value');
}

deleteItemFromStore();

Assim como add() e put(), ele também pode ser usado como parte de uma transação:

import {openDB} from 'idb';

async function deleteItemsFromStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Delete multiple items from the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.delete('Sandwich'),
    tx.store.delete('Eggs'),
    tx.done
  ]);
}

deleteItemsFromStore();

A estrutura da interação do banco de dados é a mesma das outras operações. Verifique novamente se toda a transação foi concluída incluindo o método tx.done na matriz que você transmite para Promise.all para ter certeza de que a exclusão foi realizada.

Como receber todos os dados

Até agora, você só recuperou um objeto por vez na loja. Também é possível recuperar todos os dados (ou um subconjunto) de um repositório ou índice de objetos usando o método getAll() ou cursores.

Como usar o método getAll()

A maneira mais simples de recuperar todos os dados de um armazenamento de objetos é chamar o método getAll() no repositório ou índice de objetos, desta forma:

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('storeName');

  console.dir(allValues);
}

getAllItemsFromStore();

Esse método retorna todos os objetos no armazenamento de objetos, sem restrições. É a maneira mais direta de conseguir todos os valores de um armazenamento de objetos, mas também o menos flexível.

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('foods');

  console.dir(allValues);
}

getAllItemsFromStore();

Aqui, o getAll() é chamado no armazenamento de objetos 'foods'. Isso retorna todos os objetos do 'foods' do armazenamento ordenados pela chave primária.

Como usar cursores

Outra forma de recuperar todos os dados – uma que lhe traga mais flexibilidade do que simplesmente obter tudo de uma vez – é usar um cursor. Um cursor seleciona cada objeto em um armazenamento de objetos ou indexa um por um, permitindo que você faça algo com os dados à medida que eles são selecionados. Cursores, assim como as outras operações de banco de dados, funcionam dentro de transações.

Para criar o cursor, chame o método openCursor() no armazenamento de objetos. Isso é feito como parte de uma transação. Usando o armazenamento 'foods' dos exemplos anteriores, é assim que você avançaria um cursor em todas as linhas de dados em um armazenamento de objeto:

import {openDB} from 'idb';

async function getAllItemsFromStoreWithCursor () {
  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');

  // Open a cursor on the designated object store:
  let cursor = await tx.store.openCursor();

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

getAllItemsFromStoreWithCursor();

Nesse caso, a transação é aberta no modo 'readonly', e o método openCursor é chamado. Em uma repetição while subsequente, a linha na posição atual do cursor pode ter as propriedades key e value lidas, e você pode operar nesses valores da maneira que fizer mais sentido para seu aplicativo. Quando estiver tudo pronto, chame o método continue() do objeto cursor para ir para a próxima linha. A repetição while terminará quando o fim do conjunto de dados for alcançado.

Como usar cursores com intervalos e índices

É possível conseguir todos os dados de algumas maneiras diferentes, mas e se você quiser apenas um subconjunto dos dados com base em uma propriedade específica? É aqui que entram os índices. Os índices permitem que você busque os dados em um armazenamento de objetos por uma propriedade diferente da chave primária. É possível criar um índice em qualquer propriedade (que se torna o keyPath do índice), especificar um intervalo nessa propriedade e acessar os dados dentro desse intervalo usando o método getAll() ou um cursor.

Defina o intervalo usando o objeto IDBKeyRange. Esse objeto tem cinco métodos usados para definir os limites do intervalo:

Como esperado, os métodos upperBound() e lowerBound() especificam os limites superior e inferior do intervalo.

IDBKeyRange.lowerBound(indexKey);

ou:

IDBKeyRange.upperBound(indexKey);

Elas usam um argumento, que é o valor keyPath do índice do item que você quer especificar como limite máximo ou mínimo.

O método bound() é usado para especificar um limite máximo e um limite mínimo, e o limite mínimo é o primeiro argumento:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

O intervalo dessas funções é inclusivo por padrão, mas pode ser especificado como exclusivo transmitindo true como o segundo argumento (ou o terceiro e quarto no caso de bound(), para os limites mínimo e máximo, respectivamente). Um intervalo inclusivo inclui os dados nos limites do intervalo. Um intervalo exclusivo, não.

Vejamos um exemplo. Para esta demonstração, você criou um índice na propriedade 'price' no armazenamento de objetos 'foods'. Você também adicionou um pequeno formulário com duas entradas para os limites superior e inferior do intervalo. Imagine que você está transmitindo os limites inferior e superior à função como números de ponto flutuante que representam os preços:

import {openDB} from 'idb';

async function searchItems (lower, upper) {
  if (!lower === '' && upper === '') {
    return;
  }

  let range;

  if (lower !== '' && upper !== '') {
    range = IDBKeyRange.bound(lower, upper);
  } else if (lower === '') {
    range = IDBKeyRange.upperBound(upper);
  } else {
    range = IDBKeyRange.lowerBound(lower);
  }

  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');
  const index = tx.store.index('price');

  // Open a cursor on the designated object store:
  let cursor = await index.openCursor(range);

  if (!cursor) {
    return;
  }

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

// Get items priced between one and four dollars:
searchItems(1.00, 4.00);

Primeiro, o código recebe os valores dos limites e verifica se eles existem. O próximo bloco de código decide qual método usar para limitar o intervalo com base nos valores. Na interação do banco de dados, abra o armazenamento de objetos na transação como de costume e, em seguida, abra o índice 'price' no armazenamento de objetos. O índice 'price' permite pesquisar os itens por preço.

Em seguida, abra um cursor no índice e transmita o intervalo. O cursor agora retorna uma promessa que representa o primeiro objeto no intervalo ou undefined se não houver dados dentro do intervalo. O método cursor.continue() retorna um cursor que representa o próximo objeto e assim por diante na repetição até chegar ao fim do intervalo.

Como usar o controle de versões do banco de dados

Ao chamar o método openDB(), especifique o número da versão do banco de dados no segundo parâmetro. Em todos os exemplos neste guia, a versão foi definida como 1, mas é possível fazer upgrade de um banco de dados para uma nova versão caso seja necessário modificá-lo. Se a versão especificada for mais recente que a versão do banco de dados atual, o callback upgrade no objeto de evento será executado, permitindo que você adicione novos repositórios de objetos e índices ao banco de dados.

O objeto db no callback upgrade tem uma propriedade oldVersion especial, que indica o número da versão atual do banco de dados no navegador. É possível transmitir esse número de versão em uma instrução switch para executar blocos de código dentro do callback upgrade com base no número da versão do banco de dados. Confira um exemplo:

import {openDB} from 'idb';

const db = await openDB('example-database', 2, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');
    }
  }
});

Neste exemplo, definimos a versão mais recente do banco de dados como 2. Quando esse código é executado pela primeira vez e como o banco de dados ainda não existe no navegador, oldVersion é 0, e a instrução switch começa em case 0. No exemplo, isso resulta na adição de um armazenamento de objetos 'store' ao banco de dados.

Para criar um índice 'description' no armazenamento de objetos 'store', atualize o número da versão e adicione um novo bloco case da seguinte maneira:

import {openDB} from 'idb';

const db = await openDB('example-database', 3, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');

      case 2:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('description', 'description');
    }
  }
});

Supondo que o banco de dados que você criou no exemplo anterior ainda exista no navegador, quando isso executa, oldVersion é 2. O case 0 e o case 1 são ignorados, e o navegador executa o código em case 2, criando um índice 'description'. Depois que tudo isso terminar, o navegador terá um banco de dados na versão 3, contendo um armazenamento de objetos 'store' com índices 'name' e 'description'.

Sugestões de leitura

Os recursos a seguir podem fornecer um pouco mais de informações e contexto sobre o uso do IndexedDB.

Documentação do IndexedDB

Limites de armazenamento de dados