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