5. Patrón MVC: Dominios y servicios.

En esta tercera sesión sobre Grails veremos como Grails hace uso de GORM para mapear objetos contra la base de datos de la aplicación. Recordar que GORM es una capa superior a Hibernate con lo que gran parte de lo aprendido en módulos anteriores sobre Hibernate serán totalmente válidos para trabajar con Grails.

Por último, veremos lo sencillo que es en Grails implementar servicios que se encarguen de la lógica de negocio de nuestra aplicación creando un sencillo ejemplo.

5.1. Dominios

5.1.1. Creación de dominios

Como puedes imaginar, lo primero que se debe hacer con una clase de dominio es crearla. Como ya vimos en sesiones anteriores, el comando utilizado para generar una nueva clase de dominio es grails create-domain-class seguido del nombre de la nueva clase que queremos crear. Pero, ¿qué pasa en la base de datos cuando generamos nuestras clases de dominio? Para ver exactamente que es lo que pasa en la base de datos cuando creamos una nueva clase de dominio, vamos a tomar como ejemplo la clase Todo.

package es.ua.expertojava.todo

class Todo {
    String title
    String description
    Date date
    Date reminderDate
    String url
    Boolean done = false
    Category category

    static hasMany = [tags:Tag]
    static belongsTo = [Tag]

    static constraints = {
        title(blank:false)
        description(blank:true, nullable:true, maxSize:1000)
        date(nullable:false)
        reminderDate(nullable:true)
        url(nullable:true, url:true)
        done(nullable:false)
        category(nullable:true)
    }

    String toString(){
        title
    }
}

Pasemos a ver que tabla nos ha creado nuestra aplicación en la base de datos. Para esto, vamos a ver una de las características añadidas en las últimas versiones de Grails que es la posibilidad de utilizar una consola web para acceder a la estructura de la base de datos creada. Esta consola se puede acceder desde la dirección http://localhost:8080/todo/dbconsole. Hay que tener en cuenta que como parámetro JDBC URL debemos introducir la misma url que tengamos configurada en el archivo DataSource.groovy, que por defecto será jdbc:h2:mem:devDb.

Conexión con DbConsole

Una vez dentro de la consola de H2, podemos ejecutar el comando SHOW COLUMNS FROM TODO para ver el esquema de la tabla TODO y compararla de eta forma con la clase de dominio que nosotros hemos creado para encontrar las siguientes diferencias.

Table Todo

Nuevas columnas

La primera diferencia que encontramos es la existencia de dos nuevas columnas en la tabla correspondiente de la base de datos. La primera de ellas es la columna id que además es la clave primaria de la tabla y autoincremental. Esto puede parecer que es algo en contra de Grails ya que no vamos a poder establecer la clave primaria que nosotros queremos, pero en la práctica se ha demostrado que es la mejor forma a la hora de interactuar con la base de datos. La otra nueva columna que se ha generado en la tabla es el campo version y servirá para garantizar la integridad transaccional y el bloqueo eficiente de las tablas cuando se realizan operaciones de entrada.

Nombre de las columnas

El nombre de las propiedades que en la clase de dominio seguían el convenio CamelCase (palabras sin espacios en blanco y con la primera letra de cada una de ellas en mayúscula), en la tabla se convierten por defecto al formato snake_case (todas las palabras en minúsculas y separadas por un subrayado bajo _), a pesar de que en la consola de H2 nos aparezca todo en mayúsculas. Si lo comprobáramos con una base de datos MySQL veríamos este cambio de nombre de las propiedades.

Claves ajenas

GORM representa las claves ajenas en la tabla con el nombre de la clase/tabla referencia en minúsculas seguido de _id. En nuestro caso tenemos la columna category_id que identifica la relación entre las tareas y categorías, con lo que cada tarea sólo puede pertenecer a una categoría.

Tipos de datos

Dependiendo de la base de datos utilizada (en nuestro caso H2), GORM transformará los tipos de datos de las propiedades de la clase de dominio en otros tipos de datos en la tabla. Por ejemplo, el tipo de dato String se sustituye en la base de datos por varchar(), mientras que el tipo de dato Date es reemplazado por timestamp. Todo esto dependerá de la base de datos utilizada y es posible que varíe entre ellas.

5.1.2. Relaciones entre clases de dominio

Cualquier aplicación que vayamos a desarrollar, presentará relaciones entre sus clases de dominio. En nuestro ejemplo de scaffolding ya definimos algunas relaciones entre ellas y ahora vamos a ver más en detalle esas relaciones. Las posibles relaciones entre las clases de dominio de una aplicación son:

  • Uno a uno

  • Uno a muchos

  • Muchos a muchos

Si hacemos algo de memoria de la definición de nuestras clases de dominio, recordaremos que utilizábamos la palabra reservada hasMany y belongsTo para establecer la relación entre las tareas y las etiquetas. También existen la palabra reservada hasOne. A continuación veremos en detalle cada una de las posibles relaciones y como especificarlas.

Uno a uno

Una relación uno-a-uno se da cuando un objeto de la clase A está únicamente relacionado con un objeto de la clase B y viceversa. Por ejemplo, imaginad la aplicación Biblioteca en donde un libro sólo puede tener una operación activa. Esto se representaría con una relación uno-a-uno y podríamos hacerlo de tres formas diferentes.

Primera forma de establecer una relación uno a uno

class Book{
}

class ActiveOperation{
    ....
    Book book
    ....
}

Aquí estamos representando una relación unidireccional desde las operaciones activas hasta los libros. Si necesitamos especificar una relación bidireccional podemos utilizar cualquiera de las siguientes formas.

Segunda forma de establecer una relación uno a uno

class Book{
    ....
    ActiveOperation actoper
    ....
}

class ActiveOperation{
    ....
    static belongsTo = [book:Book]
    ....
}

En este caso, las inserciones y los borrados se realizan en cascada. Si por ejemplo hacemos new Book(actoper:new ActiveOperation()).save() en primer lugar se creará una operación activa y posteriormente se creará el libro. De igual forma, si eliminamos el libro asociado a la operación activa, ésta se eliminará también de la base de datos.

Tercera forma de establecer una relación uno a uno

class Book{
    ....
    static hasOne = [actoper:ActiveOperation]

    static constraints = {
        actoper unique:true
    }
    ....
}

class ActiveOperation{
    ....
    Book book
    ....
}

En esta ocasión, se creará una relación bidireccional de uno a uno entre los libros y las operaciones activas y se creará una columna de clave ajena en la tabla active_operation llamada book_id.

En estos casos, desde la documentación de Grails se aconseja que especifiquemos también una restricción indicando que la propiedad será única.

Uno a muchos

Una relación uno-a-muchos se da cuando un registro de la tabla A puede referenciar muchos registros de la tabla B, pero todos los registros de la tabla B sólo pueden referenciar un registro de la tabla A. Por ejemplo, en nuestra aplicación de ejemplo, una tarea sólo puede estar relacionada con una categoría, pero una categoría puede tener muchas tareas. Esto se representa de la siguiente forma en la definición de las clases de dominio.

class Category {
    ....
    static hasMany = [todos:Todo]
    ....
}

class Todo {
    ....
    Category category
    ....
}

Con esta definición de las clases de dominio, las actualizaciones y los almacenamientos se realizan en cascada mientras que los borrados sólo se realizarán en cascada si especificamos la otra parte de la relación con un belongsTo.

class Category {
    ....
    static hasMany = [todos:Todo]
    ....
}

class Todo {
    ....
    static belongsTo = [category:Category]
    ....
}

Estas propiedades que representan la relación de uno a muchos son consideradas como colecciones de datos y para insertar o eliminar datos de las mismas únicamente debemos utilizar un par de métodos de GORM que son addTo() y removeFrom().

//Ejemplo para insertar datos en relaciones
def todo = new Todo(title:"Limpiar cocina", description:"Limpiar la cocina a fondo", ...)

def category = new Category(name:"Hogar")

category.addToTodos(todo)
category.save()

//Ejemplo para eliminar datos de relaciones
category.removeFromTodos(todo)
category.save()

//Ejemplo para buscar tareas dentro de una categoría
def todosfound = category.todos.find {it.title = "Limpiar cocina"}
Muchos a muchos

En una relación muchos-a-muchos un registro de la tabla A puede referenciar muchos registros de la tabla B y un registro de la tabla B referenciar igualmente muchos registros de la tabla A. En nuestra aplicación ejemplo disponemos de la relación entre tareas y etiquetas en la que una tarea puede estar relacionada con muchas etiquetas y viceversa. Esto se representa con una relación muchos-a-muchos de la siguiente forma.

class Tag{
    ....
    static hasMany = [todos:Todo]
    ....
}

class Todo{
    ....
    static hasMany = [tags:Tag]
    static belongsTo = Tag
    ....
}

En GORM, una relación muchos-a-muchos se representa indicando en ambas clases la propiedad hasMany y en al menos una de ellas la propiedad belongsTo.

5.2. Restricciones

El siguiente punto que vamos a ver trata a fondo las definiciones de restricciones en las clases de dominios.

5.2.1. Restricciones predefinidas en GORM

Para controlar estas restricciones, GORM dispone de una serie de funciones comunes predefinidas para establecerlas. Además, también nos permitirá crear nuestras propias restricciones. Hasta ahora, ya hemos visto algunas de estas restricciones como pueden ser size, unique o blank entre otras. Recordamos que para establecer las restricciones de una clase de dominio debemos establecer la propiedad estática constraints como en el siguiente ejemplo.

package es.ua.expertojava.todo

class Todo {
    String title
    String description
    Date date
    Date reminderDate
    String url
    Boolean done = false
    Category category

    static hasMany = [tags:Tag]
    static belongsTo = [Tag]

    static constraints = {
        title(blank:false)
        description(blank:true, nullable:true, maxSize:1000)
        date(nullable:false)
        reminderDate(nullable:true)
        url(nullable:true, url:true)
        done(nullable:false)
        category(nullable:true)
    }

    String toString(){
        title
    }
}

A continuación, vamos veremos las restricciones que podemos utilizar directamente con GORM.

Nombre Descripción Ejemplo

blank

Valida si la cadena puede quedar o no vacía

login(blank:false)

creditCard

Valida si la cadena introducida es un número de tarjeta correcto

tarjetaCredito(creditCard:true)

email

Valida si la cadena introducida es un correo electrónico correcto

correoElectronico(email:true)

password

Indica si la propiedad es una contraseña, con lo que al introducirla no se mostrará directamente

contrasenya(password:true)

inList

Valida que la propiedad contenga cualquiera de los valores pasados por parámetro

tipo(inList:["administrador", "bibliotecario", "profesor", "socio"])

matches

Valida la cadena introducida contra una expresión regular pasada por parámetro

login(matches:"[a-zA-Z]+")

max

Valida que el número introducido no sea mayor que el número pasado por parámetro

price(max:999F)

min

Valida que el número introducido no sea menor que el número pasado por parámetro

price(min:0F)

minSize

Valida que la longitud de la cadena introducida sea superior al número pasado por parámetro

hijos(minSize:5)

maxSize

Valida que la longitud de la cadena introducida sea inferior al número pasado por parámetro

hijos(maxSize:15)

notEqual

Valida que el objeto asignado a la propiedad no sea igual que el objeto pasado por parámetro

login(notEqual:"admin")

nullable

Valida si el objeto puede ser null o no

edad(nullable:true)

range

Valida que el objeto esté dentro del rango pasado por parámetro

edad(range:0..99)

scale

Indica el número de decimales de la propiedad

salario(scale:2)

size

Valida que la longitud de la cadena esté dentro del rango pasado por parámetro

login(size:5..15)

unique

Especifica que la propiedad debe ser única y no se puede repetir ningún objeto

login(unique:true)

url

Valida que la cadena introducida sea una dirección URL correcta

website(url:true)

Pero además de estas restricciones, Grails también tiene otras más que suponen una serie de cambios en la interfaz de usuario generado y aunque no es muy recomendable mezclar este tipo de información en las clases de dominio, cuando usamos el scaffolding en gran parte de nuestro proyecto, es muy cómodo indicarle algunas particularidades a nuestro proyecto para facilitarle su renderizado. Estas restricciones son las siguientes:

Nombre Descripción Ejemplo

display

Indica si la propiedad debe mostrare o no en las vistas generadas por el scaffolding. El valor por defecto es true

password(display:false)

editable

Indica si la propiedad es editable o no. Se suele utilizar en aquellas propiedades de solo lectura

username(editable:false)

format

Especifica el formato aceptado en aquellos tipos que lo acepten, como pueden ser las fechas

dateOfBirth(format:'yyyy-MM-dd')

password

Indica si la propiedad es una contraseña, con lo que al introducirla no se mostrará directamente

password(password:true)

5.2.2. Construir tus propias restricciones

Pero seríamos un tanto ilusos si pensáramos que el equipo de desarrollo de Grails ha sido capaz de crear todas las posibles restricciones existentes en cualquier aplicación. Ellos no lo han hecho, pero si han dejado la posibilidad de crear nuestras propias restricciones de una forma rápida y sencilla.

Si echamos un vistazo a la clase de dominio Todo, podemos detectar que una posible restricción que nos falta por añadir sería la comprobación de que la fecha sea posterior a la fecha actual. Para esta restricción, Grails se ha olvidado de nosotros ya que si repasamos la lista de restricciones predefinidas, no encontramos ninguna que satisfaga dicha restricción. Para crear nuevas restricciones, tenemos el closure validator. Veamos como quedaría esta restricción:

date(nullable:true,
    validator: {
        if (it) {
            return it?.after(new Date())
        }
        return true
    }
)

El closure validator debe devolver true en caso de que la validación se pase correctamente y false en caso contrario. Gracias a la variable it podemos acceder al valor introducido para analizarlo y comprobar que cumple los requisitos, en este caso, que la fecha no sea anterior a la actual.

Este closure validator también puede recibir un par de parámetros que nos permitirá evaluar el valor proporcionado a una propiedad con respecto a cualquier otra propiedad del objeto. El siguiente ejemplo de restricción nos permitirá comprobar que la fecha de recordatorio sea siempre anterior a la fecha de la propia tarea:

reminderDate(nullable:true,
    validator: { val, obj ->
        if (val && obj.date) {
            return val.before(obj?.date)
        }
        return true
    }
)

En ocasiones es conveniente reutilizar el código de una restricción en diferentes propiedades de diferentes clases de dominio. Para ello en lugar de definir la restricción en la clase de dominio la definiremos en el archivo de configuración Config.groovy de la siguiente forma:

grails.gorm.default.constraints = {
    max20chars(nullable: false, blank: false, maxSize:20)
}

Posteriormente en la propiedad que necesites utilizar esta restricción, debes referenciarla de esta forma:

class User {
    ...
    static constraints = {
        password shared: "max20chars"
    }
}

5.2.3. Mensajes de error de las restricciones

Sin lugar a dudas, los mensajes que se generan cuando se produce un error en las restricciones de una clase de dominio, no son lo más deseable para un usuario final, así que es necesario que entendamos el mecanismo que utiliza Grails para devolver estos mensajes cuando se producen estos errores.

Si recordamos la clase de dominio Todo, tenemos que el título no puede dejarse vacío. Esto lo hacíamos utilizando la restricción blank:false. Al intentar crear una tarea sin escribir su título, recibiremos un mensaje de error como el siguiente La propiedad [title] de la clase [class Todo] no puede ser vacía. Pero, ¿cómo se encarga Grails de mostrar estos mensajes de error?

Grails implementa un sistema jerárquico para los mensajes de error basado en diferentes aspectos como puede ser el nombre de la clase de dominio, la propiedad o el tipo de validación. Para el caso del error producido al saltarnos la restricción de no dejar en blanco el campo title de la clase de dominio Todo, dicha jerarquía sería la siguiente.

todo.title.blank.error.title
todo.title.blank.error.java.lang.String
todo.title.blank.error
todo.title.blank.title
todo.title.blank.java.lang.String
todo.title.blank
blank.title
blank.java.lang.String
blank

Este sería el orden en el que Grails buscaría en nuestro archivo message.properties hasta encontrar cualquier propiedad y mostrar su mensaje. En caso de no encontrar ninguna de estas propiedades, se mostraría la cadena asociado a la propiedad default.blank.message, que es la que encontramos por defecto en el archivo message.properties.

De esta forma, ya podemos personalizar los mensajes de error generados al incumplir las restricciones de nuestras clases de dominio. Simplemente comentar que para las restricciones creadas por nosotros mismos, la jerarquía de los mensajes en el validador introducido en la clase de dominio Todo empezaría por todo.date.validator.error.date y así sucesivamente tal y como hemos visto anteriormente.

5.3. Aspectos avanzados de GORM

Ahora que ya hemos visto la parte básica a la hora de crear una clase de dominio (establecer sus propiedades, las restricciones y las relaciones entre ellas), vayamos un paso más adelante y veamos aspectos algo más avanzados de GORM

5.3.1. Ajustes de mapeado

Hay ocasiones en que los administradores de las bases de datos quieren, por cualquier motivo, que las columnas de las tablas de sus bases de datos sean nombradas siguiendo un determinado patrón. Estos motivos pueden ir desde cuestiones de facilidad en la lectura de los campos hasta argumentos como "así se ha hecho toda la vida y no se va a cambiar". El tema está en que hay que conformarse con sus ordenes y acatarlas y encontrar la mejor forma para adaptarse a ellas.

Para solucionar este posible problema que se nos puede plantear en cualquier organización, GORM dispone de una forma rápida y sencilla para ajustar estos nombres de las columnas de las tablas relacionadas. Para ello, GORM emplea una sintaxis de tipo DSL y exige crear un nuevo closure estático llamado mapping de la siguiente forma static mapping = {//Todo el mapeado de la tabla aquí}. Veamos los posibles cambios que podemos hacer con GORM.

Nombres de las tablas y las columnas

Si necesitáramos cambiar por ejemplo el nombre de la tabla todo por tbl_todo, deberíamos escribir dentro del closure mapping el siguiente código table 'tbl_todo'. Si ahora quisiéramos cambiar el nombre de la columna description por desc, podríamos hacer lo siguiente:

class Todo {
    ....
    static mapping = {
        table 'tbl_todo'
        description column:'desc'
    }
}

Con esto, ya tendríamos cambiados los nombres de las tablas y de las columnas para adaptarlos al convenio que nosotros queramos seguir.

Deshabilitar el campo version

Por defecto, GORM utiliza un campo llamado version para garantizar la integridad de los datos y está demostrado que es un buen método. Sin embargo, por cualquier motivo es posible que no queramos tener este campo en nuestra tabla y para ello, simplemente debemos especificar en el mapeado de la clase lo siguiente: version false.

Carga perezosa de los datos

Por defecto, Grails realiza una carga perezosa de los datos de las propiedades. Esto es que no se cargarán en memoria hasta que no sean solicitados por la operación, lo cual nos previene de posibles pérdidas de rendimiento al cargar demasiados datos innecesarios. Sin embargo, si queremos cambiar este comportamiento por defecto de Grails, podemos hacerlo de dos formas:

class Person {

    String firstName Pet pet

    static hasMany = [addresses: Address]

    static mapping = {
        addresses lazy: false
        pet fetch: 'join'
    }
}


class Address {
    String street
    String postCode
}


class Pet {
    String name
}

Indicando addresses lazy: false nos aseguramos que cuando los datos de una persona se carguen en nuestra aplicación, sus direcciones serán cargadas al mismo tiempo. Por otro lado, con la opción pet fetch:'join' hacemos básicamente lo mismo salvo que en lugar de hacerlo con un SELECT como se estaría haciendo con la opción anterior lo estaríamos haciendo con una operación de tipo JOIN. Si queremos reducir el número de queries y mejorar el rendimiento de la operación, deberíamos utilizar la segunda opción. Sin embargo, hay que tener cuidado con estas opciones ya que podemos cargar demasiados datos innecesarios.

Sistema caché

Uno de las grandes ventajas de Hibernate es la posibilidad de utilizar una caché de segundo nivel, que almacena los datos asociados a una clase de dominio. Para configurar esta caché de segundo nivel, en primer lugar debemos modificar el archivo de configuración grails-app/conf/DataSource.groovy para indicarle quien se encargará de gestionar esta caché.

hibernate {
    cache.use_second_level_cache = true
    cache.use_query_cache = true
    cache.region.factory_class = 'net.sf.ehcache.hibernate.EhCacheRegionFactory'
    singleSession = true
    flush.mode = 'manual'
}

Posteriormente, para utilizar este sistema de caché, podemos especificarlo en la propia clase de dominio de la siguiente forma:

class User {
    ...
    static mapping = {
        table 'tbl_users'
        cache true
    }
}

Esta caché sería de tipo lectura-escritura e incluiría todas las propiedades, tanto las perezosas como las no perezosas. Pero también es posible ser un poco más específico e indicarle como queremos que se configure la caché de una determinada clase de dominio.

class User {
    ...
    static mapping = {
        table 'tbl_users'
        cache usage: 'read-only', include: 'non-lazy'
    }
}

Incluso también podemos especificar la caché para una asociación de la siguiente forma:

class Person {

    String firstName

    static hasMany = [addresses: Address]

    static mapping = {
        table 'people'
        version false
        addresses column: 'Address', cache: true
    }
}

De esta forma, sólo estaríamos cacheando la propiedad addresses de la clase Person.

Modificar la clave primaria

En ocasiones podemos necesitar modificar el sistema de clave primaria que utiliza Grails por defecto en el que se añade una propiedad llamada id autonumérico. Podríamos por ejemplo establecer como clave primaria de la tabla de las tareas la composición del título y la categoría de la siguiente forma:

static mapping = {
    id composite: ['title', 'category']
}

5.3.2. Herencia de clases

La herencia de clases es algo muy común y GORM lo implementa de forma tan sencilla como extendiendo la clase. Por ejemplo, imagina el caso de una clase de dominio Usuario que a su vez puede tener 3 tipos distintos. Podríamos tener algo así:

class User{
    ....
}

class Administrator extends User{
    ....
}

class Registered extends User{
    ....
}

class Guest extends User{
    ....
}

Pero, ¿cómo se transformaría esto en la base de datos? GORM habría almacenado todos los datos en una única tabla y además, hubiera añadido un campo llamado class que permitiría distinguir el tipo de instancia creada. No obstante, si optásemos por tener una tabla por cada tipo de usuario, podríamos tener lo siguiente:

class User{
    ....
}

class Administrator extends User{
    static mapping = {
        table = 'administrator'
    }
}

class Registered extends User{
    static mapping = {
        table = 'registered'
    }
}

class Guest extends User{
    static mapping = {
        table = 'guest'
    }
}

Otra opción sería especificar en la clase padre la propiedad tablePerHierarchy a falso de la siguiente forma:

class User{
    .....
    static mapping = {
        tablePerHierarchy false
    }
}

5.3.3. Propiedades transitorias

Por defecto, con GORM todas las propiedades definidas en una clase de dominio son persistidas en la base de datos. Sin embargo, en ocasiones es posible que tengamos determinadas propiedades que no deseamos que sean almacenadas en la base de datos, tales como por ejemplo la confirmación de una contraseña por parte de un usuario. Para ello, podemos añadir una nueva propiedad llamada transients a la clase de dominio correspondiente con la propiedad que no deseamos persistir en la base de datos.

class User {
    static transients = ["confirmPassword"]

    String username
    String password
    String confirmPassword
    String name
    String surnames
}

5.3.4. Eventos GORM

GORM dispone de dos métodos que se llaman automáticamente antes y después de insertar y actualizar las tablas de la base de datos. Estos métodos son beforeInsert() y beforeUpdate(). Gracias a estos métodos, vamos a poder realizar determinadas operaciones siempre antes de insertar o actualizar datos de nuestros registros. Por ejemplo, imagina el caso en el que tuviéramos que almacenar quien creó una tarea y quien fue la última persona en actualizarla. Necesitaríamos por ejemplo un par de propiedades llamadas createdBy y lastModifiedBy. Para automáticamente actualizar esta información en nuestra clase de dominio, podríamos utilizar los métodos beforeInsert() y beforeUpdate() de la siguiente forma:

class Todo {
    ....
    User createdBy
    User lastModifiedBy

    def beforeInsert = {
        createdBy = session?.user
        lastModifiedBy = session?.user
    }

    def beforeUpdate = {
        lastModifiedBy = session?.user
    }
}

Además de estos dos métodos comentados, GORM también dispone de los métodos beforeDelete(), beforeValidate(), afterInsert(), afterUpdate(), afterDelete() y onLoad().

5.3.5. Fechas automáticas

Grails además presenta una característica conocida como automatic timestamping. Con esta característica si queremos saber de una instancia de una determinada clase cuando se creo y cuando se modificó por última vez, simplemente debemos crear dos propiedades de tipo Date en la clase de dominio llamadas dateCreated y lastUpdated y Grails se encargará automáticamente de rellenar esta información por nosotros.

class Todo {
    ....
    Date dateCreated
    Date lastUpdated
}

Si por cualquier motivo, deseamos tener estas dos propiedades pero desactivar el automatic timestamping, podemos hacerlo de la siguiente forma:

static mapping = {
    autoTimestamp false
}

5.4. Interactuar con la base de datos

Cuando interactuamos con una base de datos, lo primero que debemos conocer es como realizar las operaciones básicas en cualquier aplicación (CRUD). Si echamos un vistazo al código del controlador de la clase de dominio Todo, detectaremos la presencia de los métodos delete() y save(). También podemos comprobar como no existe ninguno de estos dos métodos implementados en la clase de dominio Todo. Entonces, ¿cómo es capaz Grails de persistir la información en la base de datos si ninguno de estos dos métodos existen?

En Grails tenemos varias formas de realizar consultas a la base de datos que son:

  • Consultas dinámicas de GORM

  • Consultas HQL de Hibernate

  • Consultas Criteria de Hibernate

5.4.1. Consultas dinámicas de GORM

Básicamente, la consultas dinámicas de GORM son muy similares a los métodos que habéis visto en el módulo de JPA con lo que únicamente veremos un resumen de las mismas.

Método Descripción Ejemplo

count()

Devuelve el número total de registros de una determinada clase de dominio

Todo.count()

countBy()

Devuelve el número total de registros de una determinada clase de dominio que cumplan una serie de requisitos encadenados tras el nombre del método

Todo.countByTitle('Pintar cocina')

findBy()

Devuelve el primer registro encontrado de una determinada clase de dominio que cumpla una serie de requisitos encadenados tras el nombre del método

Todo.findByTitle('Pintar cocina')

findWhere()

Devuelve el primer registro encontrado de una determinada clase de dominio que cumpla una serie de requisitos pasados por parámetro en forma de mapa

Todo.findWhere(["title":"Pintar cocina"])

findAllBy()

Devuelve todos los registros encontrados de una determinada clase de dominio que cumplan una serie de requisitos encadenados tras el nombre del método

Todo.findAllByTitleIlike('%cocina%')

findAllWhere()

Devuelve todos los registros encontrados de una determinada clase de dominio que cumplan una serie de requisitos pasados por parámetro en forma de mapa

Todo.findAllWhere(["title":"Pintar cocina"])

get()

Devuelve el registro de una determinada clase de dominio cuyo identificador coincida con el pasado como parámetro

Todo.get(1)

getAll()

Devuelve todos los registros de una determinada clase de dominio cuyo identificador coincida con cualquier de los pasados parámetro en forma de lista

Todo.getAll([1,3])

list()

Devuelve todos los registros de una determinada clase de dominio. Acepta los parámetros max, offset, sort y order

Todo.list(["max":10, "offset":0, "sort":"title", "order":"asc"])

listOrderBy()

Devuelve todos los registros de una determinada clase de dominio ordenador por un criterio. Acepta los parámetros max, offset y order

Todo.listOrderByName(["max":10, "offset":0, "order":"asc"])

Podemos además encadenar varias propiedades junto con el operador utilizado, como por ejemplo:

Todo.findAllByTitleAndDescriptionAndDone('Pintar cocina',null, false)

Seguido de la propiedad podemos indicar el método a seguir en la comparación de registros. En la siguiente tabla se muestra una tabla resumen de esos métodos que podemos utilizar en las comparaciones.

Método Descripción Ejemplo

InList

Devuelve aquellos registros en los que el valor de la propiedad dada coincida con cualquiera de los elementos de la lista pasada por parámetro

Category.findAllByNameInList(["Hogar","Trabajo"])

LessThan

Devuelve aquellos registros en los que el valor de la propiedad dada sea menor que el valor pasado por parámetro

House.findAllByPriceLessThan(150000)

LessThanEquals

Devuelve aquellos registros en los que el valor de la propiedad dada sea menor o igual que el valor pasado por parámetro

House.findAllByPriceLessThanEquals(150000)

GreaterThan

Devuelve aquellos registros en los que el valor de la propiedad dada sea mayor que el valor pasado por parámetro

Car.findAllByPriceGreaterThan(25000)

GreaterThanEquals

Devuelve aquellos registros en los que el valor de la propiedad dada sea mayor o igual que el valor pasado por parámetro

Car.findAllByPriceGreaterThanEquals(25000)

Like

Es equivalente a una expresión LIKE en una sentencia SQL

Todo.findAllByTitleLike('%cocina%')

ILike

Similar al método Like salvo que en esta ocasión case insensitive

Todo.findAllByTitleIlike('%cocina%')

NotEqual

Devuelve aquellos registros en los que el valor de la propiedad dada no sea igual al valor pasado por parámetro

Todo.findAllByDoneNotEqual(true)

Between

Devuelve aquellos registros en los que el valor de la propiedad dada se encuentre entre los dos valores pasados por parámetro. Necesita de dos parámetros.

Car.findAllByPriceBetween(10000,20000)

IsNotNull

Devuelve aquellos registros en los que el valor de la propiedad dada no sea nula. No se necesita ningún argumento.

User.findAllByDateOfBirthIsNotNull()

IsNull

Devuelve aquellos registros en los que el valor de la propiedad dada sea nula

User.findAllByDateOfBirthIsNull()

5.4.2. Consultas HQL de Hibernate

Otra forma de realizar consultas sobre la base de datos es mediante el lenguaje de consulta propio de Hibernate, HQL (Hibernate Query Language). Con los métodos dinámicos de GORM vimos que teníamos la posibilidad de ejecutar llamadas a métodos para obtener un registro o un conjunto. Con HQL vamos a tener también esta capacidad con los métodos find() y findAll() más uno nuevo llamado executeQuery().

Con el método find() vamos a poder obtener el primer registro devuelto de la consulta HQL pasada por parámetro. El siguiente ejemplo, devuelve la primera tarea que encuentre en la clase de dominio Todo.

def sentenciaHQL1 = Todo.find("From Todo as t")

Pero lo habitual no es simplemente obtener el primer registro sino que normalmente necesitaremos obtener todos los registros que cumplan una serie de condiciones para posteriormente, actuar en consecuencia. Al igual que con los métodos dinámicos de GORM, HQL dispone también del método findAll() al cual debemos pasarle por parámetro la sentencia HQL correspondiente. En el siguiente ejemplo, vamos a ver tres formas diferentes de obtener determinadas tareas de nuestra aplicación.

def hqlsentence2 = Todo.findAll("from Todo as t where t.title='Pintar cocina'")

def hqlsentence3 = Todo.findAll("from Todo as t where t.title=?", ["Escribir tests unitarios"])

def hqlsentence4 = Todo.findAll("from Todo as t where t.title=:title", [title:"Cocinar pastel"])

En la primera de ellas, la sentencia HQL se pasa como parámetro al método findAll() tal cual y sin utilizar ningún parámetro. También es posible pasar parámetros a la sentencia HQL tal y como se ha hecho en la segunda forma. Utilizando el caracter ? le especificamos que es valor se pasará en forma de parámetro posicional, es decir, el primer caracter ? encontrado en la sentencia HQL se referirá al primer parámetro pasado, el segundo ? al segundo parámetro y así sucesivamente.

Otra forma posible de implementar sentencias HQL es pasando un mapa con las variables introducidas en la sentencia HQL. Para introducir una variable en una sentencia HQL basta con escribir el carácter : seguido del nombre de la variable que deseemos crear. En el ejemplo hemos creado la variable title, que posteriormente se rellenará en el mapa pasado como parámetro.

De igual forma que veíamos anteriormente con los métodos list() y listOrderBy(), HQL permite utilizar los parámetros max, offset, sort y order para afinar aún más la búsqueda.

Por último, el método executeQuery() es algo diferente a los métodos find() y findAll(). En estos dos métodos se devolvían todas las columnas de la tabla en cuestión, cosa que no siempre puede ser necesaria. El método executeQuery() permite seleccionar que columnas vamos a devolver. El siguiente ejemplo devuelve sólo la fecha de la tarea cuyo título sea "Pintar cocina".

Todo.executeQuery("select date from Todo t where t.title='Pintar cocina'")

5.4.3. Consultas Criteria de Hibernate

El último método para realizar consultas a la base de datos se refiere a la utilización de Criteria, un API de Hibernate diseñado específicamente para la realización de consultas complejas. En Grails, Criteria está basado en un Builder de Groovy y el código mostrado a continuación es un ejemplo de utilización de éste. En este ejemplo vamos a ver como podríamos obtener un listado de las siguientes 15 tareas a realizar en los próximos 10 días ordenadas ascendentemente.

void nextTodos() {
    def c = Todo.createCriteria()
    def result = c{
        between("date",new Date(),new Date()+10)
        maxResults(15)
        order("date","asc")
    }
}

Criteria dispone de una serie de criterios disponibles para ser utilizados en este tipo de consultas. En el ejemplo hemos utilizado between, maxResults y order, pero existen muchos más que vamos a ver en la siguiente tabla.

Criterio Descripción Ejemplo

between

Cuando el valor de la propiedad se encuentra entre dos valores

between("date",new Date()-10, new Date())

eq

Cuando el valor de una propiedad es igual al valor pasado por parámetro

eq("make","volkswagen")

eqProperty

Cuando dos propiedades tienen el mismo valor

eqProperty("startDate","endDate")

gt

Cuando el valor de una propiedad es mayor que el valor pasado por parámetro

gt("date",new Date()-5)

gtProperty

Cuando el valor de una propiedad es mayor que el valor de la propiedad pasada por parámetro

gtProperty("startDate","endDate")

ge

Cuando el valor de una propiedad es mayor o igual que el valor pasado por parámetro

ge("date", new Date()-5)

geProperty

Cuando el valor de una propiedad es mayor o igual que el valor de la propiedad pasada por parámetro

geProperty("startDate", "endDate")

idEq

Cuando el identificador de un objeto es igual al valor pasado por parámetro

idEq(1)

ilike

La sentencia SQL like, pero en modo case insensitive

ilike("name", "fran%")

in

Cuando el valor de la propiedad es cualquiera de los valores pasados por parámetro. In es una palabra reservada en Groovy, así que debemos utilizar las comillas simples

'in'("age",[18..65])

isEmpty

Cuando la propiedad se encuentra vacía

isEmpty("description")

isNotEmpty

Cuando la propiedad no se encuentra vacía

isNotEmpty("description")

isNull

Cuando el valor de la propiedad es null

isNull("description")

isNotNull

Cuando el valor de la propiedad no es null

isNotNull("description")

lt

Cuando el valor de una propiedad es menor que el valor pasado por parámetro

lt("startDate",new Date()-5)

ltProperty

Cuando el valor de una propiedad es menor que el valor de la propiedad pasada por parámetro

ltProperty("startDate","endDate")

le

Cuando el valor de una propiedad es menor o igual que el valor pasado por parámetro

le("startDate",new Date()-5)

leProperty

Cuando el valor de una propiedad es menor o igual que el valor de la propiedad pasada por parámetro

leProperty("startDate","endDate")

like

La sentencia SQL like

like("name","Fran%")

ne

Cuando el valor de una propiedad no es igual al valor pasado por parámetro

ne("model","volkswagen")

neProperty

Cuando el valor de una propiedad no es igual al valor de la propiedad pasada por parámetro

neProperty("startDate","endDate")

order

Ordena los resultados por la propiedad pasada por parámetro y en el orden especificado (asc o desc)

order("name","asc")

rlike

Similar a la sentencia SQL like, pero utilizando expresiones regulares. Sólo está disponible para Oracle y MySQL

rlike("name",/Fran.+/)

sizeEq

Cuando el tamaño del valor de una determinada propiedad es igual al pasado por parámetro

sizeEq("name",10)

sizeGt

Cuando el tamaño del valor de una determinada propiedad es mayor al pasado por parámetro

sizeGt("name",10)

sizeGe

Cuando el tamaño del valor de una determinada propiedad es mayor o igual al pasado por parámetro

sizeGe("name",10)

sizeLt

Cuando el tamaño del valor de una determinada propiedad es menor al pasado por parámetro

sizeLt("name",10)

sizeLe

Cuando el tamaño del valor de una determinada propiedad es menor o igual al pasado por parámetro

sizeLe("name",10)

sizeNe

Cuando el tamaño del valor de una determinada propiedad no es igual al pasado por parámetro

sizeNe("name",10)

Los criterios de búsqueda se pueden incluso agrupar en bloques lógicos. Estos bloques lógicos serán del tipo AND, OR y NOT. Veamos algunos ejemplos.

and {
    between("date", new Date()-10, new Date())
    ilike("content", "%texto%")
}

or {
    between("date", new Date()-10, new Date())
    ilike("content", "%texto%")
}

not {
    between("date", new Date()-10, new Date())
    ilike("content", "%texto%")
}

5.5. Servicios

En cualquier aplicación que utilice el patrón Modelo Vista Controlador, la capa de servicios debe ser la responsable de la lógica de negocio de nuestra aplicación. Si conseguimos esto, nuestra aplicación será fácil de mantener y evolucionará de forma sencilla y eficiente.

Cuando implementamos una aplicación siguiendo el patrón MVC, el principal error que se comete es acumular demasiado código en la capa de control. Si en lugar de hacer esto, implementásemos servicios encargados de esta tarea, nuestra aplicación sería más fácil de mantener. Pero, ¿qué son los servicios en Grails?.

Siguiendo el paradigma de convención sobre configuración, los servicios no son más que clases cuyo nombre termina en Service y que se ubican en el directorio grails-app/services.

En la aplicación que estamos desarrollando como ejemplo, vamos a crear un método en un servicio que devuelva aquellas tareas que estén planeadas para los próximas X días. Para ello, en primer lugar debemos crear el esqueleto del servicio con el comando grails create-service es.ua.expertojava.todo.todo. Este comando, además del servicio, también creará un test unitario. La clase de servicio que hemos creado tiene la siguiente estructura:

package es.ua.expertojava.todo

import grails.transaction.Transactional

@Transactional
class TodoService {

    def serviceMethod() {

    }
}

Como vemos, la clase creada especifica la anotación @Transactional que indica que la clase completa con todos los métodos que añadamos serán transaccionales. Esto significa que si en algún método se produce algún error, las operaciones realizadas hasta el momento del error serán desechadas. Sin embargo, existe una forma de ser algo más selectivo e indicar que métodos serán transaccionales y cuales no. Esto se hace también con anotaciones y este es un ejemplo de lo que podríamos tener:

import grails.transaction.Transactional

class BookService {

    @Transactional(readOnly = true)
    def listBooks() {
        Book.list()
    }

    @Transactional
    def updateBook() {
        // ...
    }

    def deleteBook() {
        // ...
    }
}

En el método listBooks(), le estamos especificando que el método será transaccional de solo lectura. Por otro lado, el método updateBook() será transaccional de lectura-escritura y por último, y que sirva únicamente como ejemplo, tenemos el método deleteBook() que al no indicarle nada, le estamos diciendo a Grails que no es transaccional (por supuesto, esta operación debería ser transaccional. Tómalo únicamente como un ejemplo).

Cuando creábamos el servicio TodoService, Grails generaba automáticamente un método vacío llamado serviceMethod() que ahora vamos a sustituir por uno propio que devuelva una serie de tareas comprendidas entre un par de fechas. El método recibirá una fecha inicio y fin y además, un mapa con una serie de parámetros que utilizaremos para paginar en caso de que sea necesario. El método devolverá una colección de instancias de la clase Todo. También crearemos un método que devuelva un contador de cuantas tareas coinciden con ese criterio.

package es.ua.expertojava.todo

import grails.transaction.Transactional

class TodoService {

    def getNextTodos(Integer days, params) {
        Date now = new Date(System.currentTimeMillis())
        Date to = now + days
        Todo.findAllByDateBetween(now, to, params)
    }

    def countNextTodos(Integer days) {
        Date now = new Date(System.currentTimeMillis())
        Date to = now + days
        Todo.countByDateBetween(now, to)
    }
}

El siguiente paso, será modificar el controlador de la clase de dominio Todo para crear un nuevo método que utilice este servicio recien creado. El método lo vamos a llamar listNextTodos().

class TodoController {
    def todoService

    ....

    def listNextTodos(Integer days) {
        respond todoService.getNextTodos(days, params),
                model:[todoInstanceCount: todoService.countNextTodos(days)],
                view:"index"
    }

    ....
}

Al inicio del controlador debemos crear también la variable def todoService para poder utilizar sus servicios. La convención es que esta variable debe llamarse igual que el servicio salvo que la primera letra será minúscula. De esta forma, una instancia del servicio correspondiente se inyectará convenientemente en nuestro controlador, con lo que no debemos preocuparnos por su ciclo de vida.

El método es muy similar al método index() con la excepción de que ahora le pasaremos el parámetro view:"index" para que cuando la petición se realice desde una aplicación web, esta sea renderizada en una página HTML.

Con esto tenemos que si accedemos a través del navegador a la url http://localhost:8080/todo/todo/listNextTodos?days=2 la aplicación nos mostrará las tareas que tengamos que hacer en los próximos dos días. Si por otro lado ejecutamos desde la línea de comandos:

curl -v -H "Accept: text/xml" -H "Content-type: text/xml" -X GET  http://localhost:8080/todo/todo/listNextTodos?days=2

el resultado será un xml con el contenido de las próximas tareas. En el caso en que necesitemos el resultado en formato JSON:

curl -v -H "Accept: application/json" -H "Content-type: application/json" -X GET  http://localhost:8080/todo/todo/listNextTodos?days=2

5.5.1. Ámbito de los servicios

Por defecto, todos los servicios en Grails son de tipo singleton, es decir, sólo existe una instancia de la clase que se inyecta en todos los artefactos que declaren la variable correspondiente. Esto puede servirnos para la mayoría de las situaciones, pero tiene un inconveniente y es que no es posible guardar información privada de una petición en el propio servicio puesto que todos los controladores verían la misma instancia y por lo tanto, el mismo valor. Para solucionar esto, podemos declarar una variable scope con cualquiera de los siguiente valores:

  • prototype: cada vez que se inyecta el servicio en otro artefacto se crea una nueva instancia

  • request: se crea una nueva instancia para cada solicitud HTTP

  • flash: cada instancia estará accesible para la solicitud HTTP actual y para la siguiente

  • flow: cuando declaramos el servicio en un web flow, se creará una instancia nueva en cada flujo

  • conversation: cuando declaramos el servicio en un web flow, la instancia será visible para el flujo actual y todos sus sub-flujos (conversación)

  • session: se creará una instancia nueva del servicio para cada sesión de usuario

  • singleton: sólo existe una instancia del servicio

Por lo tanto, si queremos modificar el ámbito del servicio para que cada sesión de usuario tenga su propia instancia del servicio, debemos declararlos así:

class TodoService{
    static scope = 'session'
    ...
}

5.5.2. Servicios REST

Ya hemos visto durante las sesiones anteriores que es muy sencillo generar en Grails una aplicación que sepa responder a las distintas peticiones de una aplicación web, bien sea a través de una página web o bien desde una aplicación nativa utilizando REST. Pero, ¿qué es exactamente REST?

Los servicios web REST son algo más que devolver datos en formato XML o JSON sino que consiste en una forma de trabajar que en función del método HTTP utilizado en la petición se debe procesar mediante una acción u otra, tal y como se representa en la siguiente tabla.

Método Acción

GET

show()

PUT

update()

POST

save()

DELETE

delete()

A partir de esta tabla, sólo necesitaríamos una url para poder realizar todas las operaciones sobre nuestras clases de dominio. Imagina por ejemplo las operaciones de tipo CRUD sobre los usuarios. Con REST, sólo deberíamos tener una posible url que sería http://localhost:8080/todo/todo/id_todo, donde el identificador de la tarea sería optativo. Veamos en otra tabla como quedarían todas las posibles peticiones para realizar las operaciones del usuario.

URL Método HTTP Acción

http://localhost:8080/todo/todos

GET

show(), debería mostrar un listado de las tareas

http://localhost:8080/todo/todos/4

GET

show(), debería mostrar los datos de la tarea con id = 4

http://localhost:8080/todo/todos/4

PUT

update(), debería actualizar los datos de la tarea con id = 4

http://localhost:8080/todo/todos

POST

save(), debería crear una nueva tarea con los datos pasados en el formulario

http://localhost:8080/todo/todos/4

DELETE

delete(), debería eliminar la tarea con id = 4

En Grails la forma más sencilla de exponer clases de dominio como servicios REST es mediante entradas en el archivo de configuración /conf/UrlMappings.groovy, que veremos más adelante en profundidad.

class UrlMappings {

    static mappings = {
        "/$controller/$action?/$id?(.$format)?"{
            constraints {
                // apply constraints here
            }
        }

        "/api/todos"(resources:"todo")
        "/api/tags"(resources:"tag")
        "/api/categories"(resources:"category")

        "/"(view:"/index")
        "500"(view:'/error')
    }
}

De esta forma estaremos exponiendo nuestras clases de dominio como recursos REST que podrán ser accedidos de la siguiente forma:

5.6. Ejercicios

5.6.1. Construye tus propias restricciones (0.25 puntos)

En la clase de dominio de las tareas tenemos tanto una fecha para la tarea como una fecha de recuerdo. Vamos a imponer como restricción que la fecha para recordar la tarea deba ser siempre anterior a la fecha de realización de la misma. Crea para ello una restricción que nos permita conseguir esto.

Crea también una nueva propiedad en las etiquetas para especificar un color. Este color vendrá en formato RGB, que te recuerdo está especificado por el carácter # seguido de 6 caracteres hexadecimales, como por ejemplo #23FA8F. Define esta restricción globalmente con el nombre rgbcolor para que pueda ser reutilizada en cualquier otra propiedad de nuestra aplicación.

5.6.2. Asegurar los borrados de las categorías y las tareas (0.25 puntos)

En la aplicación actualmente tenemos un problema que nos impide eliminar categorías por la relación con las tareas y la clave ajena que se genera a nivel de base de datos con lo que antes de proceder al borrado de la categoría, debemos asegurarnos que ésta no esté asignada a ninguna tarea.

Crea el servicio CategoryService.groovy y mueve el eliminado que ahora mismo se está realizando en el controlador CategoryController.groovy para que asigne a null todas las tareas de la categoría a eliminar.

Soluciona al mismo tiempo el problema existente a la hora de eliminar tareas por la relación existente entre ellas y las etiquetas. Mueve el borrado de las tareas a un método en el servicio adecuado.

5.6.3. Consultas a base de datos (0.25 puntos)

Vamos a crear un nuevo informe en nuestra aplicación que nos permita listar todas las tareas ordenadas por fecha y cuya categoría coincida con las pasadas por parámetro. Necesitaremos crear una vista y su correspondiente método (listByCategory()) en el controlador TodoController() para poder seleccionar aquellas categorías de las que queramos listar sus tareas.

A continuación, deberás implementar un nuevo método en el controlador TodoController() que devuelva todas las tareas cuya categoría coincidan con aquellas que el usuario haya seleccionado en el paso anterior. En esta vista puedes mostrar todas las categorías junto a un checkbox para que el usuario pueda seleccionar varios al mismo tiempo.

Por último, edita los menús de navegación de la aplicación para añadir el nuevo informe que acabamos de generar en todas páginas relativas a las tareas.

5.6.4. ¿Cuándo han sido realizadas las tareas? (0.50 puntos)

El autotimestamp es una característica de Grails que se suele utilizar para saber cuando se crean y actualizan las instancias de nuestras clases de dominio. En primer lugar vamos a añadir esta característica de Grails a las tareas.

A continuación, vamos a crear una nueva propiedad en las tareas que nos permite saber cuando se han realizado las tareas y llama a esta propiedad dateDone. Para rellenar esta propiedad vamos a crear un servicio que se encargue de esta lógica y a su vez, mover el guardado que se está realizando actualmente en el controlador.

Para ello añadiremos en el servicio TodoService un nuevo método llamado saveTodo(Todo todoInstance) de tal forma que cuando la propiedad done sea cierta, completaremos con la fecha actual la nueva propiedad dateDone. Este método deberá ser utilizado tanto cuando se crea como cuando se actualiza una tarea.

A continuación, crearemos un nuevo método en el servicio TodoService() llamado lastTodosDone(Integer hours) que nos permitirá pasar como parámetro un número de horas y nos devuelva todas las últimas tareas realizadas en ese intervalo de tiempo, es decir, desde el momento actual hasta hace hours horas.