Consultas com Hibernate e a API Criteria – Parte I

Quando utilizamos o Hibernate para realizar consultas no banco de dados, temos a oportunidade de trabalhar com SQL, com HQL e também com Criteria Query API. A API (Application Programming Interface) Criteria do Hibernate fornece uma maneira elegante de construção de query dinamica para consultas no banco de dados. A API é uma alternativa as consultas HQL (Hibernate Query Language) e também ao SQL tradicional.

Quando usamos SQL como consulta, trabalhamos diretamente com tabelas e colunas que devem ser descritas na consulta através de strings. O HQL modificou a forma como as consultas são realizadas quando ao invés de trabalharmos com tabelas e colunas, usamos classes e atributos das classes (variáveis), construindo assim o relacionamento orientado a objeto entre banco de dados e aplicação. Essa técnica possibilita a consulta através de objetos, porém continuamos ainda escrevendo a consulta em linhas strings e Nao em código Java.

Já a API Criteria une as vantagens do HQL com a praticidade de escrever consultas com código Java através da interface org.hibernate.Criteria, abolindo assim o uso de longas strings. A interface Criteria define os metodos necessários para trabalhar com Criteria e por ser um consulta de forma programática, erros no código podem ser sinalizados ainda em tempo de compilação, coisa que com HQL ou SQL Nao é possível observar.

Neste artigo vou exemplificar algumas consultas básicas para quem desconhece a API Criteria. O artigo será dividido em 2 partes, na primeira parte veremos exemplos de consultas realizadas em apenas uma tabela, na segunda parte veremos consultas entre tabelas com relacionamento 1-N e N-N.

1. Arquivos necessários

Veja as bibliotecas necessárias na Figura 1.

Figura 1 - Bibliotecas

Figura 1 – Bibliotecas

Montei um pacote com todas as bibliotecas e as disponibilizei para download neste link (http://www.megaupload.com/?d=UHR7G6WI.

2. HibernateUtil e o arquivo de configuração

Como todos sabem precisamos de uma classe para criar uma SessionFactory (Fabrica de sessões) para usar do Hibernate, para isso, vamos utilizar a classe HibernateUtil, conforme a Listagem 1. Essa classe disponibiliza o metodo getSession() para abrir uma sessão e também o metodo close() para fechar a sessão. Para simplificar o artigo, vamos estender essa classe nas classes que iremos usar para realizar as consultas.

Listagem 1. Classe HibernateUtil.
package com.wp.mb.consultas.util;

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {
    private static final SessionFactory ourSessionFactory;

    static {
        try {
            ourSessionFactory =
                new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }

    public static Session getSession() throws HibernateException {
        return ourSessionFactory.openSession();
    }

    public void close() {
        if (getSession() != null && getSession().isOpen()) {
            getSession().close();
        }
    }
}

O arquivo usado para configurar a SessionFactory é o hibernate.cfg.xml, como pode ser visualizado na Listagem 2. Este arquivo deve ser inserido na pasta raiz do projeto, dentro do diretório dos arquivos fontes (src). Caso Nao tenha ainda trabalhado com Hibernate, leia antes o artigo “Utilizando Swing com Hibernate (SessionFactory)” no link: https://www.mballem.com/post/utilizando-swing-com-hibernate-sessionfactory.

Listagem 2. Arquivo de configuração hibernate.cfg.xml
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <!-- Database connection settings -->
        <property name="connection.driver_class">
            com.mysql.jdbc.Driver
        </property>

        <property name="connection.url">
            jdbc:mysql://localhost:3306/HibernateConsultas
        </property>

        <property name="connection.username">root</property>
        <property name="connection.password"></property>

        <!-- JDBC connection pool (use the built-in) -->
        <property name="connection.pool_size">1</property>

        <!-- SQL dialect -->
        <property name="dialect">
            org.hibernate.dialect.MySQL5Dialect
        </property>

        <!-- Echo all executed SQL to stdout -->
        <property name="show_sql">false</property>

        <!-- Echo format executed SQL to stdout -->
        <property name="format_sql">true</property>

        <!-- Drop and re-create the database schema on startup -->
        <property name="hbm2ddl.auto">update</property>

        <mapping class="com.wp.mb.consultas.model.Pessoa"/>
        <mapping class="com.wp.mb.consultas.model.Veiculo"/>
        <mapping class="com.wp.mb.consultas.model.Telefone"/>
    </session-factory>
</hibernate-configuration>

3. Tabela Pessoas

Para iniciar os exemplos vou disponibilizar um script para criação da tabela PESSOAS. Nesse script temos alguns dados que devem ser inseridos no banco de dados, já que as consultas serão realizadas em cima desses dados para obtenção dos resultados desejados nos exemplos.

Listagem 3. Script Tabela Pessoas
--
-- Estrutura da tabela `pessoas`
--

CREATE TABLE IF NOT EXISTS `pessoas` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT,
  `IDADE` int(11) DEFAULT NULL,
  `NOME` varchar(40) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=24 ;

--
-- Dados da tabela `pessoas`
--

INSERT INTO `pessoas` (`ID`, `IDADE`, `NOME`) VALUES
(1, 19, 'Ana Maria Silva'),
(2, 25, 'Joana Gomes'),
(3, 30, 'Maria Fontona'),
(4, 25, 'Helen Carla Marin'),
(5, 36, 'Silda Paula de Olin'),
(6, 27, 'Marta Soares'),
(7, 28, 'Otavio Costa'),
(8, 21, 'Marcus Odim'),
(9, 19, 'Silvio Dodda'),
(10, 32, 'Carlos André Marty'),
(11, 40, 'Marcio Olacco'),
(12, 42, 'Marta Perez'),
(13, 19, 'Carla Maria Audio'),
(14, 18, 'Fabio Fernandes'),
(15, 48, 'Matias Oliveira'),
(16, 38, 'Ubaldo Nunes'),
(17, 28, 'Tiago Otão'),
(18, 18, 'Mirian Froules'),
(19, 21, 'Tatiane Fernandes'),
(20, 19, 'Diego da Silva Fernandes'),
(21, 31, 'Pedro Olivio Bastos'),
(22, 33, 'Fabio Gonçalves'),
(23, 25, 'Maria Anita Silva');

4. Classe Pessoa e PessoaDao

Vamos começar criando duas classes, a classe Pessoa (Listagem 4) e a classe PessoaDao (Listagem 5) que será onde teremos as consultas. Os metodos da classe PessoaDao veremos mais a frente no artigo, crie apenas a classe e importe as classes necessárias nos imports.

Listagem 4. Classe Pessoa.
package com.wp.mb.consultas.model;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "PESSOAS")
public class Pessoa implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID", nullable = false)
    private Long id;
    @Column(name = "NOME", length = 40)
    private String nome;
    @Column(name = "IDADE", length = 3)
    private int idade;

    // gerar os metodos get/set, toString() e equals/hashCode
}
Listagem 5. Classe PessoaDao.
package com.wp.mb.consultas.dao;

import com.wp.mb.consultas.model.Pessoa;
import com.wp.mb.consultas.util.HibernateUtil;
import org.hibernate.Criteria;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;

import java.util.List;

public class PessoaDao extends HibernateUtil {
	//insira os metodos citados aqui quando avistar
	//o comentario: //Inserir na classe PessoaDao
}

5. Realizando consultas na tabela Pessoas

Agora vamos partir para as consultas, mas antes devemos criar uma classe de testes, veja na Listagem 6.

Listagem 6. Classe de testes.
package com.wp.mb.consultas.executa;

import com.wp.mb.consultas.dao.PessoaDao;
import com.wp.mb.consultas.model.Pessoa;

import java.util.List;

public class Main {
    public static void main(String[] args) {
        //insira os metodos citados aqui quando avistar
        //o comentario: // Inserir no metodo main()
    }
}

Começamos então criando duas consultas bem básicas, a findAll() que irá listar todos os registros cadastrados e a consulta findById() que irá listar um único registro. Na Listagem 7, podemos ver os metodos que devem ser inseridos na classe PessoaDao e o código que faz a chamada a esses metodos. Eles devem ser inseridos no metodo main() da classe de testes, preste atenção nos comentarios adicionados na listagem.

Listagem 7. Consulta Parte 1.
// Inserir no metodo main()

	//Criamos uma instancia da classe PessoaDao.
	PessoaDao dao = new PessoaDao();

	//busca todas as pessoas cadastradas no banco, retorna uma lista.
	List p1 = dao.findAll();
	for (Pessoa p : p1) System.out.println("findAll: " + p.toString());

//Inserir na classe PessoaDao

	// Busca todas as pessoas cadastras
	public List findAll() {
		try {
			//através de uma session obtemos o acesso
			//ao metodo creatCriteria() e passamos como
			//parametro a entidade que será consultada.
			//Como esperamos uma lista, devemos informar
			//isso usando o metodo list().

			return getSession().createCriteria(Pessoa.class).list();
		} finally {
			//Fechamos a sessão
			close();
		}
	}

// Inserir no metodo main()

	Pessoa byId = dao.findById(p1.get(5).getId());
	System.out.println(
		"findById " + p1.get(5).getId() +": "+ byId.toString()
	);

//Inserir na classe PessoaDao

	// Busca uma pessoa pelo ID
	public Pessoa findById(long id) {
		try {
			return (Pessoa) getSession().createCriteria(Pessoa.class)
				.add(Restrictions.idEq(id)).uniqueResult();

			//aqui usamos o metodo add() para adicionar
			//um criterio de busca. Usamos como criterio
			//um objeto do tipo Restrictions e ele nos
			//formece inumeros outros metodos.
			//Como a consulta é por id, vamos usar o idEq(),
			//seria um idEquals. Como vamos retornar apenas um
			//resultado, devemos indicar com o metodo uniqueResult().

		} finally {
			//Fechamos a sessão
			close();
		}
    }

Na próxima listagem (Listagem 8) vamos fazer 2 consultas pelo campo nome. A primeira consulta é com o metodo findPessoaByNome(), o qual irá retorna apenas uma pessoa com o nome informado. A segunda, do metodo findPessoasByNome(), retornará uma lista de pessoas que contenham em seu nome a palavra “Maria”. Usamos nesse caso a classe abstrata MatchMode como parametro para informar o criterio da busca. Essa classe possui os seguintes criterios: 

  • EXACT (busca um nome exato); 
  • START (busca todos que iniciam com o mesmo nome); 
  • END (busca todos que tenham o mesmo nome como último nome); 
  • ANYWHERE (busca todos que tenham o mesmo nome em qualquer parte de seu nome).
Listagem 8. Consulta Parte 2.
// Inserir no metodo main()

	// busca uma pessoa pelo nome completo
	Pessoa p2 = dao.findPessoaByNome("Marcio Olacco");
	System.out.println("findPessoaByNome: " + p2.toString());

//Inserir na classe PessoaDao

    // Busca uma pessoa por seu nome completo
    public Pessoa findPessoaByNome(String nome) {
        try {
            return (Pessoa) getSession().createCriteria(Pessoa.class)
                .add(Restrictions.eq("nome", nome)).uniqueResult();

         //O metodo usado agora é o eq(). Precismos informa 2 parametros
         //para ele. O 1° é o atributo (nome) declarado na classe Pessoa.
         //Nao confunda com o campo da tabela, deve ser o atributo.  
         //O 2° parametro é o recebido pelo metodo, que contem o nome usado

        } finally {
            close();
        }
    }

// Inserir no metodo main()

        // busca todas pessoas que tenham Maria como nome
        List p3 = dao.findPessoasByNome("Maria");
        for (Pessoa p : p3)
            System.out.println("findPessoasByNome: " + p.toString());

//Inserir na classe PessoaDao

	// Busca varias pessoas que possuem um mesmo nome
	public List findPessoasByNome(String nome) {
		try {
			return getSession().createCriteria(Pessoa.class)
				.add(
					Restrictions.like("nome", nome, MatchMode.ANYWHERE)
				).list();
				
				//Desta vez usamos o metodo like().
				//Ele possui 3 parametros. O 1° e o 2°
				//Já sabemos para que serve, o 3° serve para
				//informarmos que queremos localizar todos os
				//nomes que contenham a palavra Maria em qualquer lugar.
				//e por fim retornamos uma lista.
		} finally {
			close();
		}
    }

Na Listagem 9 vamos fazer uma busca usando o criterio between. Esse é o mesmo criterio existente nas consultas SQL, que testa valores dentro de duas condições. Nesse exemplo vamos pesquisar por pessoas que estejam dentro de um padrão de idade. Também usaremos um criterio de ordenação, semelhante ao “order by” do SQL. Nessa consulta a ordenação será primeiro por idade, caso tenham pessoas com idades iguais, vamos então ordená-las por nome. O metodo asc() ordena de “0-9 ou a-z”, o metodo desc() ordena de “9-0 ou z-a”.

Listagem 9. Consulta Parte 3.
// Inserir no metodo main()

        // busca todas pessoas com idade entre 25 e 40
        List p4 = dao.findPessoasByIdade(25, 40);
        for (Pessoa p : p4)
            System.out.println("findPessoasByIdade: " + p.toString());

//Inserir na classe PessoaDao

    // Busca todas pessoas com idade entre 25 e 40 anos
    // Ordena a lista por idade (da menor a maior)
    // Se existir mais de uma pessoa com a mesma idade ordena por nome (a-z)
    public List findPessoasByIdade(int first, int last) {
        try {
			return getSession().createCriteria(Pessoa.class)
					.add(Restrictions.between("idade", first, last))
					.addOrder(Order.asc("idade"))
					.addOrder(Order.asc("nome"))
					.list();
        } finally {
            close();
        }
    }

Na Listagem 10 vamos criar uma busca usando como criterio principal a idade da pessoa. Depois uma nova condição será inserida. Essa condição irá usar o OR do SQL. Então se uma pessoa com 19 anos for encontrada, testará se ela possui “Maria” como 1° nome. Se Nao existir nenhuma “Maria” com 19 anos, busca uma pessoa com 19 anos e que tenha “Fernandes” como último nome.

Listagem 10. Consulta Parte 4.
// Inserir no metodo main()

        // Busca todas pessoas com idade = 19
        // E primeiro nome = Maria OU ultimo nome = Fernandes
        List p5 = dao.findPessoasByNomeAndIdade("Maria", "Fernandes", 19);
        for (Pessoa p : p5)
            System.out.println("findPessoasByNomeAndIdade: " + p.toString());

//Inserir na classe PessoaDao
    // Busca todas pessoas com idade = 19
    // E primeiro nome = Maria OU ultimo nome = Fernandes
    public List findPessoasByNomeAndIdade(String firstName,
                                                  String lastName,
                                                  int idade)
    {
        try {
            return getSession().createCriteria(Pessoa.class)
                    .add(Restrictions.eq("idade", idade))
                    .add(Restrictions.or(
                            Restrictions.like("nome", firstName, MatchMode.START),
                            Restrictions.like("nome", lastName, MatchMode.END)
                    )).list();
        } finally {
            close();
        }
    }

Agora na Listagem 11 criterios semelhantes aos encontrados no SQL serão usados. São eles:

  • MAX (que retorna o maior ou ultimo ID);
  • MIN (retorna o menor);
  • SUM (que executa uma soma);
  • AVG (retorna a média).

Dessa fez ao invés de utilizar o metodo add() e os metodos da classe Restrictions, vamos usar o metodo setProjection() e os metodos da classe Projections. Outra diferença encontrada nessa busca é que criamos um objeto Criteria e a partir dele fazemos todas as consultas.

Listagem 11. Consulta Parte 5.
// Inserir no metodo main()

        dao.findMediaIdade();

//Inserir na classe PessoaDao

    public void findMediaIdade() {
        Criteria criteria = getSession().createCriteria(Pessoa.class);
        try {
            Long maxId =
                    (Long) criteria.setProjection(Projections.max("id"))
                            .uniqueResult();

            System.out.println("Ultimo ID cadastrado: " + maxId);

            Integer minIdade =
                    (Integer) criteria.setProjection(Projections.min("idade"))
                            .uniqueResult();

            System.out.println("A menor idade: " + minIdade);

            Integer maxIdade =
                    (Integer) criteria.setProjection(Projections.max("idade"))
                            .uniqueResult();

            System.out.println("A maior idade: " + maxIdade);

            Long somaIdades =
                    (Long) criteria.setProjection(Projections.sum("idade"))
                            .uniqueResult();

            System.out.println("A soma das idade: " + somaIdades);

            Double mediaIdade =
                    (Double) criteria.setProjection(Projections.avg("idade"))
                            .uniqueResult();

            System.out.printf("Média de idade é: " + "%.0f", mediaIdade);
        } finally {
            close();
        }
    }

Na Listagem 12 serão usados 2 novos metodos, o setFirstResult() e o setMaxResults(). O 1° tem como função receber o 1° registro que deve ser retornado. Se você tem uma lista com 100 registros e mandar localizar todos, terá os 100 registros exibidos. Caso faça a mesma busca, mas utilize o setFirstResult(50), a lista será composta a partir do registro 50, por que ele foi setado como sendo o 1° registro que queremos. Sendo assim, serão exibidos apenas 50 registros dos 100 (do 50 ao 100).

setMaxResults() tem como função limitar o número de registros que devem ser retornados. Se na busca dos 100 registros você setar setMaxResults(10), ela irá retornar apenas 10 registros. Esse tipo de busca é muito usando em páginas web. Quando entramos em um site de vendas e localizamos alguns produtos, vários são listados na tela. Alguns sites têm a opção em que o usuário escolhe quantos registros deseja exibir por tela.

Quando usamos o setFirstResult() e o setMaxResults() juntos, podemos fazer algo como a paginação. Se a busca retorna 100 registros, podemos listar de 10 em 10 por página. O setFirstResult() seria para informar a partir de qual registros vamos listar e o setMaxResults() quantos serão listados por pagina.

Listagem 12. Consulta Parte 6.
// Inserir no metodo main()

        int first = 0, max = 5;
        while (max == 5) {
            List p6 = dao.findPessoasByLimit(first, max);
            System.out.println("findPessoasByLimit: " + p6);
            first = first + max;
            max = p6.size();
        }

	//Inserir na classe PessoaDao

    // Busca por criterios de paginacao
    public List findPessoasByLimit(int first, int max) {
        try {
            return getSession().createCriteria(Pessoa.class)
                    //seta o maximo de registro por consulta (ex: 5)
                    .setMaxResults(max)
                    //seta quais resgistos serão retornados (0-5, 5-10, 10-15...)
                    .setFirstResult(first)
                    .list();
        } finally {
            close();
        }
    }

O código da Listagem 12 funciona da seguinte forma: temos um valor first e um valor max. Quando fazemos a chamada ao metodo findPessoasByLimit(), passamos 0 e 5 como parametro da 1ª vez. A busca que será feita irá retornar os primeiros 5 resultados, o 1° é o 0 (zero) , que para tabela equivale ao id = 1. O último é o 4, que para a tabela é o id = 5. Como estamos em um while(), a variável first é incrementada, recebendo seu próprio valor mais o valor da variável max. Assim, teríamos (0 = 0 + 5 ) que é igual a 5. Então quando a nova busca for realizada, o first será 5 e o max será 5. Então o primeiro registro encontrado seria o de posição 5, que na tabela seria o de id = 6, até o registro 10. E assim por diante no loop.

while() será encerrado quando a lista retornar, um número de registros, menor do que o do valor da variável max. Isso significa que a busca chegou ao fim da tabela.

Encerramos aqui a primeira parte do artigo sobre a API Criteria. No próximo artigo irei demonstrar como realizar consultas entre tabelas com relacionamento 1-N e relacionamento tipo N-N.

Saiba mais

Ballem

Marcio Ballem é bacharel em Sistemas de Informação pelo Centro Universitário Franciscano em Santa Maria/RS. Tem experiência com desenvolvimento Delphi e Java em projetos para gestão pública e acadêmica. Possui certificação em Java, OCJP 6.

Você pode gostar...