Material‎ > ‎Software‎ > ‎Grails‎ > ‎

Demo: Introducción a Grails

Intro

Grails es un framework web basado en Java, que tiene
  • un lenguaje de scripting llamado Groovy, cuyo uso es opcional: puedo seguir programando en Java. Groovy termina generando código Java
  • una consola desde donde podemos invocar comandos (como la generación de vistas, controllers, la compilación de todo el proyecto en modo desarrollo, test, producción, etc.)
El framework no tiene un IDE específico sino que existen plugins para cada uno de los IDEs de Java existentes. Esta demo está basada en el STS (Spring Toolkit Suite), que utiliza como base un Eclipse con algunos plugins especiales para Grails. La versión de Grails es la estable para producción (1.3.7)

Ejemplo básico: los celulares

Tomaremos como ejemplo el conocido enunciado de los clientes para una compañía de celulares.

Creamos un proyecto Grails

File > New > Grails project ... lo llamamos celulares-ui-grails (todo default: Use default Grails installation)

Definimos objetos de dominio

Parado sobre "domain", hacemos New > Domain class y creamos 
  • ModeloCelular
  • Celular

Los objetos de dominio se definen utilizando un DSL (Domain Specific Language): esto quiere decir que tenemos un lenguaje particular para este tipo de objeto. Por ejemplo, al crear el modelo de celular podemos definir que el nombre (que es un String) tenga un tamaño de 1 a 25 caracteres:

class ModeloCelular {

String nombre
    static constraints = {
nombre(size:1..25)
    }
}

El DSL está construido en Groovy, un lenguaje de scripting que termina generando código Java, pero que no necesita punto y coma (;) 

El Celular lo definimos indicando que el nombre puede tener una longitud máxima de 100 caracteres:

class Celular {

Integer numero
String nombre
ModeloCelular modelo
boolean recibeResumenCuenta
    static constraints = {
nombre(maxSize:100)
    }
}

Y listo... ya podemos generar nuestra app invocando a la consola de Groovy (Ctrl + Alt + Shift + G):

generate-all *

Entonces se genera el scaffolding: la construcción default de la programación web necesaria para generar los ABM de Celular y de ModeloCelular.

Lo probamos, invocando nuevamente a la consola Groovy:

run-app

Cuando termina de deployar el war en el Tomcat embebido, nos muestra el link. Nosotros lo abrimos desde un Browser común (ocupa menos memoria).
http://localhost:8080/celulares-ui-grails

Convention over configuration

Grails utiliza por default un esquema de convención que como hemos visto anteriormente evita tener que definir archivos de configuración (aunque podemos cambiar este comportamiento). Para ir al link principal del ABM del Celular, si sabemos que la entidad se llama "Celular", escribimos en el browser:


Si queremos visualizar el primer modelo de celular (sabiendo que su id es 1), escribimos:


Agregando validaciones

Ya podemos ver que algunas validaciones vienen "solas": no podemos dejar vacío el modelo ni el número (tenemos un div en rojo donde vemos todos los errores de validación juntos). También nos aparece un mensaje descriptivo al intentar guardar caracteres en el número de línea. Puede pasarnos que el mensaje sea un tanto extraño: 

La propiedad [modelo] de la clase [class celulares.ui.grails.Celular] no puede ser nulo 

Ehm. Pero lo bueno es que eso se puede cambiar.

Definimos nuevas constraints: que el número deba ser mayor a 1000, y que haya al menos 5 caracteres de nombre

class Celular {

    Integer numero
    String nombre
    ModeloCelular modelo
    boolean recibeResumenCuenta
    static constraints = {
        numero(length:10, min: 1001)
        nombre(blank:false, size:5..30)
    }
}

Al grabar vemos que se recompila la aplicación y el DSL agrega las nuevas validaciones (ya no podemos crear un celular cuyo número sea <= a 1000 ni un nombre de menos de 5 caracteres).

Generamos dos modelos de celular primero y vemos cómo queda el ABM de Celulares. El combo de modelos muestra un extraño 
"ar.edu....ModeloCelular : 1"
"ar.edu....ModeloCelular : 2", etc.

Esto es porque tenemos que redefinir el toString() default que muestra el nombre completo de la clase (con package) y el id, un identificador que Grails mismo define para todos los objetos de dominio, ya que asume que se mapean contra una base de datos relacional, por esto también se llaman GORM (Grails Object Relational Mappings).

Redefinimos el toString á la Grails:

String toString() {
return nombre
}

nombre es una propiedad (definida como un atributo de la entidad ModeloCelular).
Otra forma de definirlo es a través del poderoso modificador $:

String toString() {
"${nombre}"
}

Ahora sí vemos en el combo el nombre del modelo del celular.

Scaffolding: detrás de las cámaras

¿Cómo funciona todo esto que hemos visto? Por cada entidad Grails crea
  • Vistas: definen el layout y los widgets a través de páginas gsp, que son páginas similares a jsp con un Expression Language propio de Grails + HTML + ... javascript! 
  • Controllers: se programan en groovy y definen comportamiento server side, como los métodos index (que se invoca cuando se carga la página), show (si se invoca a la página en modo sólo lectura), etc.etc.
Con respecto a las domain classes, se genera comportamiento adicional
  • mediante metaprogramación se agregan métodos CRUD (create, retrieve, update, delete) contra el framework de persistencia que elijamos. Estos métodos se generan en base a las propiedades: en el caso de un Celular aparecerá un método findByNumero, findByNombre, findByNombreAndNumero, etc. Estos métodos no los tenemos que programar (ni son responsabilidad nuestra, los genera Grails)
  • cada propiedad define también sus métodos getters y setters, no necesitamos definirlo. El framework siempre trabaja sobre esta convención cuando utilizamos la propiedad de un objeto de dominio, pero esto es transparente para nosotros.
  • ¿Dónde graba todo esto? 
Tenemos que ir al directorio conf y chequear el archivo DataSource.groovy

environments {
    development {
        dataSource {
            dbCreate = "create-drop" // one of 'create', 'create-drop','update'
            url = "jdbc:hsqldb:mem:devDB"
        }
    }
    test {
        dataSource {
            dbCreate = "update"
            url = "jdbc:hsqldb:mem:testDb"
        }
    }
    ...

Una buena noticia es que puedo configurar distintos ambientes para trabajar en forma local contra una base SQL Server y en producción contra un Oracle, eso es independiente. El default es trabajar contra una base "en memoria", de manera que los cambios no se persisten. Podríamos trabajar contra una base y cambiar el dbCreate = "update", para que cada vez que levante la app no tengamos que crear todo desde cero.

Otro archivo interesante es el BootStrap.groovy, donde podemos agregar modelos de celular de prueba como hemos hecho en nuestros ejemplos anteriores (equivale al comportamiento que hemos codificado en las clases home):

class BootStrap {

    def init = { servletContext ->
new ModeloCelular(nombre: "Nokia 1100").save()
new ModeloCelular(nombre: "Motorola I80").save()
new ModeloCelular(nombre: "Samsung T Gravity").save()
    }

def destroy = {
    }
}

+ Ctrl + Shift + O para importar la entidad ModeloCelular.

Aquí vemos la forma en que Groovy nos libera de tener que definir Builders para crear objetos. Además al objeto de dominio le enviamos el mensaje save() porque también es una entidad que mapea (GORM se decía).

Paramos el server y volvemos a ejecutar run-app desde la consola Grails. Al definir un nuevo celular vemos el combo con los tres modelos.

Todo termina corriendo en una plataforma más que estándar, donde el principal sponsor es Spring.

Diseñando en Grails
Por último hacemos la búsqueda de libros.

¿Qué tiene un libro?
  • Título
  • Autor
ambos son Strings. No nos importa mucho la generación, pero sí nos gustaría que al buscar lo que escribamos sirva para buscar título o autor.

Generamos la domain class:

class Libro {

String titulo
String autor
    static constraints = {
titulo(size: 1..50)
autor(size: 1..30)
    }
}

Ahora vamos a tomar el gsp que lista y vamos a generar una copia del scaffolding, para hacer algunos cambios:
  • Vamos a views/libro y copiamos list.gsp en un directorio "gemelo": 
    • views/busquedaLibros/list.gsp (es importante el nombre list para respetar el convention y no obligar al usuario a escribir el nombre de la página gsp)
    • Le sacamos el botón que agrega un libro: 
<span class="menuButton"><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></span>

    • Lo reemplazamos por otro span que incorpora un textbox y un botón que llama a buscar: 
            <span class="menuButton">
            <g:textField name="inputBusqueda" value="${inputBusqueda}"/>
            <a class="list" href="list">Buscar libro
            </a>
            </span>

No nos importa mucho que quede precioso, nos concentramos más en la funcionalidad.
La vista list no tiene un form, si queremos guardar el input de búsqueda vamos a tener que incorporarlo, después del body:

    <body>
    <form name="list">
        ...

y luego por supuesto cerrar el tag form al final de todo.
  • Copiamos el controller LibroController a otro que se llame, por convención, igual que el directorio nuevo donde generamos la vista que se sale del scaffolding, en este caso BusquedaLibrosController (vale New > Controller o pararse en LibroController... Copy > y renombrarlo a BusquedaLibrosController)

class BusquedaLibrosController {
    static allowedMethods = [save: "POST", update: "POST", delete: "POST"]
def libroService
    def index = {
        redirect(action: "list", params: params)
    }

def list = {
params.max = Math.min(params.max ? params.int('max') : 10, 100)
def librosBusqueda = libroService.buscar(params.inputBusqueda)
[libroInstanceList: librosBusqueda, libroInstanceTotal: librosBusqueda.size(), inputBusqueda: params.inputBusqueda]
}

}

Algunas cuestiones:
  • el index define el comportamiento default de la página, en nuestro caso redirigimos la acción al list
  • el list activa parámetros de paginación (max), recibe el parámetro (no hay adapter en este caso, el string se pasa directo) y lo envía a ...
  • ... un objeto Service que es el que termina resolviendo la búsqueda contra el GORM. 
  • No necesitamos instanciar al objeto libroService, Grails lo instancia solito por convención si lo definimos como variable de instancia de un controller (si lo usamos desde otro lugar sí debemos instanciarlo nosotros)
  • Este objeto Service es nuevo para nosotros, el scaffolding no lo usa pero es recomendable tener un objeto que se encargue de:
    • abrir y cerrar las transacciones cuando corresponda: en el caso de una búsqueda de libros no necesitamos hacer "commit" a la base, pero sí nos va a pasar cuando hagamos inserts, deletes o updates
    • disparar consultas por criterios o pedir a los objetos GORM que se actualicen
    • traer el grafo de objetos en base a las relaciones que haya en la base
    • delegar a los objetos de dominio validaciones o cosas propias del negocio
  • Vemos cómo implementar una búsqueda mediante un dynamic finder, que se generan por metaprogramación con las propiedades de los objetos de dominio:
class LibroService {

    static transactional = true

def buscar(inputBusqueda) {
def likeBusqueda = "%" + inputBusqueda + "%"
return Libro.findAllByTituloLikeOrAutorLike(likeBusqueda, likeBusqueda)
}
}
  • El método findAllByTituloLikeOrAutorLike no está "definido" pero funciona en runtime porque se crea dinámicamente. 
  • También podemos construir criterios (tiene su propio DSL para búsqueda), aquellos que hayan trabajado con Hibernate les sonará muy similar.
  • Vemos cómo queda la aplicación: hacemos la búsqueda "ta" y nos traemos títulos de libros que contienen "ta" como "Las venas abiertas de América Latina" y autores que contienen "ta" como Cortazar.
  • Para ver cómo el controller funciona de adapter para el service, podemos reemplazar la búsqueda del service haciendo que reciba un objeto Libro, pero sin cambiar la interfaz (como se habrán dado cuenta, Groovy no chequea tipos en tiempo de compilación):
>>LibroController
def list = {
params.max = Math.min(params.max ? params.int('max') : 10, 100)
def libroExample = new Libro(titulo: params.inputBusqueda, autor: params.inputBusqueda)
def librosBusqueda = libroService.buscar(libroExample)
[libroInstanceList: librosBusqueda, libroInstanceTotal: librosBusqueda.size(), inputBusqueda: params.inputBusqueda]
}

>>LibroService
def buscar(libroExample) {
return Libro.findAllByTituloLikeOrAutorLike("%" + libroExample.titulo + "%", "%" + libroExample.autor + "%")
}

Todo es muy bueno...

Sí, demasiado bueno para ser cierto, ¿no? Algunas consideraciones:
  • El scaffolding y el deseo el usuario es común que difieran.
  • Si lo que genera Grails no es del gusto del usuario, tenemos dos opciones:
    • lo que hace la mayoría, genera una copia del scaffold y modifica el código (o sea, programa)
    • o modifica el scaffolding para ajustarlo a los requerimientos del usuario: esto no es trivial, requiere entender cómo lograr que nuestro lenguaje de scripting haga lo que queremos que haga cuando nos pongamos a definir el dominio. 
  • A los IDEs les falta maduración para acompañar las ventajas de esta tecnología
    • Muy floja la parte de refactors (el rename mismo de entidades y propiedades es insuficiente, no hablemos de un pull-up o un extract method)
    • A veces se traban los imports, o no saltan errores de compilación
  • Grails genera rápidamente los CRUDs (ABMs) que forman parte de la parametrización del sistema. Pero lo más problemático nunca es hacer 30 ABM iguales de entidades. Lo que más se lleva tiempo es hacer un Wizard de siete páginas, o el formulario principal que carga un pedido con muchos popups y grillas. Por eso no existe magia, Groovy facilita mucho pero la parte client-side sigue teniendo muchas cuestiones javascript que hay que resolver como el resto de los frameworks (con prototype, scriptaculous, jQuery, etc.)

¿Qué IDE me bajo?

  • STS - Spring Toolkit Suite (es el Eclipse que viene con plugins de Spring) - free
  • Otros IDEs como NetBeans, GEdit, etc.
  • IntelliJ - es pago
¿Dónde investigar más?