Como utilizar IndexedDB em aplicativos web

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.

RangeCódigo
<= xIDBKeyRange.upperBound(x)
< xIDBKeyRange.upperBound(x, true)
>= xIDBKeyRange.lowerBound(x)
> xIDBKeyRange.lowerBound(x, true)
>= x && <= yIDBKeyRange.bound(x, y)
> x && < yIDBKeyRange.bound(x, y, true, true)
> x && <= yIDBKeyRange.bound(x, y, true, false)
>= x && < yIDBKeyRange.bound(x, y, false, true)
= xIDBKeyRange.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.

Referências