Introducción a Groovy y Grails con Maven: el patrón CRUD
Índice de contenidos
Resumen
1. Introducción a Grails y Groovy
2. Requisitos iniciales
3. Generación del proyecto base mediante el plugin Archetype de Maven
4. Generación del proyecto base desde el arquetipo Maven
4.1. Inicializando la aplicación
4.2. Instalando las plantillas utilizadas por defecto
5. Compilando y probando la aplicación de ejemplo
6. El plugin de Grails para Maven
6.1. Añadir un plugin de Grails al proyecto
7. Introducción al patrón MVC y al CRUD en Grails
7.1. Las plantillas de los formularios
7.2. Un ejemplo sencillo de plantilla modificada list.gsp
8. El plugin de Fields
8.1. Instalación del plugin de Fields
9. Los controladores, la persistencia y el lenguaje Groovy
10. Conclusión
Resumen
En este artículo echaremos un primer vistazo a Grails, el servidor de aplicaciones para el lenguaje Groovy. Además haremos un pequeño ejemplo implementando el patón CRUD (consultas, altas, bajas y modificaciones), que es la base de cualquier aplicación de gestión.
Para hacer esto más sencillo vamos a utilizar como plataforma de desarrollo Maven, ya que nos permite poner en marcha el proyecto de ejemplo en poco tiempo.
1. Introducción a Grails y Groovy
Groovy es un lenguaje interpretado basado en Java al estilo de Ruby y Grails es un servidor de aplicaciones escritas en Groovy al estilo de Ruby on Rails o de PHP con Simphony. Tiene bastantes características interesantes, como es su compatibilidad con Java (podemos usar libremente las clases de java en nuestros proyectos), tipado fuerte y débil y una sintaxis rápida y sencilla, que utiliza bastantes convenios que a aquellos que venimos de Java al principio nos chocan.
Grails es un servidor de aplicaciones que permite ejecutar aplicaciones escritas en Groovy. Algunas de sus características relevantes son:
- Usa convención frente a configuración en para saber qué tiene que hacer. Por ejemplo si una clase Book.grrovy está en grails-app/domain, ya sabe que esa clase declara una entidad del modelo de datos.
- Está preparado para ejecutar aplicaciones que siguen el patrón MVC modelo vista controlador.
- El código fuente es interpretado, basado en ficheros escritos en lenguaje Groovy y páginas web generadas mediante plantillas GSP, que son bastante parecidas a los tradicionales JSPs de J2EE.
- Arquitectura basada en plugins escritos en Groovy. Esto le permite extender su funcionalidad.
- Acceso directo a todos los paquetes java estándar, con soporte para Maven
- Gestor de ciclo de vida basado en Gradle, que es bastante compatible con Maven, aunque no sigue su misma filosofía.
- Soporte nativo para mapeo de bases de datos a Groovy mediante el GORM (basado en Hibernate), con gestión automática de la base de datos subyacente.
- Sistema de generación de modelo, vista y controladores para implementar un CRUD integrado en Grails
- Y por último, las aplicaciones generadas en Grails se pueden empaquetar como WAR y desplegar en un servidor de aplicaciones estándar o en un Tomcat.
2. Requisitos iniciales
El ejemplo lo voy a realizar sobre Ubuntu 14.04 Desktop, que podemos instalar en una máquina virtual usando VirtualBox con mucha facilidad. Ya sabéis, las pruebas con gaseosa y en casa.
3. Generación del proyecto base mediante el plugin Archetype de Maven
Desde Ubuntu, en principio instalamos Maven 3 con este comando:
$ sudo apt-get install openjdk-7-jdk $ sudo apt-get install maven $ mvn --version Warning: JAVA_HOME environment variable is not set. Apache Maven 3.0.5 Maven home: /usr/share/maven Java version: 1.7.0_55, vendor: Oracle Corporation Java home: /usr/lib/jvm/java-7-openjdk-i386/jre Default locale: es_ES, platform encoding: UTF-8 OS name: "linux", version: "3.13.0-29-generic", arch: "i386", family: "unix"
Si no lo teníamos instalado nos instalará todo el JDK para Java y dejará todo listo para comenzar a crear nuestra aplicación Fácil. Limpio. Rápido…. Pero tiene un problema: en Ubuntu 14.04 nos instala Apache Maven 3.0.5, que no nos vale, ya que el plugin de Grails para Maven requiere Maven 3.1 o posterior. Por ello vamos a instalar una más reciente, por ejemplo Maven 3.2.1 sobre nuestro Ubuntu.
Para instalar una versión más reciente hay que hacer varios pasos:
$ cd ~ $ wget http://apache.rediris.es/maven/maven-3/3.2.1/binaries/apache-maven-3.2.1-bin.tar.gz $ sudo mkdir -p /usr/local/apache-maven $ sudo mv apache-maven-3.2.1-bin.tar.gz /usr/local/apache-maven/ $ cd /usr/local/apache-maven/ $ sudo tar -xzvf apache-maven-3.2.1-bin.tar.gz $ cd ~ $ nano .profile
Ahora editamos el .profile y le añadimos estas líneas al final:
export M2_HOME=/usr/local/apache-maven/apache-maven-3.2.1 export M2=$M2_HOME/bin export MAVEN_OPTS="-Xms256m -Xmx512m" export PATH=$M2:$PATH
Cerramos la sesión y volvemos a iniciarla, para coger los cambios del .profile. Y probamos el nuevo Maven:
$ mvn --version Apache Maven 3.2.1 (ea8b2b07643dbb1b84b6d16e1f08391b666bc1e9; 2014-02-14T18:37:52+01:00) Maven home: /usr/local/apache-maven/apache-maven-3.2.1 Java version: 1.7.0_55, vendor: Oracle Corporation Java home: /usr/lib/jvm/java-7-openjdk-i386/jre Default locale: es_ES, platform encoding: UTF-8 OS name: "linux", version: "3.13.0-29-generic", arch: "i386", family: "unix"
4. Creación del proyecto base desde el arquetipo Maven
Para comenzar creamos un directorio para el proyecto:
$ mkdir -p ~/workspaces/workspaceGrailsdemo $ cd ~/workspaces/workspaceGrailsdemo
NOTA: esta estructura de carpetas está especialmente diseñada para trabajar con Eclipse y Netbeans. Mi consejo es:
- Meter todos vuestros desarrollos en la carpeta workspaces
- Por cada proyecto o desarrollo crear dentro de ella una carpeta workspacenombreproyecto y dentro de ella las carpetas de los proyectos
- Esto último facilita mucho abrir los proyectos en Eclipse y luego importarlos
Mediante el uso del arquetipo Grails para Maven podemos generar de manera sencilla el proyecto base. El arquetipo es generado mediante un plugin de Maven que se descarga automáticamente del repositorio central de Maven. Yo voy a usar en este tutorial una versión concreta del arquetipo, pero se pueden usar otras versiones, es cuestión de ir probando lo que nos funciona.
Nota importante: la versión elegida del plugin es muy importante, ya que no todas funcionan…. Por ejemplo yo he probado la versión 2.3.8 y con pruebas sencillas no he logrado levantar la aplicación con la configuración por defecto. La versión 2.2.4 inicializa y ejecuta la aplicación sin problemas, por lo que para tomar contacto con Grails es la apropiada.
Podemos saber la lista de versiones disponibles consultando directamente el directorio del plugin en el repositorio central de Maven. ¿no sabes cómo buscarlo? La ruta para encontrar un artefacto o plugin en el repositorio central de Maven es:
http://repo1.maven.org/maven/GROUP-ID/ARTIFACT-ID
Donde:
- GROUP-ID es el groupId convirtiendo los puntos por barras d, por ejemplo si el gropuId es org.grails, el GROUP-ID será org/grails
- El ARTIFACT-ID es el artifactId que tenemos que usar.
- NOTA: esto vale para CUALQUIER dependencia Maven
Y el comando a ejecutar para este arquetipo es:
$ mvn archetype:generate -DarchetypeGroupId=org.grails -DarchetypeArtifactId=grails-maven-archetype -DarchetypeVersion=2.2.4 -DgroupId=es.com.gonzalezalmiron.grailsdemo -DartifactId=grailsdemo [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building Maven Stub Project (No POM) 1 [INFO] ------------------------------------------------------------------------ [INFO] [INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom >>> [INFO] [INFO]
Si no te has dado cuenta ya, la ruta del arquetipo en Maven es:
https://repo1.maven.org/maven2/
Y veremos una carpeta por cada versión del plugin.
4.1. Inicializando la aplicación
Tras la ejecución del plugin de archetype lo único que tenemos en grailsdemo es un pom.xml y una carpeta src casi vacía, salvo el web.xml que hay en src/main/webapp/WEB-INF. Parece poco, pero como siempre hay que terminar de trabajar en Maven. Inicializamos el proyecto.
~/workspaces/workspaceGrailsdemo/grailsdemo$ mvn initialize [INFO] Scanning for projects... …. Descarga un montón de paquetes desde el repositorio central….. [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building A custom grails project 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- grails-maven-plugin:2.2.4:validate (default-validate) @ grailsdemo --- [INFO] No Grails application found - skipping validation. [INFO] [INFO] --- grails-maven-plugin:2.2.4:init (default-init) @ grailsdemo --- [INFO] Cannot read application info, so initialising new application. [WARNING] Grails Start with out fork |Loading Grails 2.2.4 |Configuring classpath |Running pre-compiled script . |Environment set to development ........................................... |Created Eclipse project files. . |Created Grails Application at /home/usuario/workspaces/workspaceGrailsdemo/grailsdemo [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 17.625s [INFO] Finished at: Tue Jun 10 11:11:38 CEST 2014 [INFO] Final Memory: 21M/51M [INFO] ------------------------------------------------------------------------
4.2. Instalando las plantillas utilizadas por defecto
Este paso es un poco curioso. Lo que hace es instalar las plantillas en la carpeta /src/templates que luego se usan para generar los GSPs. Conviene hacerlo en este momento, pues luego cuando se instala el plugin de Fields ya no se puede hacer (por lo menos a mi me ha dado problemas):
$ mvn grails:install-templates [INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for es.com.gonzalezalmiron.grailsdemo:grailsdemo:grails-app:1.0-SNAPSHOT [WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: org.grails.plugins:cache:zip -> duplicate declaration of version 1.0.1 @ line 83, column 17 [WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-compiler-plugin is missing. @ line 143, column 15 [WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-surefire-plugin is missing. @ line 97, column 17 [WARNING] [WARNING] It is highly recommended to fix these problems because they threaten the stability of your build. [WARNING] [WARNING] For this reason, future Maven versions might no longer support building such malformed projects. [WARNING] [INFO] [INFO] Using the builder org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder with a thread count of 1 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building A custom grails project 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- grails-maven-plugin:2.2.4:install-templates (default-cli) @ grailsdemo --- [WARNING] Grails Start with out fork |Loading Grails 2.2.4 |Configuring classpath |Running pre-compiled script . |Environment set to development ...... |Templates installed successfully [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 8.722 s [INFO] Finished at: 2014-06-11T12:50:24+01:00 [INFO] Final Memory: 29M/247M [INFO] ------------------------------------------------------------------------ $ dir src/templates/ artifacts scaffolding testing wa $ dir src/templates/artifacts/ Controller.groovy Filters.groovy ScaffoldingController.groovy Service.groovy Tests.groovy DomainClass.groovy hibernate.cfg.xml Script.groovy TagLib.groovy WebTest.groovy $ dir src/templates/scaffolding/ Controller.groovy create.gsp edit.gsp _form.gsp list.gsp renderEditor.template show.gsp Test.groovy
El uso de las plantillas lo veremos más adelante.
5. Compilando y probando la aplicación de ejemplo
Ejecutando la aplicación:
$ mvn grails:run-app [INFO] Scanning for projects... [WARNING] [WARNING] Some problems were encountered while building the effective model for es.com.gonzalezalmiron.grailsdemo:grailsdemo:grails-app:1.0-SNAPSHOT [WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: org.grails.plugins:cache:zip -> duplicate declaration of version 1.0.1 @ line 83, column 17 [WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-compiler-plugin is missing. @ line 143, column 15 [WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-surefire-plugin is missing. @ line 97, column 17 [WARNING] [WARNING] It is highly recommended to fix these problems because they threaten the stability of your build. [WARNING] [WARNING] For this reason, future Maven versions might no longer support building such malformed projects. [WARNING] [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building A custom grails project 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- grails-maven-plugin:2.2.4:run-app (default-cli) @ grailsdemo --- [WARNING] Grails Start with out fork |Loading Grails 2.2.4 |Configuring classpath |Running pre-compiled script . |Environment set to development ................................. |Packaging Grails application |Installing zip tomcat-2.2.4.zip... ... |Installed plugin tomcat-2.2.4 .............. |Installing zip hibernate-2.2.4.zip... ... |Installed plugin hibernate-2.2.4 ... |Installing zip jquery-1.8.3.zip... ... |Installed plugin jquery-1.8.3 ... |Installing zip cache-1.0.1.zip... ... |Installed plugin cache-1.0.1 ... |Installing zip resources-1.2.zip... ... |Installed plugin resources-1.2 ... |Installing zip database-migration-1.3.2.zip... ... |Installed plugin database-migration-1.3.2 ...... |Compiling 119 source files .. |Compiling 8 source files ......... |Running Grails application |Server running. Browse to http://localhost:8080/grailsdemo
Podría ser que el Tomcat7 (puede ser otro servidor o aplicación) ya estuviera ocupando el puerto, por lo que debemos primero pararlo:
$ sudo lsof –i Buscamos el proceso que ocupa el pueto 8080. En mi caso era el Tomcat7 $ sudo service tomcat7 stop $ mvn grails:run-app …. Grails hace su trabajo |Loading Grails 2.2.4 |Configuring classpath |Running pre-compiled script . |Environment set to development ................................. |Packaging Grails application ............. |Running Grails application |Server running. Browse to http://localhost:8080/grailsdemo
Y vemos el resultado:

6. El plugin de Grails para Maven
Si instalamos Grails como servidor de aplicaciones y entorno de generación de aplicaciones, hay un montón de acciones que se pueden ejecutar desde Grails. Estas acciones suelen tener un equivalente Maven para ser ejecutadas desde el plugin de Maven.
En la documentación de Grails está toda la documentación del plugin de Grails para Maven… pero ojo, es bastante incompleta. Hay que averiguar muchas cosas mediante ensayo y error.
$ mvn grails:help
[INFO] ------------------------------------------------------------------------
[INFO] Building A custom grails project 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- grails-maven-plugin:2.2.4:help (default-cli) @ grailsdemo ---
[INFO] org.grails:grails-maven-plugin:2.2.4
Maven plugin for GRAILS applications
This plugin allows you to integrate GRAILS applications into maven 2 builds.
This plugin has 37 goals:
grails:clean
Cleans a Grails project.
grails:config-directories
Set sources/tests directories to be compatible with the directories layout
used by grails.
grails:console
Runs a Grails console inside the current project.
grails:create-controller
Creates a new controller.
grails:create-domain-class
Creates a new domain class.
grails:create-integration-test
Creates a new Grails integration test which loads the whole Grails environment
when run.
grails:create-pom
Creates a creates a maven 2 POM for an existing Grails project.
grails:create-script
Creates a Grails Gant Script.
grails:create-service
Creates a new service class.
grails:create-tag-lib
Creates a new GSP tag library.
grails:create-unit-test
Creates a new Grails unit test. A unit test requires that you mock out access
to dynamic methods, but executes a lot quicker.
grails:exec
Executes an arbitrary Grails command.
grails:generate-all
Generates a CRUD interface (controller + views) for a domain class.
grails:generate-controller
Generates a CRUD controller for a specified domain class.
grails:generate-views
Generates the CRUD views for a specified domain class.
grails:help
Display help information on grails-maven-plugin.
Call
mvn grails:help -Ddetail=true -Dgoal=
to display parameter details.
grails:init
Validate consistency between Grails and Maven settings.
grails:init-plugin
Validate consistency between Grails and Maven settings.
grails:install-templates
Installs the artifact and scaffolding templates.
grails:list-plugins
Lists the available plugins.
grails:maven-clean
Cleans a Grails project and jars in lib directory.
grails:maven-compile
Compiles a Grails project.
grails:maven-functional-test
Runs a Grails application's functional tests.
grails:maven-grails-app-war
Creates a WAR archive and register it in maven. This differs from the
MvnWarMojo in that it makes the WAR file the build artifact for the
'grails-app' packaging. The standard 'maven-war' goal is designed for the
normal 'war' packaging.
So why have two versions? Well, version 1.0 of the plugin was released with
the war packaging as the default for Grails projects. That hasn't worked out
so well, but we still need to support it, so we need one war target for the
war packaging type, and one for the grails-app packaging type (which is now
preferred over war).
grails:maven-test
Runs a Grails applications unit tests.
grails:maven-war
Creates a WAR archive for the project and puts it in the usual Maven place.
grails:package
Packages the Grails application into the web-app folder for running.
grails:package-plugin
Packages the Grails plugin.
grails:run-app
Runs a Grails application in Jetty.
grails:run-app-https
Runs a Grails application in Jetty with HTTPS listener.
grails:run-war
Runs a Grails application in Jetty from its WAR.
grails:set-version
Set the grails application version from the Maven POM version.
grails:test-app
Runs a Grails applications unit tests and integration tests.
grails:upgrade
Upgrades a Grails application to the version of this plugin.
grails:validate
Validate consistency between Grails and Maven settings.
grails:validate-plugin
Validate consistency between Grails and Maven settings.
grails:war
Creates a WAR archive.
Para saber qué propiedades podemos definir antes de llamar a una de las tareas del plugin de Grails usaremos este comando:
$ mvn help:describe -Ddetail -DartifactId=grails-maven-plugin -DgroupId=org.grails
Si queremos la ayuda de un goal complete usamos el parámetro goal:
$ mvn grails:help -Dgoal= create-domain-class -Ddetail
Por ejemplo para grails:create-domain-class:
grails:create-domain-class
Description: Creates a new domain class.
Implementation: org.grails.maven.plugin.GrailsCreateDomainClassMojo
Language: java
Available parameters:
activateAgent
User property: activateAgent
Whether to activate the reloading agent (forked mode only) for this
command
basedir (Default: ${basedir})
Required: true
The directory where is launched the mvn command.
domainClassName
User property: domainClassName
The name for the domain class to create.
env
User property: grails.env
The Grails environment to use.
extraClasspathEntries
Extra classpath entries as a comma separated list of file names. For
entries with a comma in their name, use backslash to escape. INTERNAL
This parameter is not meant to be used externally. It is used by IDEs
that require extra classpath entries to execute grails commands.
fork (Default: false)
User property: fork
Whether the JVM is forked for executing Grails commands
forkDebug (Default: false)
User property: forkDebug
Whether the JVM is forked for executing Grails commands
forkedVmArgs
List of arguments passed to the forked VM
forkMaxMemory (Default: 1024)
User property: forkMaxMemory
Whether the JVM is forked for executing Grails commands
forkMinMemory (Default: 512)
User property: forkMinMemory
Whether the JVM is forked for executing Grails commands
forkPermGen (Default: 256)
User property: forkPermGen
Whether the JVM is forked for executing Grails commands
grailsBuildListener
Fully qualified classname of a grails build listener to attach to the
Grails command
grailsEnv
User property: environment
The Grails environment to use.
grailsHome
User property: grailsHome
The path to the Grails installation.
grailsWorkDir (Default: ${project.build.directory}/work)
User property: grails.grailsWorkDir
The Grails work directory to use.
nonInteractive (Default: false)
Required: true
User property: nonInteractive
Whether to run Grails in non-interactive mode or not. The default is to
run interactively, just like the Grails command-line.
pluginsDir (Default: ${basedir}/plugins)
Required: true
User property: pluginsDirectory
The directory where plugins are stored.
showStacktrace (Default: false)
User property: showStacktrace
Turns on/off stacktraces in the console output for Grails commands.
Error importante: si al ejecutar un commando de Grails Maven retorna un error de este estilo:
[INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 15.058s [INFO] Finished at: Mon Jun 09 14:20:22 CEST 2014 [INFO] Final Memory: 26M/62M [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.grails:grails-maven-plugin:2.2.4:create-domain-class (default-cli) on project grailsdemo: Unable to start Grails: java.lang.reflect.InvocationTargetException: java.lang.IllegalStateException: User input is not enabled, cannot obtain input stream -> [Help 1]
Es muy probable que nos falte definir parámetros para el comando de Grails. En Grails, se pedirían por pantalla dichos parámetros. Pero dado que la versión evaluada tiene algunos parámetros con el modo interactivo desactivado, hay que definirlos antes de ejecutar el goal del plugin de Grails.
6.1. Añadir un plugin de Grails al proyecto
En Grails hay muchos plugins disponibles para realizar un montón de tareas. La lista completa de plugins disponibles la podemos generar con:
$ mvn grails:list-plugins
Para añadir el plugin de Grails, simplemente se mete en el pom.xml la dependencia del plugin, por ejemplo:
org.grails.plugins
authentication
2.0.1
zip
runtime
org.grails.plugins
file-uploader
1.2.1
zip
runtime
Al intentar ejecutar la aplciación, se descargará e instalará el plugin de Grails.
7. Introducción al patrón MVC y al CRUD en Grails
Grails está preparado para crear aplicaciones que siguen el patrón MVC de manera sencilla. El Proceso es el siguiente:
- Creamos el modelo. Para ello vamos a definir las entidades mediante clases Groovy que definirán los datos que se van a persistir en la base de datos
- Luego generamos los controladores asociados a dichas entidades. Se generarán automáticamente las acciones necesarias para implementar el patrón CRUD (Consltas, altas, bajas y modificaciones)
- Luego generamos las vistas que podrán mostrar la información de dicha entidad
Generemos una entidad simple: Book:
$ mvn grails:create-domain-class -DdomainClassName=Book ….. |Loading Grails 2.2.4 |Configuring classpath |Running pre-compiled script . |Environment set to development ................ |Created file grails-app/domain/grailsdemo/Book.groovy .. |Compiling 1 source files ...... |Created file test/unit/grailsdemo/BookTests.groovy [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 17.534s [INFO] Finished at: Mon Jun 09 13:41:30 CEST 2014 [INFO] Final Memory: 29M/70M [INFO] ------------------------------------------------------------------------
Y ahora veremos que se ha creado una clase groovy en la carpeta de clases del dominio grails-app/domain/grailsdemo
$ dir grails-app/domain/grailsdemo/
Book.groovy
$ nano grails-app/domain/grailsdemo/Book.groovy
package grailsdemo
class Book {
static constraints = {
}
}
Vamos a generar el controlador (ojo con el nombre de la entidad, ya que las genera dentro del paquete grailsdemo)
$ mvn grails:generate-controller -DdomainClassName=grailsdemo.Book [INFO] Scanning for projects... … [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building A custom grails project 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- grails-maven-plugin:2.2.4:generate-controller (default-cli) @ grailsdemo --- [WARNING] Grails Start with out fork |Loading Grails 2.2.4 |Configuring classpath |Running pre-compiled script . |Environment set to development ................................ |Packaging Grails application ....................... |Generating controller for domain class grailsdemo.Book |Finished generation for domain class grailsdemo.Book [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 16.739s [INFO] Finished at: Tue Jun 10 12:25:30 CEST 2014 [INFO] Final Memory: 38M/91M [INFO] ------------------------------------------------------------------------
Y ahora generamos la vista:
$ mvn grails:generate-views -DdomainClass=grailsdemo.Book [INFO] Scanning for projects... … [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building A custom grails project 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- grails-maven-plugin:2.2.4:generate-views (default-cli) @ grailsdemo --- [WARNING] Grails Start with out fork |Loading Grails 2.2.4 |Configuring classpath |Running pre-compiled script . |Environment set to development ................................ |Packaging Grails application .... |Compiling 1 source files ......... |Packaging Grails application .......... |Generating views for domain class grailsdemo.Book |Finished generation for domain class grailsdemo.Book [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 39.272s [INFO] Finished at: Tue Jun 10 12:28:23 CEST 2014 [INFO] Final Memory: 34M/88M [INFO] ------------------------------------------------------------------------
Los ficheros generados son:
- en grails-app/views/book/
- create.gsp
- edit.gsp
- _form.gsp
- list.gsp
- show.gsp
- En grails-app/controllers/grailsdemo/
- BookController.groovy
- En grails-app/domain/grailsdemo/
- Book.groovy
Las vistas generadas son GSPs, que son parecidos a los JSPs estándares, pero incluyen etiquetas Groovy para generar los elementos de la vista.
El controlador está escrito en Groovy, por lo que hay que entender algunos de los conveniso del lenguaje Grroovy para ver cómo funciona.
Ahora ejecutamos de nuevo la aplicación con
|
Text
|
|
|---|---|
|
$ mvn grails:run-app
|
|
y al abrir el navegador ya aparece el nuevo controlador.

Pinchamos en el controlador:

Y si pinchamos en Crear Book:

Ahora vamos a añadir nuevos campos a la entidad. Modificamos el fichero Book.groovy
class Book {
static constraints = {
}
String titulo;
String autor;
Date fechaPublicaon;
}
Y regeneramos las vistas con:
$ mvn grails:generate-views -DdomainClass=grailsdemo.Book
Al ir a crear un nuevo libro aparecen ya los campos.
7.1. Las plantillas de los formularios
¿Cómo genera Grails los ficheros GSPs cuando usamos grails:generate-views? El misterio está en los ficheros de plantillas. Pero la plantilla es a su vez un GSP, luego el motor de Grails genera un GSP a partir de un GSP de plantilla. El proceso exacto es bastante curioso. Para explicarlo mejor vamos a ver una plantilla y un GSP generado a partir de ella:
<% import grails.persistence.Event %>
<%=packageName%>
<html>
<head>
<meta name="layout" content="main">
<g:set var="entityName" value="\${message(code: '${domainClass.propertyName}.label', default: '${className}')}" />
<title><g:message code="default.list.label" args="[entityName]" /></title>
</head>
<body>
<a href="#list-${domainClass.propertyName}" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="\${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
</ul>
</div>
<div id="list-${domainClass.propertyName}" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="\${flash.message}">
<div class="message" role="status">\${flash.message}</div>
</g:if>
<table>
<thead>
<tr>
<% excludedProps = Event.allEvents.toList() << 'id' << 'version'
allowedNames = domainClass.persistentProperties*.name << 'dateCreated' << 'lastUpdated'
props = domainClass.properties.findAll { allowedNames.contains(it.name) && !excludedProps.contains(it.name) && it.type != null && !Collection.isAssignableFrom(it.type) }
Collections.sort(props, comparator.constructors[0].newInstance([domainClass] as Object[]))
props.eachWithIndex { p, i ->
if (i < 6) {
if (p.isAssociation()) { %>
<th><g:message code="${domainClass.propertyName}.${p.name}.label" default="${p.naturalName}" /></th>
<% } else { %>
<g:sortableColumn property="${p.name}" title="\${message(code: '${domainClass.propertyName}.${p.name}.label', default: '${p.naturalName}')}" />
<% } } } %>
</tr>
</thead>
<tbody>
<g:each in="\${${propertyName}List}" status="i" var="${propertyName}">
<tr class="\${(i % 2) == 0 ? 'even' : 'odd'}">
<% props.eachWithIndex { p, i ->
if (i == 0) { %>
<td><g:link action="show" id="\${${propertyName}.id}">\${fieldValue(bean: ${propertyName}, field: "${p.name}")}</g:link></td>
<% } else if (i < 6) {
if (p.type == Boolean || p.type == boolean) { %>
<td><g:formatBoolean boolean="\${${propertyName}.${p.name}}" /></td>
<% } else if (p.type == Date || p.type == java.sql.Date || p.type == java.sql.Time || p.type == Calendar) { %>
<td><g:formatDate date="\${${propertyName}.${p.name}}" /></td>
<% } else { %>
<td>\${fieldValue(bean: ${propertyName}, field: "${p.name}")}</td>
<% } } } %>
</tr>
</g:each>
</tbody>
</table>
<div class="pagination">
<g:paginate total="\${${propertyName}Total}" />
</div>
</div>
</body>
</html>
Ahora vemos el fichero generado:
<html>
<head>
<meta name="layout" content="main">
<g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
<title><g:message code="default.list.label" args="[entityName]" /></title>
</head>
<body>
<a href="#list-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
</ul>
</div>
<div id="list-book" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<table>
<thead>
<tr>
<g:sortableColumn property="autor" title="${message(code: 'book.autor.label', default: 'Autor')}" />
<g:sortableColumn property="fechaPublicaon" title="${message(code: 'book.fechaPublicaon.label', default: 'Fecha Publicaon')}" />
<g:sortableColumn property="titulo" title="${message(code: 'book.titulo.label', default: 'Titulo')}" />
</tr>
</thead>
<tbody>
<g:each in="${bookInstanceList}" status="i" var="bookInstance">
<tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
<td><g:link action="show" id="${bookInstance.id}">${fieldValue(bean: bookInstance, field: "autor")}</g:link></td>
<td><g:formatDate date="${bookInstance.fechaPublicaon}" /></td>
<td>${fieldValue(bean: bookInstance, field: "titulo")}</td>
</tr>
</g:each>
</tbody>
</table>
<div class="pagination">
<g:paginate total="${bookInstanceTotal}" />
</div>
</div>
</body>
</html>
Observar que entre otras cosas:
- Los \$ se cambian por $ en el fichero GSP generado
- Los ${variable} se sustituyen por su valor en el GSP generado
- Se usan sentencias de control g:if y g:each para generar elementos en el GSP generado.
Este último punto es muy importante, ya que si queremos personalizar un GSP generado hay que modificar las plantillas y añadir las exclusiones o código a medida que necesitemos… en la propia plantilla.
Una pregunta curiosa. Por defecto ¿cuántas columnas tiene como máximo un listado estándar en Grails?
Por cierto, ¿cuál es el orden de las columnas en los listados?
7.2. Un ejemplo sencillo de plantilla modificada list.gsp
Os voy a mostrar una plantilla list.gsp que incluye elementos a medida:
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main">
<g:set var="entityName" value="\${message(code: '${domainClass.propertyName}.label', default: '${className}')}" />
<title><g:message code="default.list.label" args="[entityName]" /></title>
<resource:dateChooser />
<resource:autoComplete skin="default" />
<export:resource />
</head>
<body>
<a href="#list-${domainClass.propertyName}" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
<g:if test="\${'${propertyName}' == 'autorInstance'}">
<g:form action="searchBy" controller="autor">
<div id="autocom">
<g:message code="default.search.label" args="['cliente', 'comercial']" />
<richui:autoComplete name="dato" action="completeFilter" class="required" />
<g:actionSubmitImage value="Buscar" action="searchBy" src="/wp-content/uploads/tutorial-data/\${resource(dir: 'images/skin', file: 'lupa.png')}" align="center" style="border:0px;background:transparent"/>
</div>
</g:form>
</g:if>
<g:else>
<g:form action="filterLike" controller="${domainClass.propertyName}">
<div id="autocom">
<g:message code="default.filter.label" args="['${domainClass.propertyName}']" />
<richui:autoComplete name="dato" action="completeFilter" class="required" />
<g:actionSubmitImage value="Buscar" action="filterLike" src="/wp-content/uploads/tutorial-data/\${resource(dir: 'images/skin', file: 'lupa.png')}" align="center" style="border:0px;background:transparent"/>
</div>
</g:form>
</td>
8. El plugin de Fields
En Grails, al mecanismo utilizado para generar el patrón CRUD se le llama scaffolding (andamiaje). Este mecanismo utiliza un sistema de plantillas para gerarlas vistas.
Estas plantillas analizan la clase entidad del dominio y generan la vista durante la etapa «grails:generate-views».
Pero el sistema de plantillas utilizado por defecto es muy básico. Es recomendable utilizar el plugin Fieds de Grails para tener más flexibilidad a la hora de generar las vistas.
8.1. Instalación del plugin de Fields
Se mete la dependencia en el pom.xml y se vuelve a compilar:
org.grails.plugins
fields
1.3
zip
runtime
Luego hacemos $ mvn grails:run-app para que se instale el plugin de fields.
Si volvemos a intentar regenerar la vista veremos cómo se descarga e instala el plugin automáticamente.
Para instalar las plantillas del plugin hay que hacer:
$ mvn grails:exec -Dcommand="install-form-fields-templates" … |Loading Grails 2.2.4 |Configuring classpath . |Environment set to development .... |Copying templates from /home/usuario/workspaces/workspaceGrailsdemo/grailsdemo/plugins/fields-1.3 ....... |Template installation complete [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 26.476s [INFO] Finished at: Tue Jun 10 13:35:58 CEST 2014 [INFO] Final Memory: 25M/61M [INFO] ------------------------------------------------------------------------
Las plantillas se generan en /src/templates/scaffolding
El plugin de fields nos permite modificar el comportamiento de cada campo, pudiéndole asignar a un campo un trozo de código simplemente ubicando el código de plantilla en el lugar adecuado. Por ejemplo podemos modificar el campo fechaPublicacion de la entidad Book. Para ello creamos el fichero de plantilla de dicho campo en el lugar adecuado:
$ mkdir grails-app/views/book/fechaPublicacion
$ nano grails-app/views/book/fechaPublicacion/_field.gsp
<div class="fieldcontain ${hasErrors(bean: bookInstance, field: 'fechaPublicacion', 'error')} required">
<label for="fechaInicio">
<g:message code="book.fechaPublicacion.label" />
<span class="required-indicator">*</span>
</label>
<richui:dateChooser name="fechaInicio" format="dd/MM/yyyy" firstDayOfWeek="Mo" value="${bookInstance?.fechaPublicacion}" />
</div>
Os dejo como tarea:
- Instalar el plugin richui
- Añadir la etiqueda
Text
al principio del fichero src/templates/scaffolding/create.gsp
- Regenerar la vista
- Ver que se genera un nuevo campo fecha de publicación

Ayuda: Esta es la dependencia que yo he usado:
org.grails.plugins
richui
0.8
zip
runtime
9. Los controladores, la persistencia y el lenguaje Groovy
Para el patrón MVC necesitamos un modelo, que es nuestra entidad Book, un conjunto de vistas, que son los GSP generados por Grails y nos queda nada más analizar el contolador, que es una clase Groovy.
Ya que hemos generado el controlador, listemos su contenido:
$ cat grails-app/controllers/grailsdemo/BookController.groovy
package grailsdemo
import org.springframework.dao.DataIntegrityViolationException
class BookController {
static allowedMethods = [save: "POST", update: "POST", delete: "POST"]
def index() {
redirect(action: "list", params: params)
}
def list(Integer max) {
params.max = Math.min(max ?: 10, 100)
[bookInstanceList: Book.list(params), bookInstanceTotal: Book.count()]
}
def create() {
[bookInstance: new Book(params)]
}
def save() {
def bookInstance = new Book(params)
if (!bookInstance.save(flush: true)) {
render(view: "create", model: [bookInstance: bookInstance])
return
}
flash.message = message(code: 'default.created.message', args: [message(code: 'book.label', default: 'Book'), bookInstance.id])
redirect(action: "show", id: bookInstance.id)
}
def show(Long id) {
def bookInstance = Book.get(id)
if (!bookInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), id])
redirect(action: "list")
return
}
[bookInstance: bookInstance]
}
def edit(Long id) {
def bookInstance = Book.get(id)
if (!bookInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), id])
redirect(action: "list")
return
}
[bookInstance: bookInstance]
}
def update(Long id, Long version) {
def bookInstance = Book.get(id)
if (!bookInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), id])
redirect(action: "list")
return
}
if (version != null) {
if (bookInstance.version > version) {
bookInstance.errors.rejectValue("version", "default.optimistic.locking.failure",
[message(code: 'book.label', default: 'Book')] as Object[],
"Another user has updated this Book while you were editing")
render(view: "edit", model: [bookInstance: bookInstance])
return
}
}
bookInstance.properties = params
if (!bookInstance.save(flush: true)) {
render(view: "edit", model: [bookInstance: bookInstance])
return
}
flash.message = message(code: 'default.updated.message', args: [message(code: 'book.label', default: 'Book'), bookInstance.id])
redirect(action: "show", id: bookInstance.id)
}
def delete(Long id) {
def bookInstance = Book.get(id)
if (!bookInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), id])
redirect(action: "list")
return
}
try {
bookInstance.delete(flush: true)
flash.message = message(code: 'default.deleted.message', args: [message(code: 'book.label', default: 'Book'), id])
redirect(action: "list")
}
catch (DataIntegrityViolationException e) {
flash.message = message(code: 'default.not.deleted.message', args: [message(code: 'book.label', default: 'Book'), id])
redirect(action: "show", id: id)
}
}
}
¿Cómo funciona el controlador? Es como en otros frameworks MVC. La ruta solicitada por el navegador comienza por el nombre del controlador y la acción solicitada, y los parámetros añadidos a la ruta estarán disponibles dentro del controlador. Dentro del controlador se debe invocar a un método. Así http://localhost:8080/grailsdemo/book/ invoca a la acción por defecto, que es index() y ésta a su vez redirige al list(), y http://localhost:8080/grailsdemo/book/create invoca a la acción créate del controlador BookController.groovy
Si observamos las acciones encontraremos detalles importantes al estar usando Groovy, que hay que tener en cuenta si venimos de Java:
- El estilo de escritura es muy simple, no hay muchos «;»»
- Groovy tiene más operadores que Java, como ?. y otros.
- Las variables en Groovy pueden ser débil o fuertemente tipadas.
- Al estilo de javascript, las variables no tipadas se definen con «def»
- A una variable se le puede asignar un valor o un trozo de código. Esto último es lo que se denominan «closures» en Groovy.
- Además es muy normal definir la «closure » e invocarla a la vez, como por ejemplo en estre fragmento de código:
def results = book.list (max: params.max, offset: params.offset) {
eq("user",paramTarea2)
if (params.order) {
order(params.sort, params.order)
} else {
order("description", "asc")
}
}
- Todos los métodos devuelven siempre un valor, que el valor de la última sentencia o expresión ejecutada
- Cuando se ejecuta un método del controlador, la variable params contiene los parámetros de la petición.
- Una lista se crea simplemente encerrando un conjunto de valores entre corchetes cuadrados: [ «cadena1», «cadena2», «cadena3»]
- Un mapa se crea simplemente encerrando un conjunto de pares clave:valor entre corchetes cuadrados: [nombre: «Diego», apellido:»Rojo» ]
- Si el método del controlador devuelve un mapa, éste se añadirá al conjunto de variables disponibles por la vista, como en el método edit() del controlador de Book
- Ojo con las vistas, ya que los GSPs se parecen a los JSPs estándares de java, pero son más potentes, ya que las expresiones ${ } admiten cualquier expresión Groovy, incluso llamadas a métodos.
- Todo el acceso a la base de datos lo haremos a través de GORM, que es el mapeador de Grails, basado en Hibernate. Además GORM utiliza un avanzado sistema de
- Nota: todo el sistema de persistencia por defecto trabaja en memoria, pero se puede guardar en una base de datos simplemente cambiando el DataSource.groovy
«proxis dinámicos» que simplifica la ejecución de consultas sencillas, como los típicos findByAutor()
10. Conclusión
En este tutorial hemos realizado una presentación de Grails, usando como herramienta de desarrollo simplemente Maven. Hemos visto entre otros:
- Como se crea una aplicación en Grails
- Como se crea un patrón CRUD en Grails
- Una ligera introducción al patrón MVC y su implementación en Grails
- Algunas técnicas de Grails y Groovy
Y ahora llega la pregunta del millón ¿cuándo debo usar Grails en vez de Java con JSF y los frameworks estándares?
- Para un desarrollo corto, con un modelo de datos sencillo y un flujo de pantallas basado en CRUDs, Grails nos da la oportunidad de desarrollar la aplicación en poco tiempo, si lo dominamos.
- Realmente desarrollar en Groovy también tiene su propia curva de aprendizaje. Se pueden hacer cosas bastante interesantes en poco tiempo, pero adaptar el estilo visual de la aplicación a lo que a nosotros nos gusta lleva su tiempo.
- Si el desarrollo es largo, los frameworks de Java suelen facilitar los desarrollos (por ejemplo usando Primefaces o similares) y es algo a lo que la mayor parte de desarrolladores Java están ya acostumbrados.
3 respuestas
Fanstástico artículo Cristóbal.
Tan solo un apunte, indicas en varias partes de él que Grails es un servidor de aplicaciones para el lenguaje Groovy.
Bueno tenia entendido que Grails es un framework de desarrollo para Groovy, al igual que Ror lo es para Ruby.
He visto que en los ejemplos que usas, empleas Tomcat y Jetty para el despliegue, pero no un servidor de aplicaciones que se llame Grails, por tanto sospecho que se trata de una errata, denominar a Grails como servidor de aplicaciones.
Hablo desde la más honesta humildad y desconocimiento.
Es una mala copia de http://targetveb.com/introduction-groovy-grails-maven-crud-pattern.html
Ese artículo al que haces referencia es de 2018. El publicado en Adictosaltrabajo es de 2014.