martes, 21 de junio de 2011

JPA V - Named Queries & Native Queries

La idea de esta quinta entrega del tutorial de JPA, es que el lector pueda entender el concepto de queries con nomres o estaticas y queries nativas, como asi tambien, poder ver las consultas SQL que se arrojan por detras para terminar de entender su funcionamiento, como venimos viendo en los anteriores tutoriales.

Temas a tratar:
  • Named Queries (consultas con nombres)
  • Native Queries (consultas nativas)
    • Native Query mapeando el resultado a una entidad
    • Named Native Query mapeando el resultado a una entidad
    • Scalar Query mapeando el resultado a un listado de array de objeto
    • Non-managed Entities Query mapeando el resultado a un listado de POJOs simples
      • Transformer

Seguimos como lo venimos haciendo en las entregas anteriores, con los mismos ejemplos de tablas  y datos. Para mas detalle de como son la relaciones entre las tablas Ver tutorial Tutorial JPA I - Manejo de relaciones

TABLA CUSTOMER
 
 CUSTOMER_ID     FIRST_NAME     GENDER     LAST_NAME     NAME1     NAME2     REFEREE_ID    
 --------------  -------------  ---------  ------------  --------  --------  -------------
 1               First name 1   FEMALE     Last name 1   Damian    (null)    1             
 2               First name 2   FEMALE     Last name 2   (null)    (null)    2             
 3               First name 3   FEMALE     Last name 3   (null)    (null)    1             
 4               First name 4   MALE       Last name 4   (null)    (null)    (null)       


TABLA REFEREE

 ID     COMMENTS     NAME     
 -----  -----------  ---------
 1      comments 1   Referee 1
 2      comments 2   Referee 2 


TABLA ACCOUNTS

 ACCOUNT_TYPE     ACCOUNT_ID     BALANCE     DATEOPENED               OVERDRAFTLIMIT    INTERESTRATE    CUSOMTER_ID   
 ---------------                -------------              ----------           ---------------------                 -----------------               ---------------               --------------
 C                              1                        10                 6/21/2011 12:00:00 AM       0                                (null)                        1             
 C                              2                        20                 (null)                                    0                                (null)                        1             
 S                              3                        100                6/21/2011 12:00:00 AM       (null)                          0                              1        


TABLA CUSTOMER_ADDRESS

 CUST_ID     ADD_ID   
 ----------         --------
 1                     2        
 1                     0      


TABLA ADDRESS
 
 ADDRESS_ID     ADDRESSLINE     COUNTRY     POSTCODE   
 -------------             --------------              ----------            -----------
 2                           addressLine            ARGENTINA  1428       
 0                           addressLine2          BRASIL          1629       
 
  

Named Queries

Las named queries o consultas con nombre son diferentes de las sentencias dinamicas que hemos visto hasta ahora en los anteriores tutoriales, en el sentido en que no pueden cambiar: son leidas y transformadas ensentencias SQL cuando el programa arranca en lugar de cada vez que son ejecutadas. 
Las named queries son predefinidas usando la notacion @NamedQuery o @NamedQueries en la definicion de la entidad (clase java)
Este comportamiento estatico las hace mas eficientes y por lo tanto ofrecen un mejor rendimiento. 
El alcance de cada named query es el contexto de persistencia actual, por lo tanto el nombre  de cada query sera unico bajo el mismo contexto de persistencia

En la entidad Customer agregamos la definicion del namedQuery:

@NamedQuery(name="findSelectedCustomers",
             query = "SELECT c FROM Customer c 
                       WHERE c.firstName LIKE :firstString 
                             AND c.lastName LIKE :secondString"
           )
@Entity
public class Customer implements Serializable{
 ....
.....
}

Luego armamos la consulta utilizando el metodo createNamedQuery del EntityManager.

Query query = em.createNamedQuery("findSelectedCustomers")
            .setParameter("firstString", "First name 1")
            .setParameter("secondString", "Last name 1"); 

List customers = query.getResultList();
for (Customer customer : customers) {
  log.debug("CUSTOMER " + customer.getFirstName() + " " + customer.getLastName());
}


La traza SQl es la siguiente:

Hibernate:
    select
        customer0_.CUSTOMER_ID as CUSTOMER1_2_,
        customer0_.FIRST_NAME as FIRST2_2_,
        customer0_.GENDER as GENDER2_,
        customer0_.LAST_NAME as LAST4_2_,
        customer0_.referee_id as referee7_2_,
        customer0_.name1 as name5_2_,
        customer0_.name2 as name6_2_
    from
        Customer customer0_
    where
        (
            customer0_.FIRST_NAME like ?
        )
        and (
            customer0_.LAST_NAME like ?
        )
Hibernate:
    select
        referee0_.id as id0_0_,
        referee0_.comments as comments0_0_,
        referee0_.name as name0_0_
    from
        Referee referee0_
    where
        referee0_.id=?
Hibernate:
    select
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_0_,
        accounts0_.balance as balance1_0_,
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_0_,
        accounts0_.dateOpened as dateOpened1_0_,
        accounts0_.overdraftLimit as overdraf5_1_0_,
        accounts0_.interestRate as interest6_1_0_,
        accounts0_.ACCOUNT_TYPE as ACCOUNT1_1_0_
    from
        Account accounts0_
    where
        accounts0_.CUSOMTER_ID=?


El resultado de la ejecucion de la query es lo siguiente:

CUSTOMER First name 1 Last name 1

Ahora vamos a ver como es la anotacion en el caso que querramos tener mas de una named query en la entidad

En la entidad Account agregamos la definicion del namedQuery

@NamedQueries({
    @NamedQuery(name="findAccountsWithDateOpened",
                query = "SELECT a FROM Account a
                         WHERE a.dateOpened IS NOT NULL"
               ),
    @NamedQuery(name="findAccountsWithOutDateOpened",
                query = "SELECT a FROM Account a
                         HERE a.dateOpened IS NULL"
               )
})
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="ACCOUNT_TYPE")
public abstract class Account implements Serializable{
.......
.......
}
Query query1 = em.createNamedQuery("findAccountsWithDateOpened");
List accounts = query1.getResultList();
for (Account a : accounts) {
  log.debug("ACCOUNT ID: " + a.getId() + " dateOpened " + a.getDateOpened());
}

La traza SQL es la siguiente:

Hibernate:
    select
        account0_.ACCOUNT_ID as ACCOUNT2_1_,
        account0_.balance as balance1_,
        account0_.CUSOMTER_ID as CUSOMTER7_1_,
        account0_.dateOpened as dateOpened1_,
        account0_.overdraftLimit as overdraf5_1_,
        account0_.interestRate as interest6_1_,
        account0_.ACCOUNT_TYPE as ACCOUNT1_1_
    from
        Account account0_
    where
        account0_.dateOpened is not null
Hibernate:
    select
        customer0_.CUSTOMER_ID as CUSTOMER1_2_2_,
        customer0_.FIRST_NAME as FIRST2_2_2_,
        customer0_.GENDER as GENDER2_2_,
        customer0_.LAST_NAME as LAST4_2_2_,
        customer0_.referee_id as referee7_2_2_,
        customer0_.name1 as name5_2_2_,
        customer0_.name2 as name6_2_2_,
        accounts1_.CUSOMTER_ID as CUSOMTER7_4_,
        accounts1_.ACCOUNT_ID as ACCOUNT2_4_,
        accounts1_.ACCOUNT_ID as ACCOUNT2_1_0_,
        accounts1_.balance as balance1_0_,
        accounts1_.CUSOMTER_ID as CUSOMTER7_1_0_,
        accounts1_.dateOpened as dateOpened1_0_,
        accounts1_.overdraftLimit as overdraf5_1_0_,
        accounts1_.interestRate as interest6_1_0_,
        accounts1_.ACCOUNT_TYPE as ACCOUNT1_1_0_,
        referee2_.id as id0_1_,
        referee2_.comments as comments0_1_,
        referee2_.name as name0_1_
    from
        Customer customer0_
    left outer join
        Account accounts1_
            on customer0_.CUSTOMER_ID=accounts1_.CUSOMTER_ID
    left outer join
        Referee referee2_
            on customer0_.referee_id=referee2_.id
    where
        customer0_.CUSTOMER_ID=?


El resultado de la ejecucion de la consulta es lo siguiente:

ACCOUNT ID: 1 dateOpened 2011-06-23
ACCOUNT ID: 3 dateOpened 2011-06-23



Native Queries

Este tipo de consultas requiere una sentencia SQL en lugar de una  sentencia  JPQL.
Las consultas nativas SQL pueden ser definidas de manera estatica como las consultas con nombre (como vimos en el apartado anterior), obteniendo los mismos beneficios de eficiencia y rendimiento. A estas se las llaman Named Native Queries.
La  principal desventaja que tenemos al usar directametne SQL nativo es que se pierde la portabilidad sobre la cual fue pensado JPA. En cambio la ventaja que tiene sobre JDBC directo es que los resultados de las consultas SQL nativas pueden ser retornados como entidades

Los  resultados  se  podrian  mapear en:
  • Objetos del  tipo entidades
  • Valores escalares (en un listado de array de objetos)
  • Objetos POJOs simples (no entidades)

Para desarrollar queries nativas tendremos que armar la consulta utilizando el metodo createNativeQuery() de la interface javax.persistence.EntityManager (JPA) o bien utilizando el metodo createSQLQuery() de la interface org.hibernate.Session (que pertenece a la implementacion del ORM que en estos ejemplos es Hibernate)


Native Query (Query Nativa) mapeando el resultado en un objeto del tipo entidad

Tener en cuenta que en el SELECT tendremos que poner todos los campos de la tabla o en su defecto colocar asterisco (*). Ejemplo: SELECT * FROM CUSTOMER.
Un dato importante es que el resultado si o si tendra que ser mapeado a un objeto del tipo entidad y no a un simple POJO en esta forma de desarrollar queries nativas

Query query = em.createNativeQuery("SELECT CUSTOMER_ID, " +
                "                                   FIRST_NAME, " +
                "                                   LAST_NAME, " +
                "                                   GENDER, " +
                "                                   NAME1, " +
                "                                   NAME2, " +
                "                                   REFEREE_ID " +
                "                           FROM CUSTOMER", Customer.class); 
List customers = query.getResultList(); 
for (Customer customer : customers) { 
  log.debug("CUSTOMER " + customer.getFirstName() + " " + customer.getLastName()); 
}

La traza SQL es la siguiente: 

Hibernate:
    SELECT
        CUSTOMER_ID,
        FIRST_NAME,
        LAST_NAME,
        GENDER,
        NAME1,
        NAME2,
        REFEREE_ID       
    FROM
        CUSTOMER
Hibernate:
    select
        referee0_.id as id0_0_,
        referee0_.comments as comments0_0_,
        referee0_.name as name0_0_
    from
        Referee referee0_
    where
        referee0_.id=?
Hibernate:
    select
        referee0_.id as id0_0_,
        referee0_.comments as comments0_0_,
        referee0_.name as name0_0_
    from
        Referee referee0_
    where
        referee0_.id=?
Hibernate:
    select
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_0_,
        accounts0_.balance as balance1_0_,
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_0_,
        accounts0_.dateOpened as dateOpened1_0_,
        accounts0_.overdraftLimit as overdraf5_1_0_,
        accounts0_.interestRate as interest6_1_0_,
        accounts0_.ACCOUNT_TYPE as ACCOUNT1_1_0_
    from
        Account accounts0_
    where
        accounts0_.CUSOMTER_ID=?
Hibernate:
    select
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_0_,
        accounts0_.balance as balance1_0_,
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_0_,
        accounts0_.dateOpened as dateOpened1_0_,
        accounts0_.overdraftLimit as overdraf5_1_0_,
        accounts0_.interestRate as interest6_1_0_,
        accounts0_.ACCOUNT_TYPE as ACCOUNT1_1_0_
    from
        Account accounts0_
    where
        accounts0_.CUSOMTER_ID=?
Hibernate:
    select
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_0_,
        accounts0_.balance as balance1_0_,
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_0_,
        accounts0_.dateOpened as dateOpened1_0_,
        accounts0_.overdraftLimit as overdraf5_1_0_,
        accounts0_.interestRate as interest6_1_0_,
        accounts0_.ACCOUNT_TYPE as ACCOUNT1_1_0_
    from
        Account accounts0_
    where
        accounts0_.CUSOMTER_ID=?
Hibernate:
    select
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_,
        accounts0_.ACCOUNT_ID as ACCOUNT2_1_0_,
        accounts0_.balance as balance1_0_,
        accounts0_.CUSOMTER_ID as CUSOMTER7_1_0_,
        accounts0_.dateOpened as dateOpened1_0_,
        accounts0_.overdraftLimit as overdraf5_1_0_,
        accounts0_.interestRate as interest6_1_0_,
        accounts0_.ACCOUNT_TYPE as ACCOUNT1_1_0_
    from
        Account accounts0_
    where
        accounts0_.CUSOMTER_ID=?

El resultado de la ejecucion es lo siguiente: 


CUSTOMER First name 1 Last name 1
CUSTOMER First name 2 Last name 2
CUSTOMER First name 3 Last name 3
CUSTOMER First name 4 Last name 4 



Named Native Queries mapeando el resultado en un objeto del tipo entidad

En la entidad Address armamos el named query como lo venimos haciendo en los ejemplos anteriores con la unica diferencia que la query no esta hecha en JPQL sino que esta en SQL. Luego utilizamos la propiedad resultClass para saber contra que clase mapeara el resultado de esta named native query.
Tener en cuenta que en el SELECT tendremos que poner todos los campos de la tabla o en su defecto colocar asterisco (*). Ejemplo: SELECT * FROM ADDRESS.
Un dato importante es que el resultado si o si tendra que ser mapeado a un objeto del tipo entidad y no a un simple POJO en esta forma de desarrollar queries nativas.

@NamedNativeQuery(name="findAllAddress",
                  query = "SELECT ADDRESS_ID, 
                                 ADDRESSLINE, 
                                 COUNTRY, 
                                 POSTCODE 
                          FROM ADDRESS",                
                          resultClass=Address.class
                  )
@Entity
public class Address implements Serializable{
......
......

}
Query query1 = em.createNamedQuery("findAllAddress"); 
List address = query1.getResultList(); List
for (Address a : address) { 
  log.debug("ADDRESS ID " + a.getId() + " ADD LINE " + a.getAddressLine()); 
}

La traza SQL es la siguiente:

Hibernate:
    SELECT
        ADDRESS_ID,
        ADDRESSLINE,
        COUNTRY,
        POSTCODE
    FROM
        ADDRESS

El resultado de la query es lo siguiente:

ADDRESS ID 2 ADD LINE addressLine
ADDRESS ID 0 ADD LINE addressLine2



Scalar Queries (consultas escalares) mapeando el resultado a un listado de array de objetos (List<Object[]>)

Importante: Para utilizar valores escalares con queries nativas (Ojo q no es lo mismo que la proyeccion en JPQL ver Tutorial JPA III - HQL y JPA QL - Projection Query), en JPA se debera utilizar la anotacion @SqlResultSetMapping. Esta anotacion y los ejemplos pertinentes no sera vistos en este tutorial por considerarlos pocos practicos y muy engorrosos.

Ahora bien, para prceder a explicar este tema se utilizara directamente la implementacion del ORM elegido y en este caso, el ORM elegido es Hibernate.
Es decir, en lugar de utilizar la interfece javax.persistence.EntityManager (como lo venimos haciendo en el resto de los ejemplos) vamos a usar la interface org.hibernate.Session para llevar a cabo dichos ejemplos

Alternativa 1: La consulta nativa SQL más básica es obtener una lista de valores escalares

Session session = (Session) em.getDelegate(); 
SQLQuery sqlQuery = session.createSQLQuery("SELECT * FROM CUSTOMER"); 
List<Object[]> list = sqlQuery.list();
for (Object[] object : list) { 
  log.debug("CUSTOMER ID: " + object[0] + " FIRSTNAME: " + object[1]); 
}

Esta consulta retornara una lista de Array de objetos con los valores escarales de cada una de las columnas de la tabla Customer. Hibernate utilizara ResultSetMetadata para deducir el orden y el tipo de cada valores escalar retornado.

Alternativa 2: Obtener los valores escalares explicitando el tipo

Para evitar que Hibernate deduzca estos valores escalares o simplemente para ser mas explicitos a la hora de definir de que tipo sera los valores que sera retornados por la cosnutla es que utilzamos el metodo addScalar() de la interface SqlQuery

SQLQuery sqlQuery1 = session.createSQLQuery("SELECT * FROM CUSTOMER"); 
sqlQuery1.addScalar("CUSTOMER_ID", Hibernate.INTEGER); 
sqlQuery1.addScalar("FIRST_NAME", Hibernate.STRING); 
sqlQuery1.addScalar("GENDER", Hibernate.STRING); 
sqlQuery1.addScalar("LAST_NAME", Hibernate.STRING); 
sqlQuery1.addScalar("NAME1", Hibernate.STRING); 
sqlQuery1.addScalar("NAME2", Hibernate.STRING); 
sqlQuery1.addScalar("REFEREE_ID", Hibernate.INTEGER);

List<Object[]> list1 = sqlQuery1.list(); 
for (Object[] object : list1) {
 log.debug("CUSTOMER ID: " + object[0] + " FIRSTNAME: " + object[1]
);

La traza SQL para ambas alternativa es la siguiente:

Hibernate:
    SELECT
        *
    FROM
        CUSTOMER


El resultado es el siguiente:

CUSTOMER ID: 1 FIRSTNAME: First name 1 
CUSTOMER ID: 2 FIRSTNAME: First name 2 
CUSTOMER ID: 3 FIRSTNAME: First name 3 
CUSTOMER ID: 4 FIRSTNAME: First name 4


Non-managed entities mapeando el resultado a un listado de objetos POJOs simples

Es posible una transformacion a las consultas SQL nativas para retornar un listado de objetos POJOs que no son manejados por el contexto de persistencia de hiberante (no son objetos del tipo entidad) en lugar de retornar un listado de array de objetos (como el caso anterior).
Aqui tambien se veran los ejemplos utilizando directamente la interface de Hibernate "Session".

Session session = (Session) em.getDelegate();
SQLQuery sqlQuery2 = session.createSQLQuery("SELECT FIRST_NAME as firstName, 
" +                                                       LAST_NAME as lastName " + 
"                                                  FROM CUSTOMER");    
sqlQuery2.setResultTransformer(Transformers.aliasToBean(CustomerDef.class)); 
sqlQuery2.addScalar("firstName"); 
sqlQuery2.addScalar("lastName"); 
List<CustomerDef> list2 = sqlQuery2.list();
for (CustomerDef customerDef : list2) { 
   log.debug("CUSTOMER First Name: " + customerDef.getFirstName() + " Last Name: " + customerDef.getLastName()); 
}


Importante: El metodo addScalar() es requerido el cual hace el mapeo con el nombre de la propiedad del POJO  ya que la consulta retorna el nombre de las columnas en mayuscula (ej "FIRSTNAME") y si no utilizamos dicho metodo no se podran bindear los valores del SELECT con las propiedades del POJO.






Descargas de ejemplos de mapeos JPA y consultas JPQL

Por favor, haga clic en: Descargar ejemplos JPA


Bibliografia



* Java Persistence with Hibernate - Revised Edition fo Hiernate In Action - Christian Bauer and Gavin King - 2007

* EJB 3 Developer Guide - A Practical Guide for developers and architects to the Enterprise Java Beans
   Standard - Michael Sikora - 2008

1 comentario:

  1. Agradecemos el apoyo sobre estos temas a Sergio Jose Olijavetzky dada su larga trayectoria y experiencia en JPA

    ResponderEliminar