Implementando um banco de dados no iOS 4 do iPhone usando SQLite

Nesse artigo, veremos como implementar, para aplicações iOS do iPhone, persistência de dados através de um banco de dados SQLite, que é de longe uma das formas mais eficientes de armazenar dados no iPhone. Para atender essa necessidade, o SDK do iOS inclui tudo que é necessário para integrar banco de dados baseados em SQLite em aplicações iPhone. O objetivo desse capitulo, portante, é fornecer uma visão geral de como usar o SQLite para executar operações básicas na sua aplicação.

O que é SQLite?

O SQLite é um sistema de gerenciamento de banco de dados relacional embutido. Muitos banco de dados relacionais (Oracle e MySql são os principais exemplos) são executados como processos independentes, em cooperação com as aplicações que necessitam de  acesso ao banco de dados. O SQLite é referenciado como sendo embutido porque é fornecido  na forma de uma biblioteca que é ligada as aplicações. Dessa forma, não existe nenhum servidor independente rodando em segundo plano. Todas as operações com o banco de dados na aplicação são feitas através das funções da biblioteca do SQLite.

O SQLite é escrito na linguagem de programação C e portanto o uso do SQLite no iPhone envolve chamadas diretas a funções C  acesso a estruturas de dados C. Para que sejam ultrapassadas as diferenças entre o Objective-C e o C da biblioteca será necessário, por exemplo, converter todos os objetos NSStrings para o formato UTF8 antes de passa-los como argumentos a essas funções.

Structured Query Language (SQL)

Os dados são acessados em banco de dados SQLite através de uma linguagem de alto nível conhecida como SQL. A SQL é a linguagem padrão usada pela maioria dos sistemas de banco de dados relacionais.  Embora algumas sentenças SQL sejam usadas nesse artigo, uma visão detalhada do SQL está além do escopo do artigo. Existem, porém, muitos recursos na Internet que fornecem uma visão mais aprofundada sobre o SQL.

Testando o SQLite no MacOS X

Para quem não é familiarizado com banco de dados em geral e com o SQLite em particular, ir direto para a criação de uma aplicação para iPhone que use o SQLite pode ser um pouco intimidador. Por sorte, o Mac OS X é entregue com o SQLite pré-instalado, incluindo um ambiente interativo para usar comandos SQL a partir da janela do Terminal. Isso é útil tanto para aprender sobre SQLite e SQL, e também é uma ferramenta importante para identificar problemas com os banco de dados criados por aplicações no simulador do iOS.

Para carregar uma sessão interativa do SQLite, abra uma janela de Terminal em seu sistema, mude para um diretório adequado e execute o seguinte comando:

sqlite3 ./mydatbase.db

SQLite version 3.6.12
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>

No prompt do SQLite, comandos podem ser inseridos para executar tarefas como criação de tabelas e inserção e recuperação de dados. Por exemplo, para criar uma nova tabela em nosso banco de dados com campos para guardar ID, nome, endereço e número de telefone, as seguintes sentenças são necessárias:

create table contacts (id integer primary key autoincrement, name text, address text, phone text);

Note que cada linha da tabela precisa ter uma chave primária que seja única naquela linha. No exemplo acima, designamos o campo ID como chave primária, e o declaramos como sendo um Inteiro e pedimos ao SQLite que automaticamente incremente a numeração sempre que uma nova linha é adicionada. Essa é uma maneira comum de certificar-se que cada linha tenha uma chave primária única. Os campos restantes são declarados como campos de texto.

Para listas as tabelas do banco de dados selecionado no momento, use a sentença .tables:

sqlite> .tables
contacts

Para inserir registros na tabela:

sqlite> insert into contacts (name, address, phone) values ("Bill Smith", "123 main Street, California", "123-555-2323");
sqlite> insert into contacts (name, address, phone) values ("Mike Parks", "10 Upping Street, Idaho", "444-444-1212");

Para recuperar todas as linhas de uma tabela:

sqlite> select * from contacts;
1|Bill Smith|123 main Street, California|123-555-2323
2|Mike Parks|10 Upping Street, Idaho|444-444-1212

Para extrair uma linha que atenda a uma critério específico:

sqlite> select * from contacts where name="Mike Parks";
2|Mike Parks|10 Upping Street, Idaho|444-444-1212
To exit from the sqlite3 interactive environment:
sqlite> .exit

Quando estiver rodando uma aplicação para iPhone no ambiente do simulador, todos os arquivos de bases de dados serão criados no sistema de arquivos do computador onde o simulador estiver rodando. Isso tem a vantagem que você pode navegar até o local do arquivo, carrega-lo na ferramenta interativa sqlite3 e executar tarefas nos dados para identificar possíveis problemas que poderiam ocorrer no código da aplicação. Se, por exemplo, a aplicação cria um arquivo de banco de dados chamado contacts.db em seu diretório, o arquivo estará localizado no sistema na seguinte pasta:

/Users/<user>/Library/Application Support/iPhone Simulator/<sdk version>/Applications/<id>/Documents

Onde <user> é o login do usuário que está rodando a sessão do simulador, <sdk version> é a versão do SDK do iOS usada para compilar a aplicação e <id> é o ID único da aplicação.

Preparando um projeto iOS 4 para integração com o SQLite

Por padrão, o Xcode não assume que você irá incluir o SQLite em sua aplicação. Quando for desenvolver aplicações baseados no SQLite alguns poucos passos adicionais são necessários para garantir que o código seja compilado. Em primeiro lugar, o projeto necessita ser configurado para incluir a biblioteca dinâmica libsqlite3.dylib durante a fase de linkagem. Para conseguir isso, mantenha pressionada a tecla Ctrl e clique com o mouse no item Framework do painel Groups and Files localizada do lado esquerdo da janela do projetdo Xcode. No menu resultante, selecione a opção Add -> Existing Frameworks… Na lista de opções de framework, role para baixo até o item libsqlite3.dylib, selecione-o e clique no botão Add.

Em segundo lugar, o arquivo sqlite3.h precisa ser incluído em qualquer arquivo onde houver alguma referência as definições, declarações ou funções do SQLite. Esse arquivo está localizado no diretório /usr/include e deve ser incluído quando for necessário da forma a seguir:

#import "/usr/include/sqlite3.h"

Funções chave do SQLite

Quando estiver implementando um banco de dados usando o SQLite será necessário utilizar algumas funções do C contidas na biblioteca libsqlite3.dylib. Um resumo das funções mais usadas segue abaixo:

  • sqlite3_open() – Opens specified database file. If the database file does not already exist, it is created.
  • sqlite3_close() – Closes a previously opened database file.
  • sqlite3_prepare_v2() – Prepares a SQL statement ready for execution.
  • sqlite3_step() – Executes a SQL statement previously prepared by the sqlite3_prepare_v2() function.
  • sqlite3_column_<type>() – Returns a data field from the results of a SQL retrieval operation where <type> is replaced by the data type of the data to be extracted (text, blob, bytes, int, int16 etc).
  • sqlite3_finalize() – Deletes a previously prepared SQL statement from memory.
  • sqlite3_exec() – Combines the functionality of sqlite3_prepare_v2(), sqlite3_step() and sqlite3_finalize() into a single function call.

Isso, naturalmente, representa apenas um sub-conjunto pequeno dentre todas as funções disponíveis para o SQLite. Uma lista completa pode ser encontrada em http://www.sqlite.org/c3ref/funclist.html.

Declarando um banco de dados SQLite

Antes que qualquer tarefa possa ser executada no banco de dados, ele precisa ser declarado. Para fazer isso, é necessário declarar uma variável que aponte para uma instância da estrutura do tipo sqlite3 (*essa estrutura é definida no arquivo sqlite3.h). Por exemplo:

sqlite3 *contactDB; //Declare a pointer to sqlite database structure

Abrindo ou criando um banco de dados

Uma vez declarado, um arquivo de banco de dados pode ser aberto usando a função sqlite3_open(). Se o arquivo especificado não existir ele será criado antes de ser aberto. A sintaxe dessa função segue abaixo:

int sqlite3_open(const char *filename, sqlite3 **database);

Na sintaxe acima, filename é o caminho para o arquivo na forma de uma string de caracteres no formato UTF-8 e database é uma referência para a estrutura do banco de dados. O resultado da operação é retornado como um inteiro. Várias definições para valores de resultado são definidas no arquivo include como SQLITE_OK para indicar o sucesso da operação.

For example, the code to open a database file named contacts.db in the Documents directory of an iPhone application might read as follows. Note that the code assumes an NSString variable named databasePath contains the path to the database file:

sqlite3 *contactDB; //Declare a pointer to sqlite database structure

const char *dbpath = [databasePath UTF8String]; // Convert NSString to UTF-8

if (sqlite3_open(dbpath, &contactDB) == SQLITE_OK)
{
	//Database opened successfully
} else {
	//Failed to open database
}

O ponto chave a notar no exemplo acima é que a string contida no objeto NSString tem que ser convertida para o formato UTF-8 antes de ser passado para a função que abre o banco de dados. Essa pode ser uma atividade comum que você verá que é executada com frequência quando estiver trabalhando com SQLite e Objective-C.

Preparando e executando uma sentença SQL

As sentenças SQL são preparada e armazenadas em uma estrutura do tipo sqlite3_smt usando a função sqlite3_prepare_v2(). Por exemplo:

sqlite3_stmt *statement;

NSString *querySQL = @"SELECT address, phone FROM contacts”;

const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
	//Statement prepared successfully
} else {
	//Statement preparation failed
}

Uma sentença SQL preparada pode ser executada usando uma chamada a função sqlite3_step(), com a variável sqlite3_stmt sendo passada como argumento:

sqlite3_step(statement);
sqlite3_finalize(statement);

Note que a função sqlite3_step() também retorna um valor. O valor retornado, porém, dependerá da natureza da sentença sendo executada. Por exemplo, uma inserção de dados em uma tabela resultará em um SQLITE_OK, enquanto a recuperação de dados retornará um SQLITE_ROW.

Alternativamente, os mesmo resultados podem ser conseguidos com o uso da função sqlite_exec() como ilustrado na seção a seguir.

Criando uma tabela no banco de dados

Os dados do banco de dados são organizados em tabelas. Antes que os dados possam ser inseridos no banco de dados, uma tabela precisa ser criada. Isso é feito mediante o uso da sentença SQL CREATE TABLE. O exemplo de código abaixo ilustra a criação de uma tabela chamado contacts, a preparação e execução da sentença é feita usando a função sqlite3_exec():

const char *sql_stmt = "CREATE TABLE IF NOT EXISTS CONTACTS (ID INTEGER PRIMARY KEY AUTOINCREMENT, NAME TEXT, ADDRESS TEXT, PHONE TEXT)";

if (sqlite3_exec(contactDB, sql_stmt, NULL, NULL, &errMsg) == SQLITE_OK)
{
          // SQL statement execution succeeded
}

Extraindo dados da tabela do banco de dados

Aqueles que são familiarizados com SQL saberão que os dados são lidos do banco através do uso de sentenças SELECT. Dependendo do critério definido na sentença, é comum que mais de um dados seja retornado. É importante, por isso, aprender como recuperar os dados do banco de dados usando as chamadas de função SQL em C.

Como nos exemplos anteriores, as sentença SQL precisa ser preparada primeiro. No código a seguir, uma sentença SQL para extrair o endereço e telefone de todas as linhas de uma tabela chamada contacts é preparada:

sqlite3_stmt    *statement;

NSString *querySQL = @"SELECT address, phone FROM contacts”;

const char *query_stmt = [querySQL UTF8String];

sqlite3_prepare_v2(contactDB, query_stmt, -1, &statement, NULL);

A sentença precisa ser executada. Se uma linha de dados coincidir com os critérios de seleção for encontrada na tabela, a função sqlite3_step() retorna SQLITE_ROW. O dado dessa linha é armazenado em uma estrutura sqlite3_stmt e pode ser lido usado a função sqlite3_column_<type>(), onde <type> deve ser substituído  pelo tipo do dado que está sendo lido. Através da implementação de um loop while, a função sqlite3_loop() pode ser chamada repetidamente por múltiplas linhas coincidentes até que todas tenham sido lidas. Note que a função sqlite3_column_<type>() precisa de um segundo argumento que é o número da coluna a ser lida. No caso do nosso exemplo, a coluna 0 contém o endereço e a coluna 1 o telefone.

while (sqlite3_step(statement) == SQLITE_ROW)
{
       NSString *addressField = [[NSString alloc] initWithUTF8String:
                (const char *) sqlite3_column_text(statement, 0)];

       NSString *phoneField = [[NSString alloc] initWithUTF8String:
                (const char *) sqlite3_column_text(statement, 1)];

	// Code to do something with extracted data here

	[phoneField release];
	[addressField release];
}
sqlite3_finalize(statement);

Fechando o banco de dados SQLite

Quando a aplicação tiver finalizado trabalho no banco de dados é importante que ele seja fechado. Isso é feito com uma chamada a função  sqlite3_close(), passando como argumento um ponteiro para o banco de dados a ser fechado: sqlite3_close(contactDB);

Sumário

Nesse artigo, vimos os conceitos básicos da implementação de um banco de dados para uma aplicação iPhone usando o sistema de gerenciamento de banco de dados SQLite.

Traduzido de