segunda-feira, 25 de junho de 2012

SQLite no iOS

Olá povo,

Nesse post vou mostrar como utilizar o SQLite em aplicações iOS através da criação de um cadastro simples. Vou utilizar também o recurso de Storyboards (veja esse post) para agilizar a criação das telas.
Comece criando um projeto no Xcode do tipo "Single View Application" que vou nomeá-lo de ExemploSQLiteBlog. Lembre-se de deixar habilitadas as opções Use storyboards e Use automatic reference counting. Após criar o projeto, devemos adicionar a biblioteca do SQLite ao nosso projeto. Para tal, clique sobre o projeto, depois selecione a aba Build phases. Na seção Link Binary With Libraries clique no botão "+" e selecione libsqlite3.dylib.
Uma vez que o projeto está configurado, vamos fazer toda a parte de "negócio" da aplicação, onde criaremos a classe de dados da aplicação, assim como a classe que fará a persistência dos dados da aplicação.
Crie uma nova classe chamada NGContact conforme abaixo (a utilização de prefixos no nome da classe em Objective-C é comum para evitar duplicidade):
// NGContact.h ----------------------
#import <Foundation/Foundation.h>

@interface NGContact : NSObject

@property (assign) NSInteger _id;
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *address;
@property (strong, nonatomic) NSString *phone;

- (id)initWithName:(NSString *)name 
  andAddress:(NSString *)address 
  andPhone:(NSString *)phone;

@end

// NGContact.m ----------------------
#import "NGContact.h"

@implementation NGContact

@synthesize _id = __id, name = _name, 
  address = _address, phone = _phone;

- (id)initWithName:(NSString *)name 
  andAddress:(NSString *)address 
  andPhone:(NSString *)phone {

  self = [super init];
  if (self){
    self.name = name;
    self.phone = phone;
    self.address = address;
  }
  return self;
}

@end
Aqui temos uma classe simples com 4 propriedades (id, nome, endereço e telefone) e um método para inicializar esses atributos. Para quem é "do Java" (como eu) esse método servirá como um construtor (lembrando que Objective-C não tem construtores).
Vamos agora criar a classe que fará a persistência desses objetos e utilizará a API do SQLite. Ela servirá de repositório da aplicação e a chamaremos de NGContactDB, e o arquivo de cabeçalho (.h) deve ficar conforme abaixo.
// NGContactDB.h
#import <Foundation/Foundation.h>
#import <sqlite3.h>

@class NGContact;

@interface NGContactDB : NSObject {
  sqlite3 *contactDB;
  NSString *databasePath;
}

- (void)createDatabase;
- (void)insertContact:(NGContact *)contact;
- (void)updateContact:(NGContact *)contact;
- (void)deleteContact:(NGContact *)contact;
- (NSArray *)getAllContacts;

@end
Como podemos observar, essa classe contém dois atributos, o primeiro é uma referência para sqlite3, e o segundo é o caminho do arquivo do banco. Além disso, temos também cinco métodos:
- createDatabase, que como o próprio nome diz, criará o banco caso ele não exista;
- insertContact, updateContact e deleteContact irão respectivamente inserir, atualizar e excluir um contato no banco;
- e getAllContacts irá obter todos os contatos cadastrados no banco.
Deixe o arquivo de implementação conforme abaixo:
#import "NGContactDB.h"
#import "NGContact.h"
#import <sqlite3.h>

@implementation NGContactDB

// Inicializa o banco -------------------------------
- (void)initDB {   
  // Obtém os diretório de arquivos da aplicação
  NSArray *dirPaths = 
    NSSearchPathForDirectoriesInDomains(
      NSDocumentDirectory, NSUserDomainMask, YES);
  // Obtém o diretório para salvar o arquivo do banco
  NSString *docsDir = [dirPaths objectAtIndex:0];
  // Cria o caminho do arquivo do banco
  databasePath = [[NSString alloc] initWithString: 
    [docsDir stringByAppendingPathComponent: 
      @"contacts.db"]];
  
  const char *dbpath = [databasePath UTF8String];
  // Inicializa o atributo contactDB com o banco  
  if (sqlite3_open(dbpath, &contactDB) != SQLITE_OK){
    NSLog(@"Failed to open/create database - 1");
  }
}

// Cria a tabela no banco ---------------------------
- (void)createDatabase {   
  // Chama o método acima...
  [self initDB];
    
  char *errMsg;
  const char *sql_stmt = "CREATE TABLE IF NOT EXISTS 
    CONTACTS (ID INTEGER PRIMARY KEY AUTOINCREMENT, 
    NAME TEXT, ADDRESS TEXT, PHONE TEXT)";

  // Cria a tabela no banco se ela não existir          
  if (sqlite3_exec(contactDB, sql_stmt, NULL, NULL, 
    &errMsg) == SQLITE_OK) {
    NSLog(@"Database successfully created");
  } else {
    NSLog(@"Failed to create table");
  }
            
  sqlite3_close(contactDB);
}

// Inserir um contato no banco ---------------------
- (void)insertContact:(NGContact *)contact {
  [self initDB];
      
  char *sql = "INSERT INTO CONTACTS (NAME,ADDRESS, 
    PHONE) VALUES (?, ?, ?);";

  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(contactDB, sql, -1, 
    &stmt, nil) == SQLITE_OK) {

    sqlite3_bind_text(stmt, 1, 
      [contact.name UTF8String], -1, NULL);
    sqlite3_bind_text(stmt, 2, 
      [contact.address UTF8String], -1, NULL); 
    sqlite3_bind_text(stmt, 3, 
      [contact.phone UTF8String], -1, NULL); 
  }
  if (sqlite3_step(stmt) == SQLITE_DONE){
    NSLog(@"Record added");
  } else {
    NSLog(@"Failed to add contact");
  }
  sqlite3_finalize(stmt);
  sqlite3_close(contactDB);
}

// Atualizar contato no banco ----------------------
- (void)updateContact:(NGContact *)contact {
  [self initDB];
      
  char *sql = "UPDATE CONTACTS SET NAME = ?, 
    ADDRESS = ?, PHONE = ? WHERE ID = ?;";

  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(contactDB, sql, -1, 
    &stmt, nil) == SQLITE_OK) {
    sqlite3_bind_text(stmt, 1, 
      [contact.name UTF8String], -1, NULL);
    sqlite3_bind_text(stmt, 2,
      [contact.address UTF8String], -1, NULL); 
    sqlite3_bind_text(stmt, 3, 
      [contact.phone UTF8String], -1, NULL); 
    sqlite3_bind_int(stmt, 4, contact._id);
  }

  if (sqlite3_step(stmt) == SQLITE_DONE){
    NSLog(@"Record updated");
  } else {
    NSLog(@"Failed to update contact");
  }
  sqlite3_finalize(stmt);
  sqlite3_close(contactDB);
}

// Excluir contato ---------------------------------
- (void)deleteContact:(NGContact *)contact {
  [self initDB];
      
  char *sql = "DELETE FROM CONTACTS WHERE ID = ?;";
  sqlite3_stmt *stmt;
  if (sqlite3_prepare_v2(contactDB, sql, -1, 
    &stmt, nil) == SQLITE_OK) {

    sqlite3_bind_int(stmt, 1, contact._id); 
  }
  if (sqlite3_step(stmt) == SQLITE_DONE){
    NSLog(@"Record removed");
  } else {
    NSLog(@"Failed to removed contact");
  }
  sqlite3_finalize(stmt);
  sqlite3_close(contactDB);
}

// Obter lista de objetos do banco -----------------
- (NSArray *)getAllContacts;
{
  [self initDB];
    
  NSMutableArray *contacts = 
    [[NSMutableArray alloc]init];
    
  sqlite3_stmt *statement;
    
  NSString *querySQL = [NSString stringWithFormat: 
    @"SELECT * FROM contacts"];
        
  const char *query_stmt = [querySQL UTF8String];
        
  if (sqlite3_prepare_v2(contactDB, query_stmt, -1, 
    &statement, NULL) == SQLITE_OK){

    while (sqlite3_step(statement) == SQLITE_ROW)
    {
      NSInteger contactId = 
        sqlite3_column_int(statement, 0);
               
      NSString *nameField = [[NSString alloc] 
        initWithUTF8String:(const char *) 
          sqlite3_column_text(statement, 1)];
                
      NSString *addressField = [[NSString alloc] 
        initWithUTF8String:(const char *) 
          sqlite3_column_text(statement, 2)];
                
      NSString *phoneField = [[NSString alloc] 
        initWithUTF8String:(const char *) 
          sqlite3_column_text(statement, 3)];
                
      NGContact *contact = [[NGContact alloc]init];
      contact._id = contactId;
      contact.name = nameField;
      contact.address = addressField;
      contact.phone = phoneField;
                
      [contacts addObject:contact];
    }
    sqlite3_finalize(statement);
  }
  sqlite3_close(contactDB);
    
  return [NSArray arrayWithArray:contacts];
}

@end
O código está comentado, mas vou fazer algumas observações. O método initDB começa tentando obter o caminho do diretório de documentos, na qual a aplicação pode salvar seus arquivos. Em seguida monta o caminho do arquivo de banco de dados com a variável databasePath. Por fim, utilizamos a função sqlite3_open (isso mesmo, função, já que o sqlite é escrito em C) para abrir o arquivo de banco de dados e inicializar o atributo contactDB.
Já o método createDatabase criará a tabela Contatcts no banco de dados caso ela não exista. Esse método deve ser chamado apenas uma vez durante o ciclo de vida da aplicação. Pois, uma vez criada, a tabela permanecerá no banco. Esse método começa chamado o initDB para inicializar o atributo contactDB depois monta o SQL para criação da tabela. Note que esse SQL é montado com um char*, que é o tipo esperado pela biblioteca (e não NSString).  Para executar o comando no banco, utilizamos a função sqlite3_exec, passando a referência para o banco e a sentença SQL (para os demais parâmetros olhe aqui).
Os métodos insertContact, updateContact e deleteContact são bem similares. Chamamos o método initDB para inicializar o atributo contactDB. Depois declaramos um sqlite3_stmt que é inicializado através da função sqlite3_prepare_v2 que recebe a referência do banco, a instrução SQL e a referência para o statement. Cada uma das "?" será um parâmetro que será preenchida com um valor através da função sqlite3_bind_* (onde * é o tipo do parâmetro que se quer passar). Para executar a instrução SQL, utilizamos a função sqlite3_step, se tudo correr bem, será retornado SQLITE_DONE. Por fim, fechamos o statement e o banco com as funções sqlite3_finalizesqlite3_close respectivamente.
O método que retorna uma lista de objetos NGContact utiliza os mesmos conceitos dos métodos anteriores, a diferença é que para percorrer os registros retornados, a função sqlite3_step retorna SQLITE_ROW quando temos uma linha para ler. E para ler essa coluna, utilizamos as funções sqlite3_column_* (onde * é o tipo da coluna).

Bem pessoal. Da parte de SQLite é isso, em um próximo post vou mostrar como integrar essa classe com uma UITableViewController (para mostrar os dados) e criar uma tela para inserir e alterar os registros. Mas quem quiser ir se adiantando, pode usar esse post aqui.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

Um comentário:

Leandro Sampaio disse...

Parabéns muito bom seu post.
Fiz igual ta no post mas não funcionou sempre que executo da a mensagem "Failed to open/create database - 1" e quando vou criar a tabela aparece a mensagem "Failed to create table"