Persistência de dados no Android com SQLite

Em muitos aplicativos no Android, existe a necessidade de salvar certos dados estruturados como o progresso do usuário em uma determinada ação quando o aplicativo é pausado.

Neste post, irei abordar o uso do banco de dados no Android.

O SQLite

O é uma biblioteca que fornece um banco de dados relacional e leve em termos de configuração, administração e espaço. Além de não ter dependências externas e ser multiplataforma, o armazena todo o banco de dados em um único arquivo. Isto o torna perfeito para ser usado em dispositivos como um celular.

Não vou falar sobre os detalhes do SQLite, então recomendo a leitura da documentação oficial.

SQLite no Android

O framework do Android disponibiliza um conjunto de APIs prontas para realizarmos o acesso a um banco de dados SQLite para nossos aplicativos. Estas APIs se encontram no pacote .

O principal utilitário que iremos usar é o , que é encarregado de abrir, criar ou atualizar o banco de dados caso necessário.

Criando o DbHelper

Como a classe é uma classe abstrata, devemos herdá-la para poder usar todos os recursos que ela disponibiliza.

Com esta herança, devemos implementar dois métodos: e .

Nossa classe que herda será chamada de DbHelper. Para o construtor, devemos informar o , o nome do banco de dados e a versão.

public class DbHelper extends SQLiteOpenHelper {

public DbHelper(Context context) {
super(context, "vendas.db", null, 1);
}

@Override
public void onCreate(SQLiteDatabase db) {
// Implementar a criação das tabelas
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Implementar a atualização do banco baseado nas versões (oldVersion e newVersion)
}
}

A inicialização do DbHelper se resume a um simples new.

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

DbHelper dbHelper = new DbHelper(this);
}
}

Meios de acesso

Existem duas formas de acessar nosso banco de dados: obter uma instância de leitura ou uma instância de escrita. Conseguimos isso através dos métodos e respectivamente.

Usando o , obtemos uma instâncida de para realizarmos operações de leitura. Já o , obtemos uma instância de para operações de escrita.

Implementando o método onCreate()

Ao obtermos uma instância de , o cuida de criar o banco caso este não exista. Assim o método é executado para criar as tabelas do banco.

Sobrescrevendo o método , recebemos uma instância de por parâmetro para executarmos as ações desejadas. Lembrando que esta instância de é uma instância que permite escrita.

No caso iremos usar apenas o , que nos permite executar um comando SQL passado por parâmetro.

Como no nome do meu banco de dados deste exemplo é vendas.db, irei criar uma tabela chamada clientes.

@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE clientes (" +
"_ID INT PRIMARY KEY," +
"nome TEXT NOT NULL," +
"cpf TEXT NOT NULL);");
}

Então quando o banco de dados for criado pela primeira vez teremos nossa tabela clientes criada também.

Inserindo dados

Como já citado anteriormente, usamos o método para executarmos operações de escrita no banco.

DbHelper dbHelper = new DbHelper(this);

SQLiteDatabase db = dbHelper.getWritableDatabase();

O método adequado para inserção de dados é o da classe .

Duas informações são necessárias para o método : a tabela e os valores das colunas. A tabela é uma simples e os valores são passados através de um .

A classe armazena os dados no formato chave-valor, que seria equivalente a coluna-valor.

DbHelper dbHelper = new DbHelper(this);

SQLiteDatabase db = dbHelper.getWritableDatabase();

ContentValues novoCliente = new ContentValues();
novoCliente.put("nome", "João Vitor");
novoCliente.put("cpf", "111.222.333-44");

db.insert("clientes", null, novoCliente);

O método retorna um long que representa o id do novo registro, ou -1 caso ocorra um problema durante a inserção.

Atualizando dados

O método é bem parecido com o método , a diferença é que podemos definir a condição de atualização, através de dois parâmetros: whereClause e whereArgs.

Para o whereClause definimos os campos que fazem parte da condição, como o whereClause é uma e não um array, devemos definir os campos da seguinte maneira: "campo1 = ? AND campo2 = ?". Exatamente como uma cláusula WHERE do SQL, porém ao invés de informar os valores, informamos interrogações.

Estas interrogações serão substituidas pelos valores do parâmetro whereArgs.

DbHelper dbHelper = new DbHelper(this);

SQLiteDatabase db = dbHelper.getWritableDatabase();

ContentValues novosValores = new ContentValues();
novoCliente.put("nome", "João");

db.update("clientes", novosValores, "cpf = ?", new String[]{"111.222.333-44"});

Esta técnica previne possíveis ataques de .

Removendo dados

Parecido com os métodos anteriores, no método devemos informar apenas o nome da tabela, a condição e seus valores.

DbHelper dbHelper = new DbHelper(this);

SQLiteDatabase db = dbHelper.getWritableDatabase();

db.delete("clientes", "nome = ?", new String[]{"João"});

Consultando dados

O método é o mais indicado para recuperar dados do banco.

Com ele podemos informar a tabela, as colunas, a condição (WHERE), agrupamento (GROUP BY), condição pós-agrupamento (HAVING), ordenação (ORDER BY) e até definir limites (LIMIT).

Apesar de ter vários parâmetros, o mínimo necessário para o método funcionar, é a tabela e as colunas, as outras informações são opcionais e podemos informar null.

O retorno deste método é uma instância da classe . Esta classe representa o resultado de uma consulta.

DbHelper dbHelper = new DbHelper(this);

SQLiteDatabase db = dbHelper.getReadableDatabase();

String[] colunas = new String[]{"nome", "cpf"};

Cursor cursor = db.query("clientes", colunas, null, null, null, null, "nome");

A classe disponibiliza métodos de navegação como , , e . Todos estes métodos retornam um , assim podemos saber se houve sucesso ou não.

Também podemos usar métodos auxiliares como , , , , e .

Para acessar um dado da consulta, usamos o índice (iniciado em zero) da coluna. Um facilitador é o onde este método retorna o índice do nome da coluna passada por parâmetro.

DbHelper dbHelper = new DbHelper(this);

SQLiteDatabase db = dbHelper.getReadableDatabase();

String[] colunas = new String[]{"nome", "cpf"};

Cursor cursor = db.query("clientes", colunas, null, null, null, null, "nome");

if (cursor.moveToFirst()) {
final int INDICE_NOME = 0;
final int INDICE_CPF = 1;

String nomeCliente;
String cpfCliente;

do {
nomeCliente = cursor.getString(INDICE_NOME);
cpfCliente = cursor.getString(INDICE_CPF);

// Aqui você já em os valores desejados e pode executar
// qualquer coisa que precisar com os valores
} while (cursor.moveToNext());
}

Dicas

Cuidado com o getColumnIndex() dentro de loops

O método é bem conveniente, informamos o nome da coluna que desejamos e recebemos o índice dela. Mas isso pode ser perigoso dentro de um loop.

if (cursor.moveToFirst()) {
String nomeCliente;
String cpfCliente;

do {
nomeCliente = cursor.getString(cursor.getColumnIndex("nome"));
cpfCliente = cursor.getString(cursor.getColumnIndex("cpf"));
} while (cursor.moveToNext());
}

A execução de é necessário apenas uma vez: antes do loop.

if (cursor.moveToFirst()) {
int indiceNome = cursor.getColumnIndex("nome");
int indiceCpf = cursor.getColumnIndex("cpf");

String nomeCliente;
String cpfCliente;

do {
nomeCliente = cursor.getString(indiceNome);
cpfCliente = cursor.getString(indiceCpf);
} while (cursor.moveToNext());
}

Muito menos processamento e mais performático, não?

Acesse uma coluna pelo índice quando possível

Na maioria das consultas através do método as colunas são fixas, ou seja, não dependem de alguma condição para aparecer ou não.

Nestes casos o uso do é desnecessário e só aumenta o processamento.

Acesse as colunas diretamente pelo seu índice.

if (cursor.moveToFirst()) {
final int INDICE_NOME = 0;
final int INDICE_CPF = 1;

String nomeCliente;
String cpfCliente;

do {
nomeCliente = cursor.getString(INDICE_NOME);
cpfCliente = cursor.getString(INDICE_CPF);
} while (cursor.moveToNext());
}

Os métodos getWritableDatabase() e getReadableDatabase() podem ser perigosos

Devido ao fato do se encarregar de criar ou atualizar o banco de dados para nós, executar o método ou pode demorar um pouco dependendo da quantidade de operações que você realiza para criação ou atualização do banco de dados.

Portanto recomendo mover as chamadas destes métodos para uma thread diferente da thread principal.

Para operações em massa, o beginTransaction() te ajudará

Quando temos muito INSERT e desejamos que todos sejam inseridos corretamente, podemos iniciar uma transação para esta operação.

O método que inicia uma transação é o , e ao final de qualquer transação (com sucesso ou não) devemos executar o método .

db.beginTransaction();
try {
for (int i = 0; i < dadosClientes.length; i++)
db.insert("clientes", null, dadosClientes[i]);

db.setTransactionSuccessful();
} finally {
db.endTransaction();
}

O sucesso da operação é marcado pelo método .

Quando executamos sem executar , é entendido que a transação não foi bem sucedida, portando as alteraçõe são revertidas.

Singleton ao resgate

Provavelmente instanciaremos o DbHelper várias vezes em nossas aplicações, e para evitarmos despejos de memória e alocações desnecessárias, podemos criar apenas uma instância do DbHelper e usar a mesma instância por toda a aplicação usando o padrão .

public class DbHelper extends SQLiteOpenHelper {

private static DbHelper instance;

private DbHelper(Context context) {
super(context, "vendas.db", null, 1);
}

public static synchronized DbHelper getInstance(Context context) {
if (instance == null) {
instance = new DbHelper(context.getApplicationContext());
}
return instance;
}

@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE clientes (" +
"_ID INT PRIMARY KEY," +
"nome TEXT NOT NULL," +
"cpf TEXT NOT NULL);");
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}

Ao invés de usarmos o new, usaremos o método getInstance().

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

DbHelper dbHelper = DbHelper.getInstance(this);
}
}

Conclusão

Neste post, abordei apenas o necessário, mesmo não existindo conteúdo muito avançado sobre o .

Como estudo complementar, recomendo ver o método .

Até mais.