viernes, 28 de octubre de 2011

Creando Hibernate Criteria en Grails

   La situación es la siguiente: Tenemos una aplicación grails en la que tenemos que buscar por nombre de usuario. La primera aproximación sería utilizar ilike para buscar en los campos nombre y apellidos y todo funcionaría perfectamente hasta que llegamos a los nombres con tildes. ¿Qué ocurre si el usuario está dado de alta como Iván y nosotros introducimos Ivan, pues que sencillamente no aparecerá.
   Hay varias formas de afrontar el problema incluyendo el full text search con plugins como searchable o elasticsearch, pero si no queremos complicarnos en configurarlos o nuestra situación no requiere de búsquedas complejas, podemos usar otra solución.
   En mi caso estoy utilizando PostgreSQL como base de datos en su versión 9.0. A partir de esta versión se incluye por defecto (sólo es necesario instalarla) la función unaccent que elimina todas las tildes de un campo. Así, la consulta que contruiríamos a mano sería algo como:
select * from user
where unaccent("name") ilike unaccent('%iván%')
Y devolvería cualquier usuario que se llamase: ivan, Iván, iván, IVÁN,...   Todo esto está muy bien, pero que ocurre si ya tenemos el siguiente criteria de grails:
def users = User.createCriteria().list() {
    ilike('name', '%' + value + '%')
}
¿Cómo añadimos esa llamada a la función unaccent?. Vamos a crear nuestro propio Hibernate Criteria.
Editamos el archivo BootStrap.groovy y añadimos lo siguiente:
HibernateCriteriaBuilder.metaClass.unaccent = { String propertyName, Object propertyValue ->

 if (!validateSimpleExpression()) {
  throwRuntimeException(new IllegalArgumentException("Call to [unaccent] with propertyName [" +
    propertyName + "] and other property name [" + otherPropertyName + "] not allowed here."));
 }

 propertyName = calculatePropertyName(propertyName);
 propertyValue = calculatePropertyValue(propertyValue);

 def query = "unaccent(\"${propertyName}\") ilike unaccent('%${propertyValue}%')"
 return addToCriteria(Restrictions.sqlRestriction(query));

 def query = "unaccent(\"${propertyName}\") ilike unaccent(?)"
 def value = "%${propertyValue}%"
 return addToCriteria(Restrictions.sqlRestriction(query.toString(), value.toString(), Hibernate.STRING));
}
[ACTUALIZACIÓN]: He cambiado la forma de generar la consulta para que evitar posibles ataques por inyección de sql.

Lo que estamos haciendo es inyectar al HibernateCriteriaBuilder un método llamado unaccent que recibe como parámetros un string con el nombre de la propiedad y un objeto con el valor que queremos comparar.
Con esto podemos reescribir la consulta anterior de la siguiente manera:
def users = User.createCriteria().list() {
    // Old method
    //ilike('name', '%' + name + '%')
    unaccent('name', value)
}
Si ejecutamos el criteria vemos que la consulta es la siguiente:
select
        this_.id as id19_0_,
        this_.name as name19_0_
     from
        user this_
     where
        unaccent("name") ilike unaccent('%iván%')
Que efectivamente devuelve los mismos registros de antes :-)
   Todo esto se puede mejorar puesto que en función del número de registros que esperemos tener en la tabla, aplicar la función unaccent a la columna obliga a la base de datos a hacer un full scan en toda la tabla. Podríamos crear un índice, usar un campo paralelo para realizar las búsquedas que se mantenga automáticamente con un trigger, controlar este campo desde la aplicación grails con los métodos afterSave() y afterUpdate() de la clase de dominio User,... en fin, unas cuantas alternativas.

3 comentarios:

siari dijo...

Hola Iván trate de agregar la función de unaccent en el BootStrap.groovy pero no me lo reconoce me marca un error al correr la aplicación, te envío mi BootStrap

class BootStrap {

def init = { servletContext ->

HibernateCriteriaBuilder.metaClass.unaccent = { String propertyName, Object propertyValue ->

if (!validateSimpleExpression()) {
throwRuntimeException(new IllegalArgumentException("Call to [unaccent] with propertyName [" +
propertyName + "] and other property name [" + otherPropertyName + "] not allowed here."));
}

propertyName = calculatePropertyName(propertyName);
propertyValue = calculatePropertyValue(propertyValue);



def query = "unaccent(\"${propertyName}\") ilike unaccent(?)"
def value = "%${propertyValue}%"
return addToCriteria(Restrictions.sqlRestriction(query.toString(), value.toString(), Hibernate.STRING));
}
}
def destroy = {
}


}

Iván dijo...

Hola siari,

sin ver el error es muy difícil poder ayudarte. Tal vez te falten los imports o alguna otra cosa.

Si quieres crear criterias personalizados te recomiendo que eches un vistazo a este plugin del que soy autor para utilizar tipos nativos de Postgresql en aplicaciones Grails. Podrás ver cómo se crean y se utilizan los distintos criterias.

https://github.com/kaleidos/grails-postgresql-extensions

Saludos, Iván.

Dan dijo...

Hola Iván, espero te encuentres bien. Te cuento estoy por presentar una aplicación para una materia y se me está agotando el tiempo =(
Tengo una consulta, no estoy pudiendo hacer queries desde grails en bd mysql,
me devuelve el siguiente error:
No signature of method: java.util.ArrayList.findAllByFechaAndHora() is applicable for argument types: (java.lang.String, java.lang.String) values: [2016/02/27, 20]

en controller este es la línea de code:
def reservadas= listaReservas.findAllByFechaAndHora(params.fecha, params.hora)
fecha está definido en domain y bd como date y hora es de tipo float en ambas.
He estado investigando y probando muchas cosas pero no encuentro manera de resolverlo,
Puede ser que las queries con findBy soporte sólo strings??
y en ese caso qué opciones se pueden aplicar?
Desde ya muchas gracias!
Saludos!!

Publicar un comentario