O que é o IndexedDB
O IndexedDB é uma api que permite armazenar dados localmente no navegador do usuário. Com ela você pode criar aplicativos que podem ser usados offline, fazer queries, ordenar registros e muito mais. Atualmente essa API é a melhor solução para aplicativos que precisam armazenar uma grande quantidade de dados localmente.
Criando um banco de dados
Para criar ou abrir uma conexão com o banco de dados é só executar o método open.
O primeiro parâmetro é o nome do banco de dados e o segundo parâmetro é a versão do banco. A versão do banco é um número inteiro e ela determina a estrutura do banco de dados. Quando o banco não existe ou quando muda a versão, o evento onupgradeneeded é executado e com ele é possível criar as tabelas, índices ou popular o banco.
Esse método retorna um objeto do tipo IDBOpenDBRequest que herda do objeto IDBRequest. O IDBOpenDBRequest possui o evento onupgradeneeded e o IDBRequest possui os eventos onerror para execuções com erros e o onsuccess para execuções com sucesso.
O metodo onupgradeneeded possui, no evento recebido por parâmetro, duas propriedades, a oldVersion e a newVersion. Com elas você pode atualizar seu banco de dados a medida que vai incrementando versões.
Veja abaixo um exemplo completo da criação de um banco de dados.
var request = indexedDB.open("MeuBanco", 1);
request.onupgradeneeded = function(event) {
//fazer a criação das tabelas, indices e popular o banco se necessário
}
request.onsuccess = function (event) {
//sucesso ao criar/abrir o banco de dados
}
request.onerror = function (event) {
//erro ao criar/abrir o banco de dados
}
Criando a estrutura do banco de dados
O IndexedDB trabalha armazenando objetos em object store e cada banco pode conter varias object stores e para cada uma devemos definir uma chave. Para criar uma object store é necessário executar método createObjectStore.
O primeiro parâmetro é o nome da object store e o segundo é um parâmetro opcional com um objeto, com as informações da chave, com os campo keyPath e autoIncrement. O keyPath é responsável por definir o nome da chave e o autoIncrement é responsável por definir se a chave será gerada automaticamente.
Veja abaixo um exemplo de criação de object stores com os tipos de definição de chaves possíveis.
var request = indexedDB.open("MeuBanco", 1);
request.onupgradeneeded = function(event) {
var db = event.target.result;
//Object store sem key path e sem key generator
//Nesse exemplo vc deve informar a chave ao incluir um objeto
var store1 = db.createObjectStore("store1");
store1.add({campo: 'Valor 1'}, 1); //1 = chave do objeto
store1.add({campo: 'Valor 2'}, 2); //2 = chave do objeto
//Object store com key path
//Nesse exemplo o campo definido como chave deve possuir um valor e deve ser único
var store2 = db.createObjectStore("store2", { keyPath: "minhaChave" });
store2.add({minhaChave: 'Chave1', campo: 'Valor 1'});
store2.add({minhaChave: 'Chave2', campo: 'Valor 2'});
//Object store com key generator
//Nesse exemplo uma chave será criada automaticamente ao incluir um objeto
var store3 = db.createObjectStore("store3", { autoIncrement: true });
store3.add({campo: 'Valor 1'});
store3.add({campo: 'Valor 2'});
//Object store com key path e key generator
//Nesse exemplo uma chave será criada automaticamente no campo "minhaChave" ao incluir um
//novo objeto
var store4 = db.createObjectStore("store4", { keyPath: "minhaChave", autoIncrement: true });
store4.add({campo: 'Valor 1'});
store4.add({campo: 'Valor 2'});
}
Alem das object stores temos os indexes, que são responsáveis por permitir buscar um registro no banco de dados por outros campos sem ser pela chave.
Para criar um index é necessário executar o método createIndex. O primeiro parâmetro é o nome do index, o segundo parâmetro é a coluna que você deseja criar o index e o terceiro parâmetro é um objeto onde você informa se o index só pode ter valores únicos ou não.
Veja abaixo um exemplo de criação de index.
request.onupgradeneeded = function(event) {
var db = event.target.result;
var storeContato = db.createObjectStore("contato", { keyPath: "id", autoIncrement: true });
//cria um index por cpf e define que não poderá ter cpf duplicado no banco de dados
storeContato.createIndex("cpf", "cpf", { unique: true });
//cria um index por nome e permite nomes duplicados no banco de dados
storeContato.createIndex("nome", "nome", { unique: false });
}
Transações no IndexedDB
Qualquer interação no banco de dados é necessário abrir uma transação. Para abrir uma transação você precisa informar qual object store e qual o tipo de transação. Existem dois tipos de transação: readonly para ler dados e readwrite para ler e escrever dados. Por padrão, quando nenhum tipo é informado, a transação é aberta com o tipo readonly. As transações possuem o evento oncomplete onde você pode executar alguma ação quando a transação terminar e o onerror onde você pode efetuar o tratamento de erros.
Veja abaixo um exemplo de criar transações.
// Abrindo uma transação para ler/inserir/atualizar/excluir dados
var transaction = db.transaction('contato', "readwrite");
// Quando a transação é executada com sucesso
transaction.oncomplete = function(event) {
};
// Quando ocorre algum erro na tansação
transaction.onerror = function(event) {
};
Incluindo dados no IndexedDB
O código abaixo é um exemplo de como incluir registros no banco de dados.
//Abrindo a transação com a object store "contato"
var transaction = db.transaction('contato', "readwrite");
// Quando a transação é executada com sucesso
transaction.oncomplete = function(event) {
console.log('Transação finalizada com sucesso.');
};
// Quando ocorre algum erro na transação
transaction.onerror = function(event) {
console.log('Transação finalizada com erro. Erro: ' + event.target.error);
};
//Recuperando a object store para incluir os registros
var store = transaction.objectStore('contato');
//contatos para serem adicionados
var contatos = [
{ nome: 'Fulano', cpf: '111.111.111-11', email: 'fulano@email.com'},
{ nome: 'Ciclano', cpf: '222.222.222-22', email: 'ciclano@email.com'},
{ nome: 'Beltrano', cpf: '333.333.333-33', email: 'beltrano@email.com'}
];
for (var i = 0; i < contatos.length; i++) {
//incluindo o registro na object store
var request = store.add(contatos[i]);
//quando ocorrer um erro ao adicionar o registro
request.onerror = function (event) {
console.log('Ocorreu um erro ao salvar o contato.');
}
//quando o registro for incluido com sucesso
request.onsuccess = function (event) {
console.log('Contato salvo com sucesso.');
}
}
Atualizando dados no IndexedDB
O código abaixo é um exemplo de como atualizar os registros no banco de dados.
//Abrindo a transação com a object store "contato"
var transaction = db.transaction('contato', "readwrite");
// Quando a transação é executada com sucesso
transaction.oncomplete = function(event) {
console.log('Transação finalizada com sucesso.');
};
// Quando ocorre algum erro na transação
transaction.onerror = function(event) {
console.log('Transação finalizada com erro. Erro: ' + event.target.error);
};
//Recuperando a object store para alterar o registro
var store = transaction.objectStore('contato');
//Recuperando um contato pela chave primaria
var request = store.get(1);
//quando ocorrer um erro ao buscar o registro
request.onerror = function (event) {
console.log('Ocorreu um erro ao buscar o contato.');
};
//quando o registro for encontrado com sucesso
request.onsuccess = function (event) {
var contato = event.target.result;
contato.nome = 'Fulano de tal';
//Atualizando o registro no banco
var requestUpdate = store.put(contato);
//quando ocorrer erro ao atualizar o registro
requestUpdate.onerror = function (event) {
console.log('Ocorreu um erro ao salvar o contato.');
};
//quando o registro for atualizado com sucesso
requestUpdate.onsuccess = function (event) {
console.log('Contato salvo com sucesso.');
};
};
Excluindo dados no IndexedDB
O código abaixo é um exemplo de como excluir registros do banco de dados.
//Abrindo a transação com a object store "contato"
var transaction = db.transaction('contato', "readwrite");
// Quando a transação é executada com sucesso
transaction.oncomplete = function(event) {
console.log('Transação finalizada com sucesso.');
};
// Quando ocorre algum erro na transação
transaction.onerror = function(event) {
console.log('Transação finalizada com erro. Erro: ' + event.target.error);
};
//Recuperando a object store para excluir o registro
var store = transaction.objectStore('contato');
//Excluindo o registro pela chave primaria
var request = store.delete(3);
//quando ocorrer um erro ao excluir o registro
request.onerror = function (event) {
console.log('Ocorreu um erro ao excluir o contato.');
}
//quando o registro for excluído com sucesso
request.onsuccess = function (event) {
console.log('Contato excluído com sucesso.');
}
Buscando dados no IndexedDB
O código abaixo é um exemplo de como buscar por chave primaria.
var transaction = db.transaction('contato', "readonly");
var store = transaction.objectStore('contato');
var request = store.get(4);
request.onsuccess = function (event) {
console.log(event.target.result);
}
O código abaixo é um exemplo de como buscar vários registros com um cursor.
var transaction = db.transaction('contato', "readonly");
var store = transaction.objectStore('contato');
var request = store.openCursor();
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue();
}
}
Para filtrar registros é necessário utiliza um index. Também é possível limitar a quantidade de registros utilizando os limitadores do IndexedDB. Veja abaixo a tabela de limitadores.
Range | Código |
---|---|
<= x | IDBKeyRange.upperBound(x) |
< x | IDBKeyRange.upperBound(x, true) |
>= x | IDBKeyRange.lowerBound(x) |
> x | IDBKeyRange.lowerBound(x, true) |
>= x && <= y | IDBKeyRange.bound(x, y) |
> x && < y | IDBKeyRange.bound(x, y, true, true) |
> x && <= y | IDBKeyRange.bound(x, y, true, false) |
>= x && < y | IDBKeyRange.bound(x, y, false, true) |
= x | IDBKeyRange.only(x) |
O código abaixo é um exemplo de como filtrar os registros com um index.
var transaction = db.transaction('contato', "readonly");
var store = transaction.objectStore('contato');
var index = store.index("idade");
//filtra os contatos que tenham o idade maior ou igual a 20 e menor ou igual a 25
var filtro = IDBKeyRange.bound(20, 25);
var request = index.openCursor(filtro);
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue();
}
}
Para filtrar os registros por mais de uma coluna é necessário criar um index com mais de uma coluna.
storeContato.createIndex("nome_idade", ['nome', 'idade'], { unique: false });
O código abaixo é um exemplo de como filtrar com mais de uma coluna.
var transaction = db.transaction('contato', "readonly");
var store = transaction.objectStore('contato');
var index = store.index("nome_idade");
//filtra os contatos que tenham o nome igual a 'Fulano' e a idade igual a 20
//Em SQL seria esse where: where nome = 'Fulano' and idade = 20
var filtro = IDBKeyRange.only(['Fulano', 20]);
var request = index.openCursor(filtro);
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue();
}
}
Para ordenar os registros em ordem crescente ou decrescente é incluir o segundo parâmetro do método openCursor. Para ordenar em ordem crescente utilizar o valor “next” e em ordem decrescente o valor “prev”. Por padrão, os registros são ordenados em ordem crescente.
O código abaixo é um exemplo de como ordenar os registros.
var transaction = db.transaction('contato', "readonly");
var store = transaction.objectStore('contato');
var index = store.index("nome");
//ordenando em ordem decrescente de nome
var request = index.openCursor(null, 'prev');
request.onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue();
}
}
Conclusão
O IndexedDB é uma API bem simples e bem fácil de trabalhar, a curva de aprendizado é pequena e é possível utiliza-lo em aplicativos web e em aplicativos mobile híbridos.