Hibernate – Projections com API Criteria

A várias postagens atrás, mais especificamente no ano de 2012, foram postados dois tutorias específicos sobre consultas com a API Criteria do Hibernate Framework. Os tutorias Consultas com Hibernate e a API Criteria, partes [1] e [2] demonstraram exemplos de consultas de vários tipos e níveis usando a API Criteria. Dentre estas consultas algumas abordaram exemplos simples sobre o uso de Projeções (Projections).

Neste tutorial irei postar alguns métodos demonstrando um pouco mais sobre o uso de projeções. Consultas que na época não foram abordadas. Para quem não sabe qual a função das projeções na API Criteria é possível fazer uma relação com as funções do SQL do tipo MAX, MIN, COUNT, DISTINCT, GROUP BY, SUN e AVG.

O tutorial foi escrito utilizando o banco de dados MySQL, o Hibernate 4.3.4.Final e o Maven como gerenciador das dependências. O projeto estará disponível para download no final do tutorial através do GitHub.

1. Banco de dados

A base de dados abaixo será usada como exemplo para a execução das consultas.  Um script com os dados a serem inseridos na tabela estão disponíveis junto ao projeto no Github.

+----+----------------+---------+---------------+--------+-------+-----------------+
| ID | CITY           | COUNTRY | FIRST_NAME    | SALARY | STATE | SURNAME         |
+----+----------------+---------+---------------+--------+-------+-----------------+
|  1 | Porto Alegre   | Brasil  | Ana Maria     |   1500 | RS    | de Souza        |
|  2 | Florianopolis  | Brasil  | Marta         |   1250 | SC    | Soares          |
|  3 | Campinas       | Brasil  | Julio Cesar   |   3100 | SP    | da Silva        |
|  4 | Rio de Janeiro | Brasil  | Pedro         |   3500 | RJ    | Fagundes        |
|  5 | Belo Horizonte | Brasil  | Maria         |   2750 | MG    | de Souza Castro |
|  6 | Santa Maria    | Brasil  | Ana Lucia     |   3500 | RS    | Pereira         |
|  7 | Porto Alegre   | Brasil  | Cristiano     |   1990 | RS    | Martins         |
|  8 | Rio de Janeiro | Brasil  | Carlos Andre  |   2600 | RJ    | da Silva        |
|  9 | Sao Paulo      | Brasil  | Mirian        |   2420 | SP    | Gouveia         |
| 10 | Porto Alegre   | Brasil  | Marco Antonio |   1500 | RS    | de Souza        |
| 11 | Santos         | Brasil  | Janaina       |   1250 | SP    | Soares          |
| 12 | Campinas       | Brasil  | Maria Rita    |   3550 | SP    | da Silva        |
| 13 | Rio de Janeiro | Brasil  | Guilherme     |   3500 | RJ    | Santos Fagundes |
| 14 | Ipatinga       | Brasil  | Mirdes        |   2500 | MG    | Castro Pereira  |
| 15 | Porto Alegre   | Brasil  | Victor        |   3500 | RS    | Ramos Neves     |
| 16 | Porto Alegre   | Brasil  | Cristian      |   1990 | RJ    | Vidal Silva     |
| 17 | Rio de Janeiro | Brasil  | Andre         |   2600 | RJ    | Matos Mello     |
| 18 | Sao Paulo      | Brasil  | Matilda       |   1990 | SP    | Gouveia Campos  |
+----+----------------+---------+---------------+--------+-------+-----------------+
18 rows in set (0.00 sec)

2. Classe de entidade Employee

Na Listagem 1 veremos a classe de entidade mapeada por meio de anotações. Os atributos desta classe representam as colunas da tabela Employees no banco de dados. Quando usamos a API Criteria para realizar as consultas devemos informar o nome do atributo como parâmetros e não o nome das colunas do banco de dados.

Listagem 1. Classe Employee.
package com.mballem.tutorial.entity;

import javax.persistence.*;
import java.io.Serializable;

/**
 * User: Marcio Ballem
 * Date: 10/03/14
 * Time: 18:47
 * http://www.mballem.com
 */
@Entity
@Table(name = "EMPLOYEES")
public class Employee implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private Long id;
    @Column(name = "FIRST_NAME")
    private String firstName;
    @Column(name = "SURNAME")
    private String surname;
    @Column(name = "SALARY")
    private Integer salary;
    @Column(name = "COUNTRY")
    private String country;
    @Column(name = "STATE")
    private String state;
    @Column(name = "CITY")
    private String city;

    public Employee() {
        super();
    }

    // gere os métodos get/set, equals/hashCode e toString.    
}

A partir deste ponto irei demonstrar algumas consultas possíveis de se executar usando projeções. Para cada consulta será postado o código Java, o resultado da consulta sobre a tabela Employees e também como essa consulta seria em SQL. Vamos começar com as consultas mais básicas em projeções. Nestas consultas veremos como utilizar funções do tipo SUM, AVG, COUNT, etc.

Na Listagem 2 temos um exemplo de consulta em SQL que retorna o maior valor de salário encontrado na tabela Employees. Veja agora na Listagem 3 a mesma consulta porém desta fez em Java utilizando Criteria com Projections. Na consulta em Java devemos ter um retorno, que neste caso será um objeto do tipo org.hibernate.Criteria. Esta classe fornece acesso ao método setProjection() o qual deverá receber como parâmetro a projeção que será utilizada, neste caso, será do tipo MAX.

Listagem 2. Consulta SQL com operador MAX.
select
   max(SALARY) 
from
   EMPLOYEES

Os métodos de projeção são disponibilizados pela classe org.hibernate.criterion.Projections e são do tipo estáticos, não precisando de uma instancia para invoca-los, como demonstrado na Listagem 3. No método max() devemos passar como parâmetro a String que representa o nome do atributo da classe Employee que mapeou a coluna SALARY da tabela Employees.

Listagem 3. Consulta Java com operador MAX
Criteria max = getSession().createCriteria(Employee.class)
                .setProjection(Projections.max("salary"));

System.out.println("The highest salary : " + max.uniqueResult());

O resultado desta consulta pode ser observado a seguir:

The highest salary : 3550

Outros métodos como: min(), para encontrar o menor salário; sum() para somar todos os salários; avg() para retornar a média entre todos os salários e rowCount() para retornar o número de linhas da tabela estão descritos na Listagem 4.

Listagem 4. Demais métodos.
Criteria min = getSession().createCriteria(Employee.class)
                .setProjection(Projections.min("salary"));
System.out.println("The lowest salary: " + min.uniqueResult());

Criteria sum = getSession().createCriteria(Employee.class)
                .setProjection(Projections.sum("salary"));
System.out.println("Sum of salaries: " + sum.uniqueResult());

Criteria avg = getSession().createCriteria(Employee.class)
                .setProjection(Projections.avg("salary"));
System.out.println("Average of salaries: " + avg.uniqueResult());

Criteria rows = getSession().createCriteria(Employee.class)
                .setProjection(Projections.rowCount());
System.out.println("Total of rows: " + rows.uniqueResult());

Os resultados destas consultas podem ser observados a seguir, juntamente com os SQL’s referentes a cada consulta:

select
     min(SALARY)
from
     EMPLOYEES 
--The lowest salary: 1250

select
     sum(SALARY) 
from
     EMPLOYEES 
--Sum of salaries: 44990

select
     avg(SALARY) 
from
     EMPLOYEES 
--Average of salaries: 2499.4444

select
     count(*) 
from
     EMPLOYEES 
--Total of rows: 18

Nos exemplos anteriores, cada consulta em Java utilizou apenas uma única projeção. É possível adicionar, entretanto, uma lista de projeções dentro da mesma consulta. Um projeção que será bastante útil será o método groupProperty(), com função idêntica a função GroupBy do SQL.

Listagem 5. SQL com Group By.
select
         count(*),
         max(SALARY),
         min(SALARY),
         sum(SALARY),
         avg(SALARY),
         STATE
from
         EMPLOYEES
group by
         STATE

No SQL apresentado na Listagem 5 temos uma consulta que agrupa valores pela coluna STATE. Como retorno será exibido o maior, o menor, a soma, a média e a quantidade de linhas que contém um valor de salário para cada estado:

+----------+-------------+-------------+-------------+-------------+-------+
| count(*) | max(SALARY) | min(SALARY) | sum(SALARY) | avg(SALARY) | STATE |
+----------+-------------+-------------+-------------+-------------+-------+
|        2 |        2750 |        2500 |        5250 |   2625.0000 | MG    |
|        5 |        3500 |        1990 |       14190 |   2838.0000 | RJ    |
|        5 |        3500 |        1500 |       11990 |   2398.0000 | RS    |
|        1 |        1250 |        1250 |        1250 |   1250.0000 | SC    |
|        5 |        3550 |        1250 |       12310 |   2462.0000 | SP    |
+----------+-------------+-------------+-------------+-------------+-------+
5 rows in set (0.00 sec)

Veja no exemplo da Listagem 6, como transcrever a consulta SQL da Listagem 5 em uma consulta usando os métodos rowCount(), max(), min(), sum(), avg() e groupProperty(). Em relação as consultas anteriores, o objeto de retorno não será um objeto org.hibernate.Criteria, mas sim um objeto do tipo java.util.List. Isto porque agora o retorno poderá conter vários resultados e não apenas um único valor.

Para ter acesso ao retorno, será necessário realizar um for() no objeto que recebe o retorno da consulta (List results). Cada linha do objeto List contém um objeto array, referente a cada agrupamento, neste caso, a cada Estado. Sendo assim, para obter os resultados precisamos transformar cada linha da lista em um objeto do tipo array e depois acessar cada posição deste array. Cada posição do array equivale a um dos métodos usados na projeção, a não ser groupProperty().

Listagem 6. Projeção com Group By.
List results = 
	 getSession().createCriteria(Employee.class)
			 .setProjection(
				Projections.projectionList()
				   .add(Projections.rowCount())
				   .add(Projections.max("salary"))
				   .add(Projections.min("salary"))
				   .add(Projections.sum("salary"))
				   .add(Projections.avg("salary"))
				   .add(Projections.groupProperty("state"))
				   ).list();

for (Object objects : results) {
         Object[] o = (Object[]) objects;
         System.out.println("------------ "+ o[5] +" -------------");
         System.out.println("Number of Employees: " + o[0]);
         System.out.println("The highest salary: " + o[1]);
         System.out.println("The lowest salary: " + o[2]);
         System.out.println("Sum of salaries: " + o[3]);
         System.out.println("Average of salaries: " + o[4]);
}

Vamos usar a mesma consulta das Listagens 5 e 6, porém desta vez iremos adicionar uma ordenação decrescente pela coluna STATE. Observe primeiramente a consulta em SQL – Listagem 7 – e em seguida o resultado apresentado.

Listagem 7. SQL com Order By.
select
         count(*),
         max(SALARY),
         min(SALARY),
         sum(SALARY),
         avg(SALARY),
         STATE
from
         EMPLOYEES
group by
         STATE
order by
        STATE desc
+----------+-------------+-------------+-------------+-------------+-------+
| count(*) | max(SALARY) | min(SALARY) | sum(SALARY) | avg(SALARY) | STATE |
+----------+-------------+-------------+-------------+-------------+-------+
|        5 |        3550 |        1250 |       12310 |   2462.0000 | SP    |
|        1 |        1250 |        1250 |        1250 |   1250.0000 | SC    |
|        5 |        3500 |        1500 |       11990 |   2398.0000 | RS    |
|        5 |        3500 |        1990 |       14190 |   2838.0000 | RJ    |
|        2 |        2750 |        2500 |        5250 |   2625.0000 | MG    |
+----------+-------------+-------------+-------------+-------------+-------+
5 rows in set (0.00 sec)

Utilizando a ordenação mudamos a ordem de apresentação dos resultados. Para adicionar a ordenação na consulta Java com projeções, vamos informar através do método as() um alias para o atributo STATE. Este alias pode ser qualquer nome, neste caso, foi usado orderBy. Em seguida, fora do método setProjection() adicionamos no método addOrder() a ordenação DESC ou ASC e o alias referente ao atributo que vamos usar para ordenar os resultados. Confira a consulta na Listagem 8.

Listagem 8. Projeção com Order By.
List results = 
	 getSession().createCriteria(Employee.class)
			 .setProjection(
				Projections.projectionList()
				   .add(Projections.rowCount())
				   .add(Projections.max("salary"))
				   .add(Projections.min("salary"))
				   .add(Projections.sum("salary"))
				   .add(Projections.avg("salary"))
				   .add(Projections.groupProperty("state").as("byOrder"))
				).addOrder(Order.desc("byOrder")).list();

for (Object objects : results) {
	 Object[] o = (Object[]) objects;
	 System.out.println("------------ "+ o[5] +" -------------");
	 System.out.println("Number of Employees: " + o[0]);
	 System.out.println("The highest salary: " + o[1]);
	 System.out.println("The lowest salary: " + o[2]);
	 System.out.println("Sum of salaries: " + o[3]);
	 System.out.println("Average of salaries: " + o[4]);
}

Vamos agora executar uma consulta que retorne a linha do empregado que possui o maior salário em cada Estado. Para isso, usamos a função MAX(SALARY) e agrupamos o resultado pela coluna STATE, conforme Listagem 9.

Listagem 9. SQL para localizar maior salário por Estado.
select
         FIRST_NAME,
         SURNAME,
         CITY,
         STATE,
         max(SALARY),
         STATE
from
         EMPLOYEES
group by
         STATE

O resultado da operação do SQL da Listagem 10 será este:

+-------------+-----------------+----------------+-------+-------------+-------+
| FIRST_NAME  | SURNAME         | CITY           | STATE | max(SALARY) | STATE |
+-------------+-----------------+----------------+-------+-------------+-------+
| Maria       | de Souza Castro | Belo Horizonte | MG    |        2750 | MG    |
| Pedro       | Fagundes        | Rio de Janeiro | RJ    |        3500 | RJ    |
| Ana Maria   | de Souza        | Porto Alegre   | RS    |        3500 | RS    |
| Marta       | Soares          | Florianopolis  | SC    |        1250 | SC    |
| Julio Cesar | da Silva        | Campinas       | SP    |        3550 | SP    |
+-------------+-----------------+----------------+-------+-------------+-------+
5 rows in set (0.00 sec) 

Para transcrever o SQL da Listagem 9 em Criteria, usando Projections, implementamos o código conforme a Listagem 10. Neste caso, temos o método property() para que seja retornado o valor do atributo, correspondente a coluna na tabela Employees. O método max() seleciona o maior salário entre o grupo de cada Estado. Desta forma, o retorno será o mesmo obtido através da consulta SQL.

Listagem 10. Projeção usando property.
List results = 
	 getSession().createCriteria(Employee.class)
		 .setProjection(
			Projections.projectionList()
			   .add(Projections.property("firstName"))
			   .add(Projections.property("surname"))
			   .add(Projections.property("city"))
			   .add(Projections.property("state"))
			   .add(Projections.max("salary"))
			   .add(Projections.groupProperty("state"))
			).list();

for (Object objects : results) {
         Object[] o = (Object[]) objects;
         System.out.println("-------------------------------------");
         System.out.println("Name: " + o[0] + " " + o[1]);
         System.out.println("City: " + o[2] + ", " + o[3]);
         System.out.println("The highest salary: " + o[4]);
} 

A consulta SQL apresentada na Listagem 11 irá ter como retorno cada valor de salário e a quantidade de linhas que possuem esse valor.

Listagem 11. SQL agrupando por valor de salarios.
select
         SALARY,
         count(SALARY)
from
         EMPLOYEES  
group by
         SALARY

Veja a seguir o resultado:

+--------+---------------+
| SALARY | count(SALARY) |
+--------+---------------+
|   1250 |             2 |
|   1500 |             2 |
|   1990 |             3 |
|   2420 |             1 |
|   2500 |             1 |
|   2600 |             2 |
|   2750 |             1 |
|   3100 |             1 |
|   3500 |             4 |
|   3550 |             1 |
+--------+---------------+
10 rows in set (0.00 sec)
Listagem 12. Projeção para contar a quantidade de cada salário.
List results = 
	 getSession().createCriteria(Employee.class)
		 .setProjection(
			Projections.projectionList()
			   .add(Projections.count("salary"))
			   .add(Projections.groupProperty("salary"))
			).list();

for (Object objects : results) {
	 Object[] o = (Object[]) objects;
	 System.out.println("-------------------------------------");
	 System.out.println("Salary equal to " + o[1] + ": " + o[0]);
}

O último exemplo a ser demonstrado neste tutorial será com a função DISTINCT. Essa função tem o objetivo de não repetir, no retorno de uma consulta, linhas que possuem colunas com os mesmos valores. No exemplo do SQL da Listagem 13, vamos usar o DISTINCT na coluna SALARY. Assim, mesmo que na tabela existam linhas com o mesmo valor de salário, apenas uma linha será retornada com tal valor.

Listagem 13. SQL usando distinct.
select
         distinct SALARY,
         STATE 
from
         EMPLOYEES  
order by
         STATE asc

A seguir temos o resultada da consulta da Listagem 13. Observe que apenas 14 linhas foram retornadas, enquanto a tabela Employees possui 18 linhas. Isso porque, 4 destes valores são valores repetidos na coluna SALARY.

+--------+-------+
| SALARY | STATE |
+--------+-------+
|   2500 | MG    |
|   2750 | MG    |
|   1990 | RJ    |
|   2600 | RJ    |
|   3500 | RJ    |
|   1500 | RS    |
|   1990 | RS    |
|   3500 | RS    |
|   1250 | SC    |
|   1250 | SP    |
|   1990 | SP    |
|   2420 | SP    |
|   3100 | SP    |
|   3550 | SP    |
+--------+-------+
14 rows in set (0.00 sec)  

Na Listagem 14 temos a consulta anterior escrita com Criteria e Projections. No método distinct(), passamos como parâmetro a propriedade salary. Em seguida informamos que queremos o retorno dos estados, através da propriedade state. E por fim ordenamos o resultado por Estado.

Listagem 14. Projeção usando distinct.
List results = 
	 getSession().createCriteria(Employee.class)
		  .setProjection(
			Projections.projectionList()
				.add(Projections.distinct(Projections.property("salary")))
				.add(Projections.property("state"))
		  ).addOrder(Order.asc("state")).list();

for (Object objects : results) {
	 Object[] o = (Object[]) objects;
	 System.out.println("-------------------------------------");
	 System.out.println("Salaries in " + o[1] + " state: " + o[0]);
}

Saiba mais

Download pelo Github

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...