Reutilizar un custom validator en Grails
En estos momentos ando colaborando con Sergio del Amo en un producto propio para su empresa Softamo, un producto que estamos desarrollando con Grails.
El tema es que teníamos un puñado de clases de dominio que están relacionadas con un propietario, una clase User. Cada instancia de esa clase de dominio debía tener un nombre único por propietario, por lo que debíamos usar un custom validator. Al final, tras varias refactorizaciones llegamos a una solución que creo que quedó bastante elegante.
Las clases de dominio heredan de una clase padre con varios métodos comunes, además de una clausura estática que contiene la lógica de la validación uniqueByUser, que es lo que nos interesa para esto.
static uniqueByUser = { value, object ->
propertyName = propertyName[0].toUpperCase() + propertyName[1..propertyName.size()-1]
def methodName = "findByUserAnd${propertyName}"
def obj = object.class."$methodName"(object.user, value)
if(obj && (obj.id != object.id)) {
return 'unique'
}
}
Vale, por si no queda del todo claro, intento detallar que hace el código:
- propertyName, es el nombre de la propiedad/atributo sobre la que se ejecutará la validación, variable que está implícita en la clausura.
- propertyName[0].toUpperCase() + propertyName[1..propertyName.size()-1], ponemos en mayúscula el primer carácter del nombre del atributo (por ejemplo "name" pasa a "Name").
- def methodName = "findByUserAnd${propertyName}", ponemos el método al que vamos a llamar para saber si ya existe un valor con mismo usuario y atributo buscado (por ej: findByUserAndName).
- def obj = object.class."$methodName"(object.user, value), ejecutamos ese findBy* estático sobre la clase del objeto instanciado que se está validando, pasándole el usuario relacionado y el valor del atributo (algo que podría ser equivalente por ejemplo a Drink.findByUserAndName(drink.user, "vodka")).
- if(obj && (obj.id != object.id)), comprobamos que si se devuelve un valor, no sea el mismo que la instancia del actual.
- return 'unique', en este caso devolvemos el código de mensaje de validación como unique, por lo que aprovecharemos los mismos mensajes i18n del unique estándar de grails.
Entonces, en nuestras clases de domino que hereden de ahí, para el atributo donde queramos utilizar ese constraint simplemente deberemos indicar validator: uniqueByUser. Algo como:
static constraints = {
name blank: false, validator: uniqueByUser
description blank: false
}
Lo único que no contempla ahora mismo esta solución es tener más de un custom validator, principalmente porque no nos ha hecho falta. Si lo llegamos a necesitar, veremos como lo hacemos, claro :P.
Mejoras y propuestas bienvenidas.
ACTUALIZACIÓN: Mejor que darle tantas vueltas como hicimos nosotros, es utilizar el soporte multi-column de la constraint unique (Nota mental: No dar por supuestas según que cosas, e ir a verlo a la documentación). Gracias al comentario de @jneira.