sexta-feira, 1 de novembro de 2013

Banco de dados - Parte 3

Olá à todos.

Neste post terminaremos de explicar as metodologias mais comuns de manipulação de Banco de Dados em Android. Criaremos um exemplo de banco de dados mais simples e rápido de ser acessado, utilizando o framework ORMLite.

Utilizaremos como exemplo um aplicativo bem simples, que apenas mostra na tela uma lista de nomes constantes em uma tabela chamada Bean. Esse aplicativo utilizará o framework ORMLite, um framework de banco de dados para Android que realiza o acesso ao banco de dados de forma bem simplificada.

A lógica deste aplicativo é bem simples:


  • Se é a primeira vez que acessamos o aplicativo, o banco de dados não foi criado ainda. Logo, é criada uma lista de 4 objetos do tipo Bean (em tempo de execução), que é persistida no Banco de Dados, e que por sua vez popula a lista da tela principal.
  • Se não é a primeira vez que acessamos o aplicativo, o banco de dados já foi criado. Assim, basta lermos os dados deste, e popularmos a lista de objetos do tipo Bean na tela principal.


Para controlar a informação descrita acima (se o banco já foi criado ou não), criaremos em nosso banco de dados, além da tabela Bean, a tabela AppInfo. Esta tabela possuirá os campos id e lastUpdate, que informará quando foi realizado o último update (acesso) ao banco de dados deste aplicativo.

Este aplicativo possuirá os seguintes pacotes:


  • activity Equivalente à classe view dos tópicos anteriores. Nela temos as telas que utilizaremos em nosso aplicativo.
  • adapter Classe que possui o Adapter (tipo criador de listas) que será utilizado na tela ListActivity.java do pacote activity.
  • helper Classe que possui as classes que farão o acesso direto ao banco de dados (tanto sua definição como as operações de CRUD). Pode ser associada com a classe dao das seções anteriores deste tópico.
  • manager Classe que resolve um problema que estávamos tendo até o último tópico: o uso de variáveis static. Tal solução de problema será explicada melhor mais adiante.
  • model Classe equivalente à classe bean das seções anteriores.
  • tasks Até o último tópico, tanto a definição do banco de dados como as consultas à ele eram feitos no pacote dao ou impl. Porém, agora, serão intermediados neste pacote, que fará o meio de campo entre os pacotes activity e helper, com o auxílio do pacote manager.
  • Sem falar nas classes AppConfiguration.java e CustomApplication.java, que estão no pacote default do aplicativo (br.android.framework).

Repare que temos muito mais pacotes do que os descritos nos posts anteriores do Blog. Porém, isto não chega a ser um empecilho, uma vez que este modelo de criação de banco de dados pouco se altera ao criarmos aplicações extremamente robustas, com dezenas de tabelas e milhares de linhas de código de dados. Esta é uma vantagem nítida de utilizarmos o ORMLite.

Vamos agora à explicação de cada um dos pacotes deste aplicativo (e consequentemente, à explicação do framework ORMLite). No pacote "default" temos 2 classes importantíssimas, com informações globais sobre o nosso aplicativo.

Na classe AppConfiguration.java temos Informações como a versão do aplicativo, o número de nomes à serem mostrados na tela principal, a versão do banco de dados, o nome do banco de dados, a variável estática que contém a informação sobre se o banco precisa ser atualizado ou não, e a TAG de Log da nossa aplicação.

Já na classe CustomApplication.java temos um conceito novo. Até então, a primeira classe referenciada no arquivo AndroidManifest.xml era a tela inicial da aplicação. Mas agora, além desta tela (que no nosso aplicativo é a tela LauncherActivity.java), temos também a classe descrita neste parágrafo. Este é um conceito interessante em Android, mais utilizado em aplicações robustas. Ou seja, além de uma tela principal, podemos ter uma classe que “gerencia” todo o ciclo de vida do nosso aplicativo.

Nesta classe temos métodos bem particulares:


  • onCreate() É o primeiro método que será acessado em toda a aplicação. Aqui geralmente são definidas informações relativas à todo o escopo da aplicação. Neste caso, a classe que gerenciará o banco de dados, e o contexto do ContentManager (que será explicado mais adiante).
  • onLowMemory() Executada toda vez que nosso aplicativo estiver com pouca memória. Neste método é aconselhável a chamada de métodos que liberem dados da memória. Como será explicado mais tarde neste post, a gerencia de memória da nossa aplicação será feita na classe ContentManager.
  • onTerminate() Executada ao fim da nossa aplicação. É um método utilizado apenas em processos de desenvolvimento. Ou seja, se a aplicação for publicada no Google Play, este método não será mais acessado.
Já no pacote model temos o mapeamento entre os Beans (tipos de dados básicos criados em nossa aplicação) e as tabelas de banco de dados. A principal mudança das classes deste pacote, em relação as outras seções deste tópico, é o surgimento de Annotations e da classe Entity.

Annotations são diretivas de programação colocadas acima de certos comandos em Android. Tais diretivas são implementadas pelo ORMLite. Podemos notar abaixo algumas delas (em vermelho), que podem ser identificadas através de um “@”.





Estas “anotações” fazem o mapeamento direto entre a classe acima e o banco de dados. Ou seja:


  • @DatabaseTable Cria uma tabela com o nome da classe.
  • @DatabaseField(id = true) Cria uma chave primária na tabela.
  • @DatabaseField   Cria uma campo simples na tabela.

Está é uma GRANDE vantagem que o ORMLite nos traz.

No pacote model também temos a classe Entity, uma classe vital na transcrição do Bean para uma tabela de SQLite. Tal transcrição é realizada na classe DatabaseHelper.java, que será abordada no pacote helper. Neste pacote temos a definição das classes que farão o acesso direto ao banco de dados (tanto sua definição como as operações de CRUD).


A classe DatabaseHelper.java faz o papel que a classe SQLiteHelper.java fazia no último tópico. Ela também possui os métodos onCreate() e onUpgrade(). Porém, possui mais alguns componentes que facilitam bastante a manipulação de operações de CRUD em nosso banco de dados (e que não serão explicados aqui, pois tais componentes são relativos à implementação do framework ORMLite).

Neste pacote temos também a classe QueryHelper.java, que faz o meio de campo entre o acesso direto ao banco de dados (ou seja, a classe DatabaseHelper.java) e as classes do pacote tasks. Quanto mais simples pudermos deixar um método, melhor. Por esta razão, é comum essa divisão de tarefas que cada pacote possui. Já no pacote manager temos uma notável evolução em relação aos outros aplicativos vistos anteriormente. Neles, tínhamos diversas variáveis estáticas conversando entre classes e pacotes. Agora, temos apenas uma. Sim, apenas uma, que é definida através do padrão Singleton. Ou seja, apenas 1 instância é mantida na memória ao longo de toda a execução do nosso aplicativo. Isto pode ser verificado abaixo.



Desta forma temos uma significativa redução de acesso de memória.

Em nosso aplicativo, tudo que é mantido em memória é mantido nesta classe, na variável sInstance definida pelo padrão Singleton acima. Também temos em nosso aplicativo o pacote tasks, um pacote importantíssimo. Nele temos as “tarefas” (tasks) que serão executadas ao longo do nosso aplicativo. Geralmente são chamadas por classes do pacote activity (tendo intermédio do ContentManager), e buscam informações de classes do pacote helper.

Temos 3 classes neste pacote: UpdaterAsyncTask.java, BeanAsyncTask.java e Notifiable.java. Todas serão explicadas detalhadamente, abaixo.


  • UpdaterAsyncTask Aqui descobrimos se nosso banco de dados foi atualizado ou não. Isto basicamente é descoberto pelo comando em Java da linha 46 desta classe: Boolean databaseNeeedsUpdate = QueryHelper.updateAppInfoUpdateTime(mContext, info.getId(), Calendar.getInstance().getTimeInMillis());
  • BeanAsyncTask Aqui obtemos a lista de Bean’s do nosso aplicativo. Se o banco já existe, lemos os dados dele. Se não, criamos uma lista de Bean’s. Esta lógica pode ser perfeitamente entendida no método update().
  • Notifiable Interface que gerencia a execução das AsyncTask.

Para entender bem como as classes UpdaterAsyncTask.java e BeanAsyncTask.java funcionam, e como a classe Notifiable.java gerencia suas execuções, precisamos entender o conceito de AsyncTask. E ele não é muito trivial.

Uma classe que extende de uma AsyncTask possui obrigatoriamente os métodos doInBackground() e onPostExecute(). Uma AsyncTask obrigatoriamente executa comandos em uma thread paralela à execução do nosso aplicativo.

No primeiro, é realizada a execução de uma determinada tarefa em paralelo à execução do nosso programa. Logo, temos uma thread em paralelo com o fluxo normal da nossa aplicação.

O segundo método é acessado quando o primeiro é finalizado. Assim, neste método, temos que relacionar este thread em paralelo com a thread principal do programa. E isto é feito através da chamada do método taskFinished() (implementação do Notifiable) constante no ContentManager. Logo, a classe Notifiable “une” as thread’s em paralelo.

Para entender todo esse fluxo, utilizaremos um exemplo. Na linha 80 da classe LauncherActivity.java é chamado o método ContentManager.getInstance().getDatabaseInfo(this, LauncherActivity.this);. Logo, estamos relacionando o pacote activity com o pacote manager.

Uma vez no ContentManager, no método getDatabaseInfo(), é chamada a task UpdaterAsyncTask:



Repare que ela possui uma construção incomum. Se a variável notifiable não for nula, a task será colocada num Map de controle de tasks pendentes. E logo após isso, a task é executada. O importante aqui é entender como o fluxo disto tudo funciona.

Inicialmente, após o comando task.execute(), somos redirecionados para a UpdaterAsyncTask. Imediatamente é acessado seu construtor, e após ele, o método doInBackground(), que realizará o processamento paralelo. Após tal processamento, somos redirecionados para o método onPostExecute(), que nos direcionará para o ContentManager, no método taskFinished().

Então, o fluxo do aplicativo recai na linha 154 do ContentManager, no seguinte comando:
if (FETCH_TASK.UPDATER == taskType). Ou seja, se a task atual for do tipo Updater, o fluxo do aplicativo será desviado até a linha 156, onde obteremos o resultado do comando executado (e já citado anteriormente) na linha 46 da UpdaterAsyncTask. E então, esta task será removida do Map (pois já foi finalizada), e o fluxo da nossa aplicação será direcionado novamente à classe LauncherActivity.java, e o resultado da task acima estará armazenado em memória, em nossa variável Singleton, no atributo mDatabaseNeedsUpdate.

Parece complicado, mas é mais simples do que parece. O importante é que, desta maneira, podemos executar de forma segura mais de um comando ao mesmo tempo, e ao mesmo tempo termos garantido o retorno à thread padrão da nossa aplicação com os dados processados na thread paralela.

Seguindo a explicação sobre AsyncTask’s, vamos destrinchar o funcionamento da task BeanAsyncTask. O entendimento dela é vital no entendimento de todo nosso aplicativo. Eis a task:



Nesta task inicialmente podemos notar que temos como atributos um contexto e uma flag booleana.

O contexto será passado aos métodos que interagirão com o banco de dados (persistBean() e getPersistedBean()). Todo acesso à banco de dados em Android necessita de um contexto.

Já a flag boolean controlará o fluxo de toda nossa task. Se o banco de dados precisa de update (ou seja, se estamos no primeiro acesso aos dados dos Bean, ou se estes dados foram modificados), obtemos os dados de sua origem (a origem pode ser um webservice ou, neste caso, a criação dos Bean em tempo de execução) e armazenamos no banco de dados (através do método persistBean()).

Se o banco de dados não precisa de um update, obtemos os dados do banco (com o método getPersistedBean()).

Esta é a lógica mais comum em operações de leitura de dados, e já foi descrita anteriormente.


Segundo nossa task, podemos ver acima os métodos doInBackground() , onPostExecute() e update(). Como descrito anteriormente, no método doInBackground() temos todo o processamento que será realizado pela AsyncTask; e o método onPostExecute() é o método acessado quando o processamento do método doInBackground() é finalizado.

No doInBackground() chamamos o método update(), cuja lógica já foi explicada na figura anterior.

Este método é o coração de nossa AsyncTask, e sem dúvida o conceito mais importante de todo este tópico. É fundamental entender que quando temos os dados de nossa aplicação armazenados no banco, precisamos obtê-los de lá. E quando não temos os dados (ou eles foram modificados), temos que obtê-los de sua fonte (webservice ou obtenção dos dados em tempo real na aplicação).

Já no método onPostExecute(), chamamos o ContentManager, que possui o “elo” entre a thread desta task e a thread da nossa aplicação, que é o método ContentManager.taskFinished().



Por fim, temos os métodos que interagem com o banco de dados. Como descrito anteriormente, a classe QueryHelper.java é a classe que chama diretamente os métodos de acesso ao banco de dados. Por questão de didática e organização (e prevendo futuramente acesso à bancos de dados enormes, que travariam a thread principal da nossa aplicação), chamamos os métodos de QueryHelper.java a partir de tasks.

Podemos ver acima que no método getPersistedBean(), tudo que fazemos é chamar um método do QueryHelper e retornar a lista dos Bean. Algo bem simples de entender. Já o método persistBean() é um pouco mais complexo.

Nele primeiramente obtemos o DatabaseHelper (para acessarmos o banco de dados diretamente), e então criamos ou atualizamos cada um dos Bean da lista de Bean. Como o acesso ao banco é uma operação cirurgicamente precisa, temos que declarar vários catch, para termos um tratamento de erros que não prejudique o usuário.

Dentre eles, temos os seguintes tratamentos: acesso ilegal ao banco de dados; exceção de SQL; exceção de argumento ilegal no acesso ao banco. Caso o acesso ao banco tenha sido correto, a cláusula finally é executada, e finalmente a conexão com o banco é fechada através do método DatabaseHelper.releaseHelper().

Se tudo que foi exposto acima foi bem entendido, o entendimento de qualquer parte da nossa aplicação se tornará muito mais fácil. Temos também em nosso aplicativo o pacote adapterNeste pacote temos os Adapters que serão usados pelo pacote activity. Mas o que é um Adapter? É a classe em Android que permite o acesso aos itens de uma lista. Ou seja, que intermedia a interface de uma lista com seus dados.

O funcionamento de um Adapter será melhor mostrado no próximo post deste Blog. E finalmente, o pacote onde tudo acontece: activity. Temos aqui as telas LauncherActivity.java e ListActivity.java. A primeira serve apenas como um “carregador” de dados. Assim que todos foram carregados, o fluxo do nosso aplicativo é direcionado para a tela ListActivity.java, onde os dados da lista serão mostrados.

A tela LauncherActivity.java é mostrada abaixo.



Acima temos os atributos e constantes da tela:


  • mBeanLoaded Flag que indica se a lista de Bean‘s já foi obtida pelas tasks.
  • mBeanList Lista de Bean’s.
  • mLauncherActivityAlreadyAcessed Flag que controla se a tela atual já foi acessada previamente. Se já foi (ou seja, se os dados já foram carregados e o fluxo do aplicativo anteriormente já tinha sido deslocado para a tela ListActivity.java), sai da tela atual.

Abaixo temos os eventos de Activity desta tela.


Repare que o único método chamado no evento onCreate() é o método updateContent(). Como o objetivo da tela LauncherActivity.java é o de carregar os dados da tabela Bean de uma fonte externa (um banco de dados, um webservice, uma lista criada em tempo de execução, etc), o que fazemos no evento de onCreate() é chamar o método que obtém conteúdo desta fonte (mais adiante este método será melhor detalhado).

Já no método onResume(), verificamos se nossa tela já foi acessada antes. Se já, é sinal de que temos os dados necessários para popular a tela ListActivity.java. Logo, saímos da tela atual.


Acima temos os métodos updateContent(), getData() e callMainActivity(). No primeiro, chamamos a task que averiguará se os dados do Bean já estão ou não no banco de dados do nosso aplicativo. No segundo, requisitamos a obtenção da lista de Bean através da task BeanAsyncTask, constante no método ContentManager.getBeanList(). E no terceiro método, se a lista de Bean já foi carregada, podemos deslocar o fluxo da aplicação para a tela ListActivity.java.



Por fim, podemos ver o retorno da task que obteve a lista de Bean lá no ContentManager. Esta task obtém a lista e à deixa armazenada na variável mBeanList do ContentManager. E como podemos ver acima, se a lista de Bean não for vazia ou nula (ou seja, se ela estiver na memória), podemos deslocar o fluxo do nosso aplicativo para a tela ListActivity.java, através do método callMainActivity().

-----

Ao fim destes últimos 3 posts, vimos diversos métodos de acesso à banco de dados em Android:


  • Acesso básico ao banco de dados. Porém, exigindo maior conhecimento de linguagem SQL. Neste método as implementações de operações CRUD são feitas de forma mais braçal.
  • Acesso mais completo ao banco de dados, possibilitando o uso de ContentValues e o mapeamento de colunas das tabelas do nosso banco de dados diretamente em nossas classes do pacote model. Porém, ainda assim, com muito trabalho braçal.
  • Acesso completo ao banco de dados, com implementação de operações CRUD de forma extremamente rápida e simples.

O método que será utilizado neste Blog é o último, pois é o mais rápido e fácil.



E enfim, terminamos mais um tema no blog.


Caso alguém tenha uma dúvida, crítica ou sugestão, sinta-se à vontade.

Nenhum comentário:

Postar um comentário