viernes, 30 de marzo de 2012

JPA CriteriaBuilder, Conjunction, Disjunction, in, like, between, greater Than, lower than

Hola a todos. Después de mucho tiempo sin publicar nada por acá, me gustaría compartir lo que últimamente he aprendido con las lecturas que he realizado y aplicado con éxito en el trabajo. La cuestión en este tema será como crear consultas dinámicas usando el api de persistencia de Java (por sus siglas en inglés JPA).
Para lograr dicho objetivo vamos a utilizar las clases EntityManager y CriteriaBuilder.
Bueno iniciemos con un pequeño ejemplo de cómo sería un método el cual nos retorne todos los registros de la tabla.

public List<EntityClass> getAll() {
     // Obtenemos una instancia de la clase, ya sea por medio de inversion de control o cualquier otro tipo de factoria
     EntityManager entityManager = getEntityManager();
     // Obtenemos el CriteriaBuilder
     CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
     // Indicamos que tipo de objetos esperamos en nuestra consulta
     CriteriaQuery<EntityClass> criteriaQuery = criteriaBuilder.createQuery(EntityClass.class);
     // Indicamos la entidad sobre la cual necesitamos crear la consulta
     Root<EntityClass> entityRoot = criteriaQuery.from(EntityClass.class);
     // retornamos una lista de la entidad especificada
     return entityManager.createQuery(criteriaQuery).getResultList();
}


Una consulta SQL equivalente sería un “SELECT entity FROM EntityClass AS entity”, y ese string pasarlo al entityManager y obtendríamos el mismo resultado.

En un caso como el anterior quizá uno llegue a plantearse la interrogante si es necesario usar el CriteriaBuilder para construir una consulta tan sencilla,….. Bueno a mi criterio si vale la pena. Ya que ese código es más fácil de mantener y en un futuro, mas fácil para escalar (Crear una clase abstracta la cual reciba como parámetro de clase el tipo de objeto el cual se va a manipular, no sé, algo así me puedo imaginar por el momento). Además usamos programación orientada a objetos lo cual es mejor que realizar una consulta tipo String.

La verdadera ventaja de usar CriteriaBuilder lo vamos a poder ver en casos en los cuales las consultas no son estáticas, óseas que dependiendo de algún tipo de parámetro enviado al método este agregara una restricción o añadirá una subquery a la búsqueda, entonces la consulta es creada en tiempo de ejecución, y es acá donde el uso de CriteriaBuilder nos facilita mucho el trabajo de construir nuestra consulta final.

Consideremos un ejemplo en el cual necesitamos que la consulta se cree en tiempo de ejecución; Tenemos una búsqueda de Usuarios (Entidad Usuario.class) en la cual le enviamos como parámetro la edad mínima y edad máxima que debe poseer un usuario para que sea tomado en cuenta en los criterios de la búsqueda, si la ambas edades no son proporcionadas, todos los usuarios de la base de datos serán retornados en el método, sin importar su edad. Tomando en cuenta que la entidad Usuario.class posee los métodos getter y setter para una propiedad edad tipo Integer, procedamos a realizar el método de la búsqueda.
    public List<Usuario> getAll(Integer edadMinima, Integer edadMaxima) {
        EntityManager em = getEntityManager();
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Usuario> criteriaQuery = cb.createQuery(Usuario.class);
        Root<Usuario> usuario = criteriaQuery.from(Usuario.class);
        // Creamos un predicado conjunction, que vienen a ser un tipo de restriccion en el query
        Predicate conjunction = cb.conjunction();
        if (edadMinima != null) {
            // agregamos una expresion al conjunction, greater than
            conjunction.getExpressions().add(cb.gt(usuario.get("edad").as(Integer.class), edadMinima));
        }
        if (edadMaxima != null) {
            // agregamos una expresion al conjunction, lower than
            conjunction.getExpressions().add(cb.lt(usuario.get("edad").as(Integer.class), edadMaxima));
        }
        // O Podemos usar la restriccion Between, la cuestion es aprender a usar la gran amplia gama de restricciones
        // que nos ofrece el CriteriaBuilder =D , ustedes eligen que restriccion se adecua a sus necesidades
        // if you know what i mean x)
        // Validamos que edad Minima y maxima != null
        if (edadMaxima != null && edadMinima != null) {
             conjuntion.getExpressions().add(cb.between(usuario.get("edad").as(Integer.class), edadMinima, edadMaxima)));
        }

        // si el conjunction posee mas de cero expresiones, entonces las agregamos al query con el metodo where
        if (conjunction.getExpressions().size() > 0) {
            criteriaQuery.where(conjunction);
        }
        // retornamos la lista filtrada
        return em.createQuery(criteriaQuery).getResultList();
    }


Bueno como dije anteriormente, el código es de fácil mantenimiento, lo que es igual a decir que es fácilmente entendimiento al lector, en este caso a un programador.


Lo primero, obtenemos una instancia de la clase EntityManager y con esta creamos un nuevo CriteriaBuilder, al CriteriaBuilder le indicamos que tipo de dato queremos que nos retorne la consulta, en este caso objeto Usuario (cb.createQuery(Usuario.class);). Luego al CriteriaQuery le indicamos  sobre qué entidad se efectuara la consulta (criteriaQuery.from(Usuario.class);) con esto tenemos una consulta la cual retorna todos los registros de la base de datos, pero ya que nuestra intención es filtrar mediante parámetros esa búsqueda creamos, a partir de la instancia del CriteriaBuilder, un Predicado (cb.conjunction();) este tipo de predicado (conjunction) lo podríamos ver como una compuerta lógica AND en el SQL estándar. Luego evaluamos si el parámetro es distinto a nulo entonces agregamos una expresión a la lista del conjunction, dicha expresión es el Greater Than (cb.gt(“propiedad de la entidad a Evaluar”), valor) y Lower Than respectivamente. Y por ultimo evaluamos si la lista del Predicado no esta vacía, de ser así, se lo agregamos al CriteriaQuery (criteriaQuery.where(conjunction);).

El mismo método podría generar diversas consultas dependiendo de los parámetros, las posibilidades podrían ser : 
-- (Ambos valores son diferente de nulo) 
SELECT usu FROM Usuario AS usu WHERE usu.edad > ? AND usu.edad < ?  

--  (Solamente edadMinima es diferente de nulo)  
SELECT usu FROM Usuario AS usu WHERE usu.edad > ? 

-- (Solamente edadMaxima es diferente de nulo)
SELECT usu FROM Usuario AS usu WHERE usu.edad < ?   

-- (Ambos valores son nulos)
SELECT usu FROM Usuario AS usu 

-- (Si usamos el Between)
SELECT usu.nombre FROM usuario AS usu WHERE usu.edad BETWEEN ? AND ?

Como podemos observar, la consulta SQL se define en tiempo de ejecución dependiendo al número de parámetros o condiciones que se evalúen como verdaderas en los parámetros enviados al método. Obviamente no quiero dar a entender que este es el único método para realizar consultas dinámicas, claro que no, pero esta es la forma que a mi parecer es un poco mas auto explicativo el código.

Eso nos regresa una lista de Usuario, pero ¿qué pasa si simplemente necesito conocer el nombre del usuario, y no todo el objeto en sí?  (una gran ayuda cuando estamos haciendo subconsultas in).

Para dar respuesta al anterior párrafo, es muy sencillo, pues simplemente hacemos unos pequeños cambios al código. Obviamente el valor de retorno, del método, ahora será uno de Tipo  List<String>  y al CriteriaBuilder le indicamos que tipo de clase esperamos como respuesta. Entonces cambiamos cb.createQuery(Usuario.class); por algo como esto cb.createQuery(String.class); (Porque ya no queremos todo el objeto, sino simplemente una propiedad tipo String de la entidad). Ahora el CriteriaBuilder ya sabe qué tipo de valor necesitamos que nos retorne, y ahora simplemente agregamos una línea debajo de criteriaQuery.from(Usuario.class);  en la siguiente línea de código le indicamos que propiedad de la entidad necesitamos que nos retorne. Y nos quedaría algo así:
criteriaQuery.select(usuario.get("nombre").as(String.class));
un equivalente (omitiendo las restricciones creadas por el predicado del conjunction) en SQL seria así : 

SELECT usu.nombre FROM Usuario AS usu

Bueno, el uso del Disjunction es igual al del Conjunction, simplemente cambia la compuerta lógica empleada para ella, que en el caso del Disjunction es OR.
A un conjunction se le puede agregar un disjunction de la siguiente manera, osea que tendríamos nuestro comparador OR dentro de un paréntesis en la consulta. para el siguiente ejemplo usaremos la tabla que se viene viendo en el post, usuario, cuya estrutura es la siguiente:



SELECT usu.nombre, usu.edad FROM usuario AS usu WHERE usu.nombre LIKE '%a%' AND (usu.tipo=1 OR usu.tipo=3)

El resultado de la query seria:


Pero la misma consulta sin los paréntesis retorna registros de la tabla que no deseamos mostrar. como podemos comprobar:

Bueno, ahora vamos a crear esa query usando instancias del CriteriaBuilder y algunos Predicados (Conjunction y Disjunction).  En esta parte voy a omitir el codigo de getEntityManager y lo del CriteriaQuery.from, ya que seria igual al ejemplo anterior ( el cual regresa la lista de usuarios ). Bueno. ahora el codigo:

    // Clase ObjetoBusqueda, posee todos los parametros que deseamos incluir en la busqueda, seria un POJO con sus
    // getters y setters
    public List getAll(ObjetoBusqueda objetoBusqueda) {
    // Obtenemos el EntityManager, CriteriaBuilder y Creamos el Query sobre la entidad deseada (Usuario.class en nuestro caso)
    // Creamos los predicados
    Predicate conjunction = cb.conjunction();
    Predicate disjunction = cb.disjunction();
    if (objetoBusqueda.getNombre() != null) {
    // % Agregamos los comodines SQL al inicio y fin del parametro a buscar
          conjunction.getExpressions().add(cb.like(cb.lower(usuario.get("nombre").as(String.class)), "%"+ objetoBusqueda.getNombre() + "%"));
    }
    if (objetoBusqueda.isBucarTipo()){
        disjunction.getExpressions().add(cb.equal(usuario.get("tipo"), 1));
        disjunction.getExpressions().add(cb.equal(usuario.get("tipo"), 3));
    }
    if (disjunction.getExpressions().size() > 0) {
        // agregamos el disjunction al conjunction para que quede encerrado en paréntesis (como en el Query SQL
        // de la Imagen de Arriba)
        conjuntion.getExpressions().add(disjunction);
    }
    if (conjuntion.getExpressions().size() > 0) {
    criteriaQuery.where(conjuntion);
    }
    // retornamos la lista filtrada
    return em.createQuery(criteriaQuery).getResultList();
}// Fin del Metodo

Bueno con ese código obtendríamos una lista de usuario al igual que en el sql de la imagen. Solo un par de detalles a resaltar del código, el parámetro que recibe el método, seria una clase java la cual tendría todos los parámetros por los cuales deseemos filtrar la búsqueda, osea, eso es mejor que enviar todos los parámetros al método,  y que los parámetros del método sean mas fáciles de manejar, igual, si en algún caso necesitamos agregar otro parámetro a la búsqueda, solo le creamos la propiedad al POJO de búsqueda y lo tratamos en el método sin modificar la lista de parámetros que recibe el método :).

Bueno, hasta acá, ya se ha cubierto una buena parte del total que este articulo tendrá, por lo tanto vamos a ver como usar un criteriaBuilder.in, para eso vamos a modificar, o mejor dicho agregar un par de tablas a la base de datos, bueno. para el siguiente ejemplo vamos a obtener una lista de usuarios que han ingresado al sistema en los últimos N días, para eso vamos a suponer que cada vez que el usuario ingresa al sistema, en ese instante se guarda la fecha y el id del usuario. entonces tendríamos una entidad la cual llamaremos historial, y al usuario le vamos a agregar una propiedad de tipo lista de objetos de historial, en otras palabras, un usuario puede tener muchos historiales, y un registro de historia solo puede pertenecer a un usuario, osea que en usuario tenemos la relación @OneToMany, indicando una propiedad tipo Lista de Historial, y en historial tenemos una relación @ManyToOne haciendo referencia a una propiedad tipo usuario.
Bueno, una imagen nos aclarara la situación ^^.

Bueno, los registros de la tabla usuario han cambiado respecto a la anterior imagen, pero es irrelevante la verdad. lo que si importa destacar acá es que tenemos 3 usuarios, 2 de ellos han tenido actividad los últimos  días (esta entrada la estoy escribiendo el 30 de Marzo del 2012). entonces, si construimos una query la cual nos retorne los usuario que han tenido actividad en los últimos... diez días, dicha consulta nos debería de retornar simplemente al usuario McCubo y Ringo 2012. vamos a construir la consulta entonces.

    public List<Usuario> getUltimosLogeados() {
        EntityManager entityManager = getEntityManager();
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Usuario> historyCriteriaQuery = cb.createQuery(Usuario.class);
        Root<Historial> historialRoot = historyCriteriaQuery.from(Historial.class);
        historyCriteriaQuery.select(historialRoot.get("usuario").as(Usuario.class));
        Predicate historyConjunction = cb.conjunction();
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, -10);
        Date date = calendar.getTime();
        // usamos greaterThanOrEqualTo en lugar de ge dado que ge espera recibir dos Expresiones tipo Numero, y dado que
        // las fechas no son numeros, recurrimos a este otro metodo del Builder
        historyConjunction.getExpressions().add(cb.greaterThanOrEqualTo(historialRoot.get("fechaIngreso").as(Date.class), date));
        historyCriteriaQuery.where(historyConjunction);
        List<Usuario> userList = entityManager.createQuery(historyCriteriaQuery).getResultList();
        
        
        CriteriaQuery<Usuario> userCriteriaQuery = cb.createQuery(Usuario.class);
        Root<Usuario> usuarioRoot = userCriteriaQuery.from(Usuario.class);
        Predicate usuarioConjunction = cb.conjunction();
        // Agregamos una restriccion a la consulta de Usuarios, indicando que necesitamos que retorne unicamente
        // Los usuarios que estan en la lista que se pasa como parametro
        usuarioConjunction.getExpressions().add(usuarioRoot.in(userList));
        userCriteriaQuery.where(usuarioConjunction);
        List<Usuario> resultList = entityManager.createQuery(userCriteriaQuery).getResultList();
        System.out.println(date);
        return resultList;
    }

Bueno, aunque pueda ser que los ejemplos no tengan mucha correlación o que las restricciones (mas que todo en esta del IN) al parecer no tiene mucha lógica o se pudo evitar el IN agregando un historyCriteriaQuery.distinct(true); para que nos retornara de una sola vez los usuarios que su ultima actividad estuviera en un lapso de tiempo menor a diez días, el objetivo era mas que nada, mostrar como se aplica la sintaxis de los métodos y para que sirva de guia para alguna alma perdida en el ciber espacio. pero bueno, después de mucho de no postear nada, espero me halla dado a entender y si no, pues en verdad lo siento, he tratado de comentar el código he ir insertando pequeñas imagen es para que el lector tenga una idea mas clara de lo que deseo compartir.

Eso es todo, y espero comenten(No comentarios agresivos -.-)  si tienen alguna duda, trataremos de darle solución...
Saludos!

ahhh por cierto, en este enlace quedan las entidades mapeadas por alguna duda.

0 comentarios:

Publicar un comentario