_sqlite Transparecendo a persistência de dados em Android com MVC Conhecendo o funcionamento do SQLite e aplicando as práticas do modelo MVC no desenvolvimento da camada DAO. Douglas Cavalheiro | [email protected] Bacharel em Sistemas de Informação, trabalha com Java desde 2009, focado no desenvolvimento Android a partir de 2011. Desenvolveu a biblioteca DroidPersistence para persistência em SQLite na S.O. para dispositivos móveis do Google. M uitas aplicações comerciais estão sendo voltadas à mobilidade. Empresas já vêm migrando parte de seus módulos para tablets ou celulares, principalmente devido à facilidade de manter esses aparelhos sempre conectados e à mão. A grande vantagem hoje de portar aplicações, ou parte delas, para tablets é todo o ambiente oferecido ao desenvolvedor, mesmo que limitado a desempenho, mas funcional. É o caso do Android SDK que nos permite escrever aplicações na linguagem Java e integrar com o banco de dados SQLite, lembrando que é possível utilizar outras formas de storage como DB4Object. O conceito MVC se aplica muito bem em projetos deste porte, pois nos permite organizar e distribuir parte das camadas para demais projetos ou envolver na nova aplicação outros projetos já amadurecidos que estejam nesta estrutura (Modelo, Visão e Controle). Aplicaremos este conceito de forma transparente e agradável para manutenções claras e com resultados bem consolidados ao longo do tempo. Temos anseio de iniciar o mais breve possível a codificação quando se trata de novas tecnologias como o Android, mas o resultado fica ainda melhor quando programar não é a preocupação principal e sim o objetivo a ser alcançado da forma mais simples possível. Planejar uma aplicação móvel que terá constante interação com base de dados torna-se uma atividade complexa se não houver métricas de desenvolvimento, assim como em qualquer outro sistema, é importante resgatarmos todas as boas práticas que vamos adquirindo ao longo do tempo e adequá-las quando formos ingressar para um novo ambiente. Olhando pelo lado do desenvolvedor, a garantia de um bom resultado no novo projeto é o sucesso das aplicações desenvolvidas anteriormente. Portanto, antes de iniciar o projeto, analise sua aplicação numa linha que inicie do ambiente real com regras de negócio e termine na implementação, esta observação parece filosófica, mas sugere uma visão menos técnica e voltada ao mundo real, tendo por consequência as expectativas do interessado (usuário) sendo atendidas da melhor forma possível pelo analista. Definindo o Projeto Seguindo a arquitetura MVC, além da view, a camada de “Business Object” ou “Service” irá realizar validações e repassar para os objetos DAO as funções de CRUD. A layer DAO terá uma estrutura com interfaces que declaram os métodos que serão implementados por cada DAO, isto permite flexibilidade para projetos não Android e homogenização da interação de dados. Também, é claro, o modelo que será nossa classe Cliente. Em qualquer dúvida, o projeto de exemplo está disponível no site da MundoJ. DAO Uma ótima prática para utilização de DAOs é a criação de uma interface que declara os métodos que irão ser utilizados. O motivo? O reúso. O objeto DAO que implementar esta interface poderá ter implementação diferente, mas as assinaturas dos métodos serão as mesmas. Conforme o que é mostrado por Silveira[2], programar voltado à interface e não a implementação é uma das boas práticas de orientação a objetos. Um exemplo numa aplicação Java é criar os métodos na interface, podemos codificar uma classe DAO que utiliza Connection e outra que utiliza o EntityManager da JPA que implementam a interface. São implementações diferentes, mas o objetivo das funções é retornar o mesmo resultado. Além do reúso, usar interfaces explica a linha de partir do menos 33 \ Quem já está acostumado a desenvolver em Java com certeza já se deparou com atividades de acesso aos dados e usou JDBC com suas factories (Connection ou implementações JPA, por exemplo) para desenvolver os famosos DAOs. No sistema Android, não temos o mesmo fluxo oferecido nas aplicações Java web ou desktop, mas podemos adequar boas práticas e minimizar o código para a camada de persistência e consulta de dados. técnico para o mais abrangente (implementação) durante o desenvolvimento para estar mais próximo do negócio neste início de construção. Listagem 1. Criação da Interface DAO. sobrescritos. Listagem 2. Criação da Classe Dao. public class ClienteDao extends SQLiteOpenHelper implements IClienteDao{ package br.com.cadastroclientes.dao; private SQLiteDatabase database; import java.util.List; /**Construtor*/ public ClienteDao(Context context, String name, CursorFactory factory,int version) { import br.com.cadastroclientes.model.Cliente; public interface IClienteDao { } Cliente getByNome(String nome); Cliente getById(long id); List<Cliente> listAll(); boolean saveCliente(Cliente cliente); boolean deleteCliente(long id); boolean updateCliente(Cliente cliente); O código da Listagem 1 deixa claro que não há dependência nenhuma de bibliotecas ou outras classes fora do contexto do projeto. Para projetos multiplataforma, a interface DAO, a camada modelo e a camada de negócio são candidatas a um projeto separado. Imagine um projeto Web e Android que ambos devem cadastrar clientes, não fica prático copiar e colar o código de um projeto a outro. Numa simples manutenção no modelo, este deverá ser alterado em ambos os projetos, não queremos isso, por fim criamos um projeto “core” que mantém toda esta estrutura e adicionamos este projeto ao build path do projeto mobile e no projeto web. Implementando o DAO Aqui se inicia a implementação da interface usando as particularidades do SDK Android. Num DAO Android a classe deve, além de implementar a interface, estender SqliteOpenHelper, pois esta classe nos permite interagir com o objeto Database, do qual provém as ações CRUD. Consequentemente o SqliteOpenHelper traz alguns métodos para serem / 34 super(context, name, factory, version); } database = getWritableDatabase(); } //demais métodos No Construtor, encontramos parâmetros que devem ser mantidos, tais como Context, que é o contexto da aplicação, sendo obtido nas classes de Activity e Application do Android. O nome do banco de dados, o CursorFactory (este, é um criador de query, no caso da aplicação de exemplo não será usado, sendo passado null) e a versão do banco de dados, esta versão é importante caso queira realizar um novo build da aplicação com alterações de estrutura ou inserção de dados, a criação de um novo campo na tabela, por exemplo. Logo abaixo temos o objeto privado SQLiteDatabase sendo instanciado pelo método getWritableDatabase() este método abre o banco de dados para uso e o cria se necessário. Como é criado o banco de dados da aplicação? E como criamos as estruturas mencionadas anteriormente? A classe SQLiteOpenHelper nos dá uma visão melhor de como a mágica acontece com os métodos de OnCreate e onUpgrade (Listagem 3). Listagem 3. Métodos que criam a tabela e atualizam se necessário. /**Método chamado quando o banco de dados é criado*/ @Override public void onCreate(SQLiteDatabase db) { StringBuilder sb = new StringBuilder(); sb.append(“create table if not exists CLIENTE(“); sb.append(“ID integer not null primary key autoincrement,”); sb.append(“NOME varchar(50),”); sb.append(“SOBRENOME varchar(50),”); sb.append(“IDADE integer”); sb.append(“);”); } db.execSQL(sb.toString()); /**Método lançado caso a versão do banco de dados tenha sido atualizada * na mesma aplicação. Usado geralmente para adicionar novos campos * ou “Drop” da tabela e criar a nova chamando o onCreate*/ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if(newVersion > oldVersion){ /*inserir aqui scripts de drop ou alter table se necessário de acordo com a versão do banco de dados */ } } //demais métodos Cliente cliente = new Cliente(); cliente.setId(cursor.getInt(0)); cliente.setNome(cursor.getString(1)); cliente.setSobrenome(cursor.getString(2)); cliente.setIdade(cursor.getInt(3)); } return cliente; /**Instancia o objeto ContentValues, facilitando insert e update*/ private ContentValues serializeContentValues(Cliente cliente){ ContentValues values = new ContentValues(); values.put(“NOME”, cliente.getNome()); values.put(“SOBRENOME”, cliente.getSobrenome()); values.put(“IDADE”, cliente.getIdade()); return values; } //demais métodos Cursor é uma interface que funciona como o ResultSet do java.sql onde obtemos o resultado de uma consulta e podemos interagir entre ela, a prática de construção de um método como o executeSelect facilita consultas onde o desenvolvedor tem de trazer todo o objeto da tabela e para transformar os dados em objeto. Outro método como o serializeByCursor é recomendado para economia e versatilidade no código do DAO transformando cada resultado em um objeto instanciado, já o método serializeContentValues faz o contrário, transforma o objeto em values para insert ou update. Com o código apresentado na Listagem 4 podemos fazer qualquer operação de dados. No DAO, basta implementar os métodos da interface descrita anteriormente. No método onCreate encontramos algo familiar que é a instrução de “CREATE TABLE” definindo então a estrutura da tabela cliente. O SqliteDatabase usado como parâmetro é o banco de dados aberto na construção do banco de dados. Lembrando que não é necessário invocar o método onCreate, pois ele é disparado no momento que o método getWritableDatabase é chamado. Na função onUpgrade informamos instruções de alter table ou inserts e updates no caso Listagem 5. Implementação da interface no objeto de sua versão do banco de dados ter sido alterada DAO. como dito anteriormente. Listagem 4. Métodos que auxiliam na interação entre o objeto e os dados. /**Método que rertorna um cursor de um select simples, * mais amigável, se necessário incluir os parametros de * groupBy e having*/ private Cursor executeSelect(String selection, String[] selectionArgs, String orderBy){ return database.query(“CLIENTE”, new String[]{ “ID”, “NOME”, “SOBRENOME”, “IDADE” }, selection, selectionArgs, null, null, orderBy); } /**Método que instancia o objeto e o serializa * com o resultado de um Cursor*/ private Cliente serializeByCursor(Cursor cursor){ @Override public Cliente getByNome(String nome) { Cliente cliente = null; Cursor cursor = executeSelect( “NOME = ?”, new String[]{nome}, null); if(cursor != null && cursor.moveToFirst()){ cliente = serializeByCursor(cursor); } } if(! cursor.isClosed()) cursor.close(); return cliente; 35 \ @Override public List<Cliente> listAll() { List<Cliente> list = new ArrayList<Cliente>(); Cursor cursor = executeSelect( null, null, “1”); if(cursor != null && cursor.moveToFirst()){ do{ list.add(serializeByCursor(cursor)); }while(cursor.moveToNext()); } } if(! cursor.isClosed()) cursor.close(); return list; @Override public boolean saveCliente(Cliente cliente) { ContentValues values = serializeContentValues(cli ente); //retorno do insert é a quantidade de registros inseridos if(database.insert(“CLIENTE”, null, values) > 0) return true; else return false; } @Override public boolean deleteCliente(long id) { if(database.delete( “CLIENTE”, “ID = ?”, new String[]{ String.valueOf(id) }) > 0) return true; else return false; } //demais métodos de busca, listagem e update Business Object (service) e seu baixo acoplamento Business Object ou Service é um objeto voltado ao controle do negócio, como cita Fowler[3] sobre Design guiado pelo domínio. Neste objeto colocamos regras de negócio antes de persistir ou realizar alguma tarefa na camada visão. Validações também são bem-vindas. No objeto ClienteBO é necessário invocar os métodos do ClienteDao, mas necessitando o mínimo possível do objeto ClienteDao, faremos isso usando a interface DAO. / 36 //trecho public class ClienteBO { private IClienteDao clienteDao; public ClienteBO(Context context, String databaseName, int databaseVersion){ clienteDao = new ClienteDao( context, databaseName, null, databaseVersion); } public void saveCliente(Cliente cliente) throws Exception{ validateCadastroCliente(cliente); if(! clienteDao.saveCliente(cliente)) throw new Exception(“Cliente não pode ser salvo !”); } public List<Cliente> getListCliente(){ return clienteDao.listAll(); } // demais métodos usando o dao e o // validateCadastroCliente omitidos } O atributo clienteDao utilizado em todos os métodos de interação é a interface instanciada no construtor com sua implementação Android ClienteDao, neste procedimento mantemos a pouca dependência do objeto que implementa a interface. Mas e o construtor? Este tem particularidades do Android usando Dependence Injection, este BO não poderá ser usado como uma lib ou projeto avulso. Isso é verdade, mas a solução é simples: criar um objeto BO para o android etendendo do BO genérico e só implementando o construtor já resolveria o problema. Usando a Classe Application Um recurso muito útil para desenvolvedores Android é poder sobrescrever a classe Application. Sua função é instanciar objetos globais que podem ser usados na aplicação. Conforme a documentação[4], singletons podem resolver isso, porém, se o singleton precisar de um Context ou demais recursos como Broadcast Receivers, já seria mais funcional utilizar a própria classe Application que contém estes atributos. Listagem 6. Classe Application. public class CadastroClientesApp extends Application{ private ClienteBO clienteBO; @Override public void onCreate() { super.onCreate(); clienteBO = new ClienteBO( this, “DATABASE_ CLIENTES”, 1 ); } } A fim de que o SDK entenda que a classe escrita será a que implementará a aplicação, é necessário informar o AndroidManifest.xml para que ela seja executada nas operações de início e fim. Desta maneira, a tag android:name deve conter o nome da classe: <application android:name=”CadastroClientesApp” … Usando o BO instanciado na Activity Com a classe Application executando, e a Classe BO instanciada, torna-se fácil a chamada do objeto e seus métodos, desde que também se utilize o método getApplication() para buscar a instância do objeto Application dentro da activity Android: //trecho private CadastroClientesApp app; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.cadastro_cliente); //Instancia a aplicação app = (CadastroClientesApp) getApplication(); } E, por fim, é possível desfrutar dos BO chamando-o diretamente da Application: //Trecho /* demais métodos omitidos*/ app.getClienteBO().saveCliente(cliente); /* demais métodos omitidos*/ A grande vantagem de usar um único Business Object instanciado é a minimização de locks no banco de dados, pois não haverá preocupação em abrir e fechar transações durante o runtime da aplicação, mas se precisar abrir e fechar as transações manualmente por questões de otimização ou até mesmo controle próprio, os métodos beginTransaction e endTransaction do SQLiteDatabase. Outro benefício é o ganho em heap, mas é interessante monitorá-lo para ver o que pode estar errado numa possível lentidão na inicialização. É importante imaginar que se a utilização de um BO for somente uma vez e chamada pelo usuário esporadicamente, talvez não seja necessária a criação do objeto na classe Application durante a inicialização do sistema e sim dentro da própria activity, porém, um cuidado especial ao parâmetro version no construtor do DAO, pois se muitas criações do BO ficarem espalhadas pela aplicação e um upgrade na versão do BD for necessário, o refactoring poderá ser doloroso. Um meio de resolver o parâmetro version é usando uma classe com constantes ou no modelo singleton. Considerações Finais O modo em que se aplica um banco de dados Sqlite no projeto Android fica muito a cargo do desenvolvedor. Fazer a leitura e atualização de dados pode ser uma prática problemática durante o desenvolvimen- to ou manutenção posterior. O artigo sugere formas de aproveitar bem os conceitos de desenvolvimento orientado a objetos e grandes facilidades que o Android SDK proporciona. Algumas particularidades do Android podem trazer benefícios à aplicação como a classe “Application” que é instanciada no momento que a aplicação é executada (não a confunda com Singleton), e esta pode conter classes provedoras de negócio que servem de ponte para o acesso a todos os DAOs do projeto, sendo fácil e rápido chamar os métodos sem ter que ficar instanciando a cada necessidade. Tudo dependerá de como for projetada a aplicação, quantas serão as interações entre os dados, quantas tabelas existirão no banco de dados etc. Uma pergunta a ser feita durante o escopo do projeto é: realmente é necessário usar DAO, BO e criar uma classe gerenciadora de dados para somente usar a tabela “Usuário” na tela de login? A resposta é implícita se tudo estiver projetado. De qualquer modo o artigo tem foco nas aplicações comerciais, que exigem bastante carga e interação de dados ou outra aplicação do gênero. Para jogos, aplicações como redes sociais, talvez o Sqlite nem seja a melhor recomendação, o Android tem também outras formas de gravar informações como o Shared Preferences e escrita em arquivo. /referências > http://www.sqlite.org/whentouse.html > Introdução à Arquitetura e Design de Software – Uma Visão sobre a Plataforma Java – Editora: Elsevier – Campus > http://martinfowler.com/bliki/AnemicDomainModel.html > http://developer.android.com/reference/android/app/ Application.html /para saber mais > Existem frameworks que auxiliam na escrita do DAO, sendo possível diminuir seu código, principalmente na criação da tabela, métodos de busca e inserção, exclusão e atualização de dados. O DroidPersistence é um projeto que possibilita isso. Mas não descarte o fato de ao menos dominar como trabalha o SDK antes de usar qualquer framework, essa ideia ajudará a resolver possíveis problemas e também servirá de argumento para decisões técnicas de projeto. > O SQLite é um banco de dados relacional simples e a linguagem usada é o SQL (como seu próprio nome indica). Uma das limitações encontradas logo de início em relação a outros SGBDs (Sistemas Gerenciadores de Banco de Dados) é a falta de Stored Procedures, a própria documentação SQLite[1] explica que por ser “Lite” não tem este recurso e se o desenvolvedor necessita utilizar Stored Procedures em seu BD, procure um que lhe atenda, como o Oracle ou PostgreSql. Contudo, as operações de CRUD (Create, Read, Update e Delete – Criar, Ler, Atualizar e Excluir) são feitas normalmente. 37 \