Com um pouco de atraso, finalmente estou postando a parte 3 deste tutorial voltado para a introdução ou fixação de conceitos utilizados na programação para web usando Java.

Nesta parte, criaremos uma busca de models pelo id e implementaremos uma idéia bastante legal e “preguiçosa” usada em JPA ou Hibernate.

Vejam também a parte 1 e a parte 2 do tutorial, caso ainda não o tenham feito.No final da parte anterior concluimos a persistência de um novo model com retorno do id gerado e implementamos a atualização de um model já persistido. Entretanto, não pudemos testar propriamente sem uma busca de model pelo seu id. Podemos começar por este ponto.

Find by Private Key

Caso você tenha lido as partes anteriores, provavelmente deve estar imaginando (ou já ter implementado =P) como poderia ser este método. Se pensou em construir um SQL para a busca como “SELECT * FROM Pessoa WHERE id = 1“, pensou certo.

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
    /**
     * Retorna um BasicModel persistido através do seu id
     *
     * @param id
     * @return {@link BasicModel}
     */
    public BasicModel findByPk(Class clazz, Integer id) throws Exception {
        String select = "SELECT id, ";
        String from = " FROM " + clazz.getSimpleName();
        String where = " WHERE id = " + id.toString();
        boolean primeiro = true;
        for (Field field : clazz.getDeclaredFields()) {
 
            //Se o campo for uma coleção de objetos ou um basic model
            if (Collection.class.isAssignableFrom(field.getType()) ||
                    BasicModel.class.isAssignableFrom(field.getType())) {
                //DO NOTHING
                continue;
            }
            else {
                if (primeiro) {
                    primeiro = false;
                } else {
                    select += ", ";
                }
            }
            select += field.getName();
        }
 
        ResultSet keys = null;
 
        try {
            Connection conn = getConnection();
            PreparedStatement pstmt = conn.prepareStatement(select + from + where);
            pstmt.execute();
 
            keys = pstmt.getResultSet();
            if (keys.next()) {
                return ResultSet2BasicModel(keys, clazz);
            }
 
        } catch (Exception ex) {
            System.err.println("Erro na operação de busca por id. A operação foi abortada.");
            throw ex;
        }
 
        return null;
    }

Após construirmos o SQL e realizarmos a consulta, convertemos o ResultSet obtido para um BasicModel da seguinte forma:

    /**
     * Monta uma nova instância de um model a partir das informações contidas no resultSet passado
     *
     * @param keys
     * @param clazz
     * @return {@link BasicModel}
     */
    private BasicModel ResultSet2BasicModel(ResultSet keys, Class clazz) throws Exception {
        if(keys != null) {
            BasicModel model = (BasicModel)clazz.newInstance();
 
            for (Field field : clazz.getDeclaredFields()) {
                //Se o campo for uma coleção de objetos ou um basic model
                if (Collection.class.isAssignableFrom(field.getType()) ||
                        BasicModel.class.isAssignableFrom(field.getType())) {
                    //DO NOTHING
                    continue;
                }
                //Se o campo for uma string
                else if(String.class.isAssignableFrom(field.getType())) {
                    Method method = model.getClass().getDeclaredMethod(getFieldSetterName(field.getName()), new Class[]{String.class});
                    method.invoke(model,keys.getString(field.getName()));
                }
                //Se o campo for um date
                else if(Date.class.isAssignableFrom(field.getType())) {
                    Method method = model.getClass().getDeclaredMethod(getFieldSetterName(field.getName()), new Class[]{Date.class});
                    method.invoke(model,keys.getDate(field.getName()));
                }
                //Se é outra coisa
                else {
                    Method method = model.getClass().getDeclaredMethod(getFieldSetterName(field.getName()), new Class[]{Object.class});
                    method.invoke(model,keys.getObject(field.getName()));
                }
            }
            model.setId(keys.getInt("id"));
            return model;
        }
        return null;
    }
 
    /**
     * Gera o nome do setter para o dado atributo
     * @param fieldName
     * @return String
     */
    private String getFieldSetterName(String fieldName) {
        return "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
    }

Então temos o model buscado pelo seu id. =)

Podemos atualizar o teste e criar um novo teste apenas para esta operação. E falando em testes, eu alterei um pouquinho a classe BasicDaoTest para ficar melhor apresentada. Antes de cada teste o banco é limpo, melhorando o isolamento dos testes.

public class BasicDaoTest extends TestCase {
 
    /**
     * {@link BasicDao}
     */
    private BasicDao basicDao;
 
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        basicDao = new BasicDao();
        basicDao.limparTabela("Pessoa");
    }
 
    public void testBasic() throws Exception {
        basicDao.testConnection();
        assertTrue(true);
    }
 
    public void testPersistNew() throws Exception {
        Pessoa pessoa = new Pessoa();
        pessoa.setDataNascimento(new Date());
        pessoa.setNome("Pessoa Qualquer 1");
 
        basicDao.persist(pessoa);
        assertNotNull(pessoa.getId());
    }
 
    public void testFindByPk() throws Exception {
        Pessoa pessoa = new Pessoa();
        pessoa.setDataNascimento(new Date());
        pessoa.setNome("Pessoa Qualquer");
        basicDao.persist(pessoa);
 
        Pessoa pessoaTest = (Pessoa)basicDao.findByPk(Pessoa.class, pessoa.getId());
        assertNotNull(pessoaTest.getId());
        assertEquals(pessoa.getNome(), pessoaTest.getNome());
    }
 
    public void testPersistUpdate() throws Exception {
        Pessoa pessoa = new Pessoa();
        pessoa.setDataNascimento(new Date());
        pessoa.setNome("Pessoa Qualquer 2");
        basicDao.persist(pessoa);
 
        Pessoa pessoa2 = new Pessoa();
        pessoa2.setId(pessoa.getId());
        pessoa2.setDataNascimento(new Date());
        pessoa2.setNome("Pessoa Qualquer 3");
        basicDao.persist(pessoa2);
 
        Pessoa pessoaTest = (Pessoa)basicDao.findByPk(Pessoa.class, pessoa.getId());
        assertEquals(pessoa2.getNome(), pessoaTest.getNome());
    }
}

E no BasicDao, implementei um método simples para limpeza de tabelas.

    /**
     * Deleta todas as entradas da tabela cujo nome foi passado.
     *
     * @param nome
     */
    public void limparTabela(String nome) throws Exception {
        String delete = "DELETE FROM " + nome;
        Connection conn;
        try {
            conn = getConnection();
            PreparedStatement ps = conn.prepareStatement(delete);
            ps.execute();
        } catch (Exception ex) {
            System.err.println("Erro ao deletar os dados da tabela '" + nome + "'");
            throw ex;
        }
    }

Lazy Connection

Bem, será que alguém notou que, ao buscar um BasicModel, nenhum outro model ou collection relacionados são obtidos?  Na linha 15 do findByPK nada é feito quando o atributo do model é do tipo model ou collection.

Isso é feito visando a questão de performace, apesar de se tratar de um exemplo meramente didático.

Para visualizar o problema, imagine que tenhamos muitos dados (pessoas) no banco. Se na busca por id incluíssemos todos os atributos, acabaria gerando uma busca recursiva (busca uma pessoa, e os pais dessa pessoa, e os pais dos pais…), passando por todas as pessoas com alguma ligação à pessoa buscada. Se necessitarmos apenas dos dados de uma pessoa, todas as outras muitas buscas realizadas foram um desperdício de processamento, conexão com o banco e tempo.

Para resolver isso, o Java Persistence API (JPA) possui o conceito de Lazy Connection (ou conexão preguiçosa ^^). Utilizando essa especificação, podemos mapear modelos de dados utilizando relacionamentos “preguiçosos”. No nosso exemplo, poderíamos mapear o relacionamento pai-filho como lazy de modo que, ao obter uma pessoa, o seu pai não é imediatamente consultado no banco. Ao invéz disso, um proxy fica no lugar do pai até que este seja requerido através, por exemplo, de um getPai().

Como não utilizaremos nenhum framework, teremos que implementar esse conceito no braço! =D

Mas não será nada muito complexo… A idéia é basicamente implementar uma busca por pai, mãe e filhos partindo de uma pessoa e, então, alterar os getters do model Pessoa para realizar a busca se necessário.

Lazy getMae() / getPai()

Como pai e mãe de uma pessoa obtida através de uma consulta ao banco estarão nulos, não temos informação para realizar uma busca por essas pessoas. Por isso, antes de fazer um findByPK para pai e mãe, temos que obter os seus id consultando o próprio filho.

    /**
     * Obtem o model pai de um model filho já instanciado.
     *
     * @param child
     * @param parentClazz
     * @param fieldName
     * @return BasicModel
     */
    public BasicModel getLazyBasicModel(BasicModel child, Class parentClazz, String fieldName) throws Exception {
        String select = "SELECT " + fieldName + " FROM " + child.getClass().getSimpleName() + " WHERE id = " + child.getId();
        try {
            Connection conn = getConnection();
            PreparedStatement ps = conn.prepareStatement(select);
            ResultSet keys = ps.executeQuery();
 
            if(keys.next() && keys.getObject(1) != null) {
                return findByPk(parentClazz, keys.getInt(1));
            }
        } catch (Exception ex) {
            System.err.println("Erro ao obter o model pai por conexão lazy. A operação foi abortada.");
            throw ex;
        }
        return null;
    }

Bastante simples. Agora alteramos os getters em questão de Pessoa para verificar a nulabilidade dos pais e buscar por eles caso afirmativo.

    public Pessoa getMae() throws Exception {
        if(mae == null && this.id != null) {
            BasicDao basicDao = new BasicDao();
            mae = (Pessoa) basicDao.getLazyBasicModel(this, Pessoa.class, "mae");
        }
        return mae;
    }
 
    public Pessoa getPai() throws Exception {
        if(pai == null && this.id != null) {
            BasicDao basicDao = new BasicDao();
            pai = (Pessoa) basicDao.getLazyBasicModel(this, Pessoa.class, "pai");
        }
        return pai;
    }

Temos, é claro, que testar! Então adicionemos o seguinte teste ao BasicDaoTest:

    public void testGetLazyModel() throws Exception {
        Pessoa pai = new Pessoa();
        pai.setDataNascimento(new Date());
        pai.setNome("Pessoa Pai");
        basicDao.persist(pai);
 
        Pessoa filho = new Pessoa();
        filho.setNome("Pessoa Filho");
        filho.setDataNascimento(new Date());
        filho.setPai(pai);
        basicDao.persist(filho);
 
        Pessoa filhoTest = (Pessoa) basicDao.findByPk(Pessoa.class, filho.getId());
        assertNotNull(filhoTest.getPai());
        assertEquals(pai.getId(), filhoTest.getPai().getId());
        assertNull(filhoTest.getMae());
    }

Se tudo ocorrer bem, podemos avançar para a próxima e um pouco mais complexa parte.

Lazy getFilhos()

Antes de mais de nada, existe uma observação que deve ser feita com relação ao modelo de dados utilizado aqui.

Como esse projeto tem uma função apenas didática e como estamos construindo cada um dos SQLs de interação com o banco, podemos nos dar a liberdade de realizar escolhas visando a facilidade de implementação. Mas nem sempre essas escolhas poderão ser feitas.

Por exemplo, no BasicModel implementado, cada pessoa possui pai, mae e filhos de maneira direta. Entretando, se fôssemos utilizar um framework de persistência como o Hibernate, teríamos que estudar um pouco melhor esses relacionamentos.

No momento de mapear a coleção de filhos, precisamos informar qual o atributo do outro lado que comanda o relacionamento. Por exemplo, num relacionamento Estado-Cidade, um Estado possui um conjunto de cidades, e o atributo que comanda esse relacionamento é Cidade.estado. Entretanto, no relacionamento de Pai-Filho, temos mais de um atributo que pode comandar (pai e mae). Por isso, utilizando o Hibernate teríamos que alterar o modelo, talvez criando uma tabela intermediária com o tipo de relacionamento.

relacionamento_pessoa.jpg

Mas voltando à nossa realidade, podemos criar um SQL como “SELECT * FROM Pessoa WHERE pai = 1 OR mae = 1“. Então mãos à obra!

    /**
     * Obtem uma lista de filhos a partir de um model pai
     *
     * @param parent
     * @param childClazz
     * @param fieldNames
     * @return {@link List} de {@link BasicModel}
     */
    public Set<BasicModel> getLazyBasicModelCollection(BasicModel parent, Class childClazz, String[] fieldNames) throws Exception {
        String select = "SELECT id, ";
        String from = " FROM " + childClazz.getSimpleName();
        String where = " WHERE ";
        boolean primeiro = true;
        for (Field field : childClazz.getDeclaredFields()) {
 
            //Se o campo for uma coleção de objetos ou um basic model
            if (Collection.class.isAssignableFrom(field.getType()) ||
                    BasicModel.class.isAssignableFrom(field.getType())) {
                //DO NOTHING
                continue;
            }
            else {
                if (primeiro) {
                    primeiro = false;
                } else {
                    select += ", ";
                }
            }
            select += field.getName();
        }
 
        primeiro = true;
        for(String fieldName : fieldNames) {
            if (primeiro) {
                primeiro = false;
            } else {
                where += " OR ";
            }
            where += fieldName + " = " + parent.getId().toString();
        }
 
        try {
            Connection conn = getConnection();
            PreparedStatement pstmt = conn.prepareStatement(select + from + where);
            pstmt.execute();
 
            Set<BasicModel> filhos = new HashSet<BasicModel>();
            ResultSet keys = pstmt.getResultSet();
            while (keys.next()) {
                filhos.add(ResultSet2BasicModel(keys, childClazz));
            }
            return filhos;
        } catch (Exception ex) {
            System.err.println("Erro ao obter uma coleção de models por conexão lazy. A operação foi abortada.");
            throw ex;
        }
    }

A implementação não ficou muito complicada. Apenas obtemos todas as pessoas cujo pai ou mãe é a pessoa base (atenção… nós não restringimos que uma pessoa pode ser apenas pai ou mãe!) e retornamos uma lista com essas pessoas.

Em Pessoa temos que alterar o getFilhos():

    public Set getFilhos() throws Exception {
        if(filhos == null && this.id != null) {
            BasicDao basicDao = new BasicDao();
            Set set = basicDao.getLazyBasicModelCollection(this, Pessoa.class, new String[]{"pai","mae"});
            filhos = set;
        }
        return filhos;
    }

E então, podemos testar!

    public void testGetLazyCollection() throws Exception {
        Pessoa pai = new Pessoa();
        pai.setDataNascimento(new Date());
        pai.setNome("Pessoa Pai");
        basicDao.persist(pai);
 
        Pessoa filho = new Pessoa();
        filho.setNome("Pessoa Filho");
        filho.setDataNascimento(new Date());
        filho.setPai(pai);
        basicDao.persist(filho);
 
        Pessoa paiTest = (Pessoa) basicDao.findByPk(Pessoa.class, pai.getId());
        assertNotNull(paiTest.getFilhos());
        assertEquals(1, paiTest.getFilhos().size());
    }
 
    public void testGetLazyCollection2() throws Exception {
        Pessoa pai = new Pessoa();
        pai.setDataNascimento(new Date());
        pai.setNome("Pessoa Pai");
        basicDao.persist(pai);
 
        Pessoa filho1 = new Pessoa();
        filho1.setNome("Pessoa Filho 1");
        filho1.setDataNascimento(new Date());
        filho1.setPai(pai);
        basicDao.persist(filho1);
 
        Pessoa filho2 = new Pessoa();
        filho2.setNome("Pessoa Filho 2");
        filho2.setDataNascimento(new Date());
        filho2.setMae(pai);
        basicDao.persist(filho2);
 
        Pessoa paiTest = (Pessoa) basicDao.findByPk(Pessoa.class, pai.getId());
        assertNotNull(paiTest.getFilhos());
        assertEquals(2, paiTest.getFilhos().size());
    }

Se tudo deu certo os testes deverão estar 100% e estamos prontos para começar, efetivamente, a camada web!… Outro dia, claro…

(Bruno! Agora é com você!)

Baixe aqui o código fonte do core com o que foi visto até agora: arvore-ginecologica. (Obs: necessita o driver do pgsql e não notem o nome “zuado” do projeto ^^)

WARNING! POST EXTENSO DETECTADO! o_O

Share