SQLite é o nono banco de dados mais utilizado do mundo e provavelmente o #1 do segmento móvel, embora essa informação seja mais difícil de mensurar. Mas provavelmente essa minha afirmação é verdade uma vez que SQLite roda em dispositivos Android, em iOS, em Blu-ray players e diversos dispositivos Smart (como as famosas TVs). E sabemos também que existem mais desses dispositivos no mundo do que servidores capazes de rodar Oracles e cia.
Curiosamente, não acho muito conteúdo bom a respeito desse banco de dados na Internet, principalmente em Português, e por isso resolvi criar esse post para ajudar quem está começando a criar seus apps que usam banco de dados local, como exemplificado neste post completão.
E então, preparado para conhecer as melhores dicas de SQLite para seus apps Android?
As dicas que veremos são (clique no link para ver uma específica):
- Boas práticas de conexões SQLite
- Como criar e atualizar banco de dados corretamente no Android
- Como utilizar um banco SQLite já existente
- Tamanho máximo da base SQLite
- Ferramentas ORM para Android/SQLite
- Como paginar consultas no SQLite
- SQLite e os tipos de dados
Mas se o que você quer aprender é como usar o SQLite para aplicações web, recomendo este tutorial.
#1 – Boas práticas de conexões SQLite
Vamos partir do princípio: a conexão com o banco de dados SQLite.
A classe SQLiteOpenHelper que criamos para lidar com as conexões (e criação do banco de dados) nos fornece os métodos getReadableDatabase e getWritableDatabase, conexão de leitura e de escrita com o banco de dados, respectivamente. Se você criar a conexão em modo de leitura, somente SELECTs serão permitidos, mas o desempenho geral do seu banco será melhor (mais rápido).
1 2 3 4 5 |
Context context = getApplicationContext(); DatabaseHelper helper = new DatabaseHelper(context); SQLiteDatabase conexaoEscrita = helper.getWritableDatabase(); |
Até aí tudo bem, isso sempre é explicado nos cursos. Mas você sabia que para cada instância do objeto da classe SQLiteOpenHelper (no exemplo temos sua subclasse DatabaseHelper) temos apenas uma conexão com o banco?
Isso pode lhe gerar alguns problemas se não lidar adequadamente com sua conexão e/ou instâncias de SQLiteOpenHelper, como usá-la de maneira static sem pensar antes ou se estiver operando com multithreading (com processos rodando em background no Android, por exemplo).
“Ah, mas então é só usar várias instâncias de SQLiteOpenHelper e terei múltiplas conexões ao mesmo tempo…”.
Não se você estiver usando conexões em modo de escrita (writable). Nesse caso, se a escrita for ao mesmo tempo, uma delas irá falhar. A dica aqui é: use apenas uma instância de SQLiteOpenHelper (singleton?). Se realmente precisar de várias conexões simultâneas, certifique-se de que somente uma conexão será em modo de escrita e as demais somente leitura. Ponto.
Outras dicas extremamente válidas, para reduzir o tempo de lock na base é usar transações, assim como você faria em bancos tradicionais. Uma última dica ainda é o uso de índices nas colunas usadas como filtro em WHEREs. Nenhuma novidade, não é mesmo? Infelizmente não vejo muitos devs se preocupando com o SQL da mesma forma que se preocupam com um MySQL.
#2 – Como criar e atualizar banco de dados no Android corretamente
No momento em que chamamos um dos dois métodos para obter conexão com o banco de dados é que os métodos onCreate ou onUpgrade da classe SQLiteOpenHelper serão chamados. Até que este momento chegue, o seu banco SQLite não existe ainda no smartphone do usuário.
Quando o banco não existe no dispositivo, o método onCreate é disparado automaticamente na primeira solicitação de conexão, executando os seus comandos SQL de criação de tabelas e salvando no banco a informação do número de versão do mesmo (número esse que é passado no construtor de SQLiteOpenHelper). Caso não aconteça nenhum erro dentro do onCreate, assume-se que o banco foi criado com sucesso.
1 2 3 |
SQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) |
Agora, se o banco já existir no dispositivo e no momento da instanciação de SQLiteOpenHelper for passado um número de versão diferente do existente no banco local, ao invés do método onCreate, o método onUpgrade será chamado automaticamente. Nesse caso, é nesse método que colocamos os comandos SQL para alteração de tabelas e dados, significando que o banco de dados será atualizado.
A dica aqui é: só use o onUpgrade se o app já estiver em produção, com usuários utilizando-o e você não tiver como comunicá-los para reinstalar o app. Isso porque o onUpgrade é extremamente complexo de implementar de maneira correta, uma vez que você terá de escrever uma série de scripts que vão alterar o banco de dados com o mesmo sendo executado, o que é algo extremamente perigoso para a integridade do mesmo. Em ambiente de desenvolvimento, ou de produção controlada (um app para uso pelos funcionários de sua empresa, por exemplo), o ideal é limpar os dados do app nas configurações do aplicativo ou mesmo reinstalá-lo.
Em um caso ainda mais caótico, onde o seu app já tenha passado por diversas versões de banco de dados e seus usuários podem estar “saltando” entre as versões sem passar pelas intermediárias (ex: está na versão 1 mas a versão mais recente é a 3, e ele mandou atualizar agora) a recomendação é que você faça as alterações incrementais a cada versão até a mais recente, usando um switch, testando a versão “antiga” do usuário e fazendo os ajustes SQL necessários para ele avançar cada uma das versões e cada vez (note a falta de breaks nos cases 1 e 2, pois somente na 3 é que iremos parar):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { switch(oldVersion) { case 1: //lógica de atualização da v1 para v2 case 2: //lógica de atualização da v2 para v3 case 3: //lógica de atualização da v3 para v4 break; default: throw new IllegalStateException( "onUpgrade() em oldVersion desconhecida " + oldVersion); } } |
#3 – Como utilizar um banco SQLite já existente
Muitos desenvolvedores preferem modelar (e até mesmo popular) seu banco SQLite em ferramentas externas como SQLite Manager e SQLite Studio e depois ficam na dúvida sobre como importar esse banco pra dentro do app.
Primeiro, salve o seu arquivo do banco dentro da pasta assets no seu app.
Segundo, use o código abaixo como exemplo para criar a sua DbHelper (classe que herda de SQLiteOpenHelper responsável por criar o banco na primeira conexão, lembra?) que carregará o banco já existente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import android.content.Context; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; public class DBHelper extends SQLiteOpenHelper { private static String TAG = "DBHelper"; // LogCat //local da sua base no smartphone private static String DB_PATH = ""; private static String DB_NAME ="MinhaBase";// o nome do seu banco private SQLiteDatabase mDataBase; private final Context mContext; public DBHelper(Context context) { super(context, DB_NAME, null, 1);// 1 = versão da base if(android.os.Build.VERSION.SDK_INT >= 17){ DB_PATH = context.getApplicationInfo().dataDir + "/databases/"; } else { DB_PATH = "/data/data/" + context.getPackageName() + "/databases/"; } this.mContext = context; } public void createDataBase() throws IOException { //se a base não existe, copia da pasta assets. boolean mDataBaseExist = checkDataBase(); if(!mDataBaseExist) { this.getReadableDatabase(); this.close(); try { //copia a base copyDataBase(); Log.e(TAG, "createDatabase base criada"); } catch (IOException mIOException) { throw new Error("Erro copiando a base"); } } } //verifica se a base existe na pasta: /data/data/your package/databases/Da Name private boolean checkDataBase() { File dbFile = new File(DB_PATH + DB_NAME); //Log.v("dbFile", dbFile + " "+ dbFile.exists()); return dbFile.exists(); } //Copia a base da pasta assets private void copyDataBase() throws IOException { InputStream mInput = mContext.getAssets().open(DB_NAME); String outFileName = DB_PATH + DB_NAME; OutputStream mOutput = new FileOutputStream(outFileName); byte[] mBuffer = new byte[1024]; int mLength; while ((mLength = mInput.read(mBuffer))>0) { mOutput.write(mBuffer, 0, mLength); } mOutput.flush(); mOutput.close(); mInput.close(); } //Abra a base de dados pra fazer consulta public boolean openDataBase() throws SQLException { String mPath = DB_PATH + DB_NAME; //Log.v("mPath", mPath); mDataBase = SQLiteDatabase.openDatabase(mPath, null, SQLiteDatabase.CREATE_IF_NECESSARY); //mDataBase = SQLiteDatabase.openDatabase(mPath, null, SQLiteDatabase.NO_LOCALIZED_COLLATORS); return mDataBase != null; } @Override public synchronized void close() { if(mDataBase != null) mDataBase.close(); super.close(); } } |
Para o correto uso do banco já existente, você terá de mandar criar o banco em seu DbAdapter, como no exemplo abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
import java.io.IOException; import android.content.Context; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.util.Log; public class DBAdapter { protected static final String TAG = "DBAdapter"; private final Context mContext; private SQLiteDatabase mDb; private DataBaseHelper mDbHelper; public DBAdapter(Context context) { this.mContext = context; mDbHelper = new DBHelper(mContext); } public DBAdapter createDatabase() throws SQLException { try { mDBHelper.createDataBase(); } catch (IOException mIOException) { Log.e(TAG, mIOException.toString() + " UnableToCreateDatabase"); throw new Error("UnableToCreateDatabase"); } return this; } public DBAdapter open() throws SQLException { try { mDbHelper.openDataBase(); mDbHelper.close(); mDb = mDbHelper.getReadableDatabase(); } catch (SQLException mSQLException) { Log.e(TAG, "open >>"+ mSQLException.toString()); throw mSQLException; } return this; } public void close() { mDbHelper.close(); } public Cursor getTestData() { try { String sql ="SELECT * FROM myTable"; Cursor mCur = mDb.rawQuery(sql, null); if (mCur!=null) { mCur.moveToNext(); } return mCur; } catch (SQLException mSQLException) { Log.e(TAG, "getTestData >>"+ mSQLException.toString()); throw mSQLException; } } } |
Para usar essa classe corretamente, dê uma olhada no exemplo abaixo:
1 2 3 4 5 6 7 8 9 |
DBAdapter mDbHelper = new DBAdapter(urContext); mDbHelper.createDatabase(); mDbHelper.open(); Cursor testdata = mDbHelper.getTestData(); mDbHelper.close(); |
#4 – Tamanho máximo da base SQLite
Em diversos pontos da documentação oficial do SQLite é dito que ele não é indicado para aplicações com uso intensivo de dados ou com grandes quantidades de dados, embora isso soe um tanto subjetivo. Na página sobre limites, muito completa aliás, fala-se que tamanho teórico que o SQLite comporta é 140TB. Sim, terabytes.
Achei muito engraçado quando li isso, pois mesmo SGBDs mais corporativos como SQL Server tem sérios problemas quando chegam na casa das dezenas de GB sem uma arquitetura eficiente, imagina o coitado do SQLite que todo mundo usa de qualquer jeito…A pergunta que não quer calar é:
Na prática, que cuidados eu devo tomar com o tamanho do arquivo do meu banco SQLite?
Procurando por cases de uso intenso de SQLite, consegui achar estes números, que não são lá muito conclusivos, mas que são melhor do que nada:
- usando apenas uma tabela com 8 colunas e 4 índices: o teste deveria inserir 50GB de dados (que é o limite de tamanho por tabela do MySQL, por exemplo), mas após 48h ainda não havia terminado, pois o tempo dos INSERTs estava astronômico (culpa dos índices vs tamanho).
- usando 700 tabelas com 8 colunas e 4 índices cada, foi possível inserir os 50GB de teste sem grandes problemas com os INSERTs, no entanto a fragmentação de disco ficou absurda, dificultando um pouco os SELECTs depois.
Sendo assim, a conclusão que podemos tirar disso é: evite tabelas muito grandes, mas não e preocupe tanto com a quantidade de tabelas.
Onde fica o arquivo do banco de dados?
Antes de encerrar essa dica, muitos alunos me perguntam onde o emulador do Android guarda a base SQLite e como pegar esse arquivo, até mesmo para verificar o tamanho dele. Não é muito difícil de achar (embora pudesse ser mais fácil…), com o seu emulador rodando o app, abra o Android Device Monitor (fica no meu de ferramentas do Android) e use a opção de verificar o sistema de arquivos para ter acesso à estrutura de pastas do emulador, incluindo a pasta data/data.package-name/databases onde o banco fica dentro. Basta usar o ícone do disquete para salvar o arquivo do SQLite no seu PC.
Caso esteja testando o app direto no smartphone, essa dica só funciona se o app estiver rodando como root.
#5 – Ferramentas ORM para Android/SQLite
O uso de ferramentas de mapeamento de banco de dados (ORM – Object-Relational Mapping) são praticamente regra no mercado de trabalho. Afinal, elas tornam o trabalho dos CRUDs (além de outras tarefas) muito mais fáceis uma vez que o programador não precisa se preocupar com SQL, apenas com objetos.
Mas…existe algum ORM que possa ser usado com SQLite? Não seriam essas bibliotecas muito pesadas?
As respostas são: sim e depende, respectivamente.
Para elucidar essa questão, separei abaixo alguns ORM popularmente usados com SQLite (nem pense em colocar um Hibernate dentro do Android ou você vai acabar o com telefone do usuário!):
- ORMLite: documentação com muitos exemplos, tamanho minúsculo.
- GreenDAO ORM: bem famoso e muito utilizado em apps de produção pois consome pouca RAM, pouco disco e tem uma performance muito boa.
- ORMDroid: um ORM minúsculo que provavelmente roda em qualquer smartphone
- ActiveAndroid: equipe de desenvolvimento bastante ativa
- DroidParts: possui injeção de dependência e uma equipe ativa
- AndrORM: sem comentários
- Sprinkles: sem comentários, mas sugerido por um programador experiente
- Realm.io: esse aqui não é apenas um ORM, mas todo um mecanismo de persistência standalone que substitui o SQLite, funcionando inclusive para iOS também.
Tem algo a declarar sobre os ORMs acima? Alguma outra sugestão que eu não lembrei? Deixe nos comentários!
#6 – Como paginar consultas no SQLite
Os cuidados com consultas/queries no SQLite são praticamente os mesmos que em outros bancos de dados mais robustos como SQL Server e MySQL, por exemplo: paginação. Sempre é uma boa ideia retornar somente o número de linhas que você irá usar imediatamente e jamais trazer toda uma tabela principalmente em um dispositivo limitado como um smartphone Android.
Aqui entra em cena a palavra reservada LIMIT, velha conhecida dos usuários de MySQL, mas que no SQL Server seria o equivalente ao TOP. Com a LIMIT nós especificamos o número máximo de elementos que queremos que nossa consulta retorne, da seguinte maneira:
1 2 3 |
SELECT * FROM Table_Name LIMIT 5; |
Neste exemplo, retornaremos apenas os 5 primeiros elementos retornados pela query. Mas e se queremos os 5 elementos após o décimo elemento (ou outra posição arbitrária qualquer)?
Aí usamos o OFFSET, que seria algo como um SKIP, ignorando alguns elementos retornados pela consulta, antes de aplicar o LIMIT:
1 2 3 |
Select * from TableName LIMIT 5 OFFSET 10 |
Um atalho de linguagem (syntax sugar) seria como abaixo, onde apenas a ordem dos parâmetros é invertida:
1 2 3 |
Select * from TableName LIMIT 10, 5 |
Se você nunca teve de paginar dados no SQLite, não sabe como esse comando acima é útil. 🙂
#7 – SQLite e os tipos de dados…
O SQLite possui uma tipagem muito fraca em suas colunas, embora seja existente. Além de possuir poucos tipos de dados, eles podem ser praticamente ignorados em diversas circunstâncias.
No entanto, a falta de dois tipos específicos causa muita confusão em desenvolvedores acostumados com bancos mais tradicionais como SQL Server e MySQL: datas (dates, times e datetimes) e booleans (bits). Logo, vou quebrar essa dica em duas partes:
Datas no SQLite
Se o seu objetivo é apenas armazenar as datas para ser exibida depois (você não vai filtrar por ela, embora seja possível), você pode armazená-la como um texto no formato UTC: yyyy-MM-dd HH:mm:ss, que é o formato default quando você chama a função datetime(‘now’) do SQLite (hmmm, seria essa outra dica interessante?). Salve nesse formato e, mais tarde, quando quiser exibir pro usuário a data no formato que ele conhece, use o método abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public static String formatDateTime(Context context, String timeToFormat) { String finalDateTime = ""; SimpleDateFormat iso8601Format = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss"); Date date = null; if (timeToFormat != null) { try { date = iso8601Format.parse(timeToFormat); } catch (ParseException e) { date = null; } if (date != null) { long when = date.getTime(); int flags = 0; flags |= android.text.format.DateUtils.FORMAT_SHOW_TIME; flags |= android.text.format.DateUtils.FORMAT_SHOW_DATE; flags |= android.text.format.DateUtils.FORMAT_ABBREV_MONTH; flags |= android.text.format.DateUtils.FORMAT_SHOW_YEAR; finalDateTime = android.text.format.DateUtils.formatDateTime(context, when + TimeZone.getDefault().getOffset(when), flags); } } return finalDateTime; } |
Agora, se você quer poder pesquisar a data e até mesmo ter uma performance boa com o seu armazenamento e retorno, eu sugiro transformar a sua data em um número inteiro e armazená-la assim. Não é nada prático, principalmente se você gosta de fazer consultas no banco para ver como as coisas estão por lá, mas muito eficiente.
Para transformar seu objeto Date em um inteiro, use o seguinte método:
1 2 3 4 5 6 7 8 |
public static Long persistDate(Date date) { if (date != null) { return date.getTime(); } return null; } |
Ou, dependendo de como você monta seus INSERTs no SQLite…
1 2 3 4 5 |
ContentValues values = new ContentValues(); values.put(COLUMN_NAME, persistDate(entity.getDate())); long id = db.insertOrThrow(TABLE_NAME, null, values); |
Já para transformar o inteiro novamente em Date, pode usar o construtor da classe Date que recebe um long por parâmetro, como no método abaixo de exemplo:
1 2 3 4 5 6 7 8 |
public static Date loadDate(Cursor cursor, int index) { if (cursor.isNull(index)) { return null; } return new Date(cursor.getLong(index)); } |
O mais bacana dessa abordagem é a eficiência até mesmo em queries usando essa coluna como ordenação. Apenas lembre-se de sempre salvar a data UTC como inteiro no banco caso esteja usando a classe Calendar do Java para regionalizar e adaptar as time-zones do dispositivo do usuário.
Booleans no SQLite
Não, SQLite não possui um tipo boolean. No entanto, a própria documentação sugere uma solução para isso:
“SQLite does not have a separate Boolean storage class. Instead, Boolean values are stored as integers 0 (false) and 1 (true).”
Em uma tradução literal: SQLite não tem uma classe de armazenamento Boolean separada. Ao invés disso, valores Boolean são armazenados como inteiros 0 (false) e 1 (true).
Simples, não?!
E se você acha isso pouco performático, lembre-se que o INTEGER do SQLite possui tamanho dinâmico, partindo de 1 byte (8 possibilidades) até 8 bytes (equivalente a um LONG), sendo assim, usar um INTEGER como Boolean vai ocupar 8-bit de disco.
E aí, tem alguma dica valiosa de SQLite que queira compartilhar de graça com o pessoal? Deixe aí nos comentários!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.
Ola Lauiz,
Tens um projeto do item “#3 – Como utilizar um banco SQLite já existente” ?
Ou um post mais detalhado.
Estou tentando ler um script de criação de tabelas e insert’s mais da erro.
Nesse seu exemplo ele so cria mesmo a Base.
É só fazer escrever um arquivo .sql com uma instrução por linha e ler ele, linha-a-linha, e ir executando as mesmas no onCreate do DbHelper. Se a performance se tornar um problema, terá de fazer isso via um serviço que rode em background no Android.
boa tarde,
muito bom seu artigo, mas não entendi uma coisa, vc diz que não tem ORM para usar com o SQLite, mas passa uma lista de frameworks…. n entendi muito bem.
desde ja agradeço.
Bá, ficou confuso mesmo, obrigado por avisar. O que eu queria dizer, é que vou arrumar, é que os ORMs tradicionais do Java, como Hibernate, não podem ser usados no Android. Valeu o toque.