Muitas aplicações precisam armazenar dados para que os mesmos estejam disponíveis após o término de sua execução. Além da opção de lidarmos diretamente com arquivos, o Qt provê diversas maneiras de guardar informações offline como por exemplo o QSettings caso se deseje armazenar apenas chaves e valores. Também é possível utilizar o módulo Qt SQL para lidar com dados estruturados de forma mais complexa. No entanto, essas abordagens exigem integração do código QML com o C++, o que as vezes não é desejável. Uma alternativa é utilizar a Offline Storage API (Armazenamento Offline) para não precisar sair do ambiente QML. Essa API provê uma interface SQL, que permite manipular bancos de dados relacionais (BDs), utilizando o SQLite como backend.
Detalhes sobre o offline storage pode ser encontrado na documentação oficial do Qt. Apesar de ser bem escrita, a documentação não nos dá um exemplo prático que ilustre o uso dessa API, portanto, decidí criar um que mostra as boas práticas do uso da mesma.
Lista de Contatos
Imagine que você tem uma lista de contatos para armazenar em sua aplicação. As informações necessárias para armazenar esses contatos são seus nomes, número de telefone e email. O primeiro passo é criar uma conexão com o banco de dados do offline storage,
// A função openDatabaseSync seguintes parametros
// 1 - Nome do banco de dados (BD)
// 2 - Versão do BD
// 3 - Descrição sobre o BD
// 4 - Estimativa de tamanho do BD
// Retorna um objeto do tipo Database
var db = openDatabaseSync("Contacts", "1.0", "Contact list database", 5*1024*1024);
Para manipular um banco de dados no Javascript uma conexão com o banco deve ser criada. A API de offline storage disponibilizada para o QML é assíncrona no que se refere a criação e execução de transações. As transações são criadas através do método transaction do objeto de conexão com o offline storage. Essa função segue o padrão de callbacks do Javascript para lidar com chamadas assíncronas. Assim que a transação está pronta para ser iniciada, ela chama uma função (o callback) passando o objeto que representa a transação como parâmetro. Com a referência do objeto da transação, as consultas e alterações no banco podem ser realizadas.
Para criar as tabelas é preciso abrir uma transação para executar o código SQL referente a sua criação. Isso pode ser feito da seguinte forma:
// Abrindo transação
db.transaction(function (tx) { // tx é um objeto do tipo transação
tx.executeSql("CREATE TABLE IF NOT EXISTS Contacts ( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \
name STRING, \
phone STRING, \
email STRING, \
UNIQUE (name) ON CONFLICT REPLACE)");
});
Abstraíndo o Banco de Dados
Em minha experiência com o offline storage, recomendo que as funções que operam o BD deveriam ficar em um módulo (ou um conjunto de módulos) Javascript a parte. Nesses módulos, além da criação do esquema do banco de dados, recomendo definir funções de acesso aos dados que escondam o SQL da aplicação QML, ao criar funções que manipulem esses dados em formatos amigáveis ao QML:
// O callback é chamado quando o contato é inserido ou atualizado e o id gerado é passado como parâmetro da callback.
function insertOrUpdateContact(name, phone, email, callback) {
db.transaction(function (tx) {
// Note que o ID é auto gerado
var resultSet = tx.executeSql("INSERT OR REPLACE INTO Contact(name, phone, email) VALUES (?, ?, ?)", [name, phone, email]);
callback(resultSet.insertId); // Retorna o id gerado para o novo usuário como parâmetro da callback
});
}
// Retorna a informação sobre um contato cujo id é passado como parâmetro
function getContactInfo(id, callback, errorCallback) {
db.trasaction(function (tx) {
var resultSet = tx.executeSql("SELECT * FROM Contacts WHERE id = ?", [id]);
if (resultSet.rows.length > 0) {
// A função resultSet.rows.item retorna um array JS com chaves
// sendo o nome das colunas e os valores os dados das colunas
callback(resultSet.rows.item(0));
} else {
errorCallback();
}
});
}
Isso nos permite utilizar as informações do BD no QML sem misturar QML com SQL diretamente:
import QtQuick 1.0
import "db.js" as DB
Column {
property int contactId
property string name
property string email
property string phone
width: 300; height: 90
onContactIdChanged: {
// A variável DB referencia o módulo Javascript
DB.getContactInfo(contactId, function (info) {
name = info.name;
email = info.email;
phone = info.phone;
});
}
Text { text: name; height: 30 }
Text { text: email; height: 30 }
Text { text: phone: height: 30 }
}
Migração entre esquemas
Suponha, que sua aplicação já está em uso, e agora você precisa suportar vários telefones para cada contato. Para fazer isso de maneira que o esquema do BD seja atualizado e os dados não sejam perdidos. Isso é importante, em ambientes onde você não tem acesso direto a maquina para executar backups ou algo do gênero, como por exemplo aplicativos publicados na Nokia Store. Para isso, a API de offline storage define uma maneira de executar migrações a partir do versionamento dos esquemas. O objeto do tipo database já provê um método (changeVersion) que nos ajuda a atualizar os metadados do banco e chamar a função responsável pela migração dos esquemas. Essa função será utilizada da seguinte forma:
// Versão vazia significa, pegar a versão mais atual
var db = openDatabaseSync("Contacts", "", "Contact list database", 5*1024*1024);
// Caso não exista a versão será uma string vazia
if (db.version == '') {
db.changeVersion('','1.1', create_scheme_1_1);
} else if (db.version == '1.0') {
db.changeVersion('1.0','1.1', migrate_scheme_1_0_to_1_1);
}
Isso nos permite identificar a versão atual e chamar o procedimento correto para tratar a criação ou migração do esquema, já passando a transação como parâmetro. Da seguinte forma:
function create_scheme_1_1(tx) {
tx.executeSql("CREATE TABLE IF NOT EXISTS Contacts ( \
id INTEGER PRIMARY KEY AUTOINCREMENT, \ // Criando chave primária automática
name STRING, \
email STRING, \
UNIQUE (name) ON CONFLICT REPLACE)"); // Essa restrição impede que dois contatos tenham o mesmo nome
tx.executeSql("CREATE TABLE IF NOT EXISTS ContactsPhones ( \
contact_id INTEGER, \ // Criando chave primária automática
phone STRING, \
FOREIGN KEY (contact_id) REFERENCES Contacts(id))");
}
function migrate_scheme_1_0_to_1_1(tx) {
// Cria tabela auxiliar para armazenar multiplos telefones por usuário
tx.executeSql("CREATE TABLE IF NOT EXISTS ContactsPhones ( \
contact_id INTEGER, \
phone STRING, \
FOREIGN KEY (contact_id) REFERENCES Contacts(id))");
// Move os dados para a nova tabela
tx.executeSql("INSERT INTO ContactsPhones (contact_id, phone) SELECT id, phone FROM Contacts");
}
É importante notar que não é possível remover colunas no SQLite, e que o desenvolvedor terá que tomar a decisão de conviver com uma coluna desenecessária ou criar uma nova tabela com as informação necessárias. No caso do último, é importante ter um pouco mais de conhecimento sobre SQL para fazer renomeação de tabelas e alteração de tabelas.
Por termos um esquema diferente, é importante atualizar as consultas realizadas. Daí a importancia de termos uma API como interface para abstraírmos o código das consultas do restante da aplicação, e termos que alterar as consultas apenas em alguns pontos.
Related posts:
RSS