domingo, 23 de octubre de 2011

Servicios JSON con Grails y BBDD existente. Parte I.


Como bien saben, Jarhalla es un sideproject que tengo desde hace unos meses.
El slogan (patrocinio de  dargorshadow) es: Here, all {brave} jar's REST, y la intención es que el REST no sea sólo una parte alegórica del slogan.  ;)

La idea a media plazo es proporcionar un API  (no necesariamente full-rest debo aclarar) para poder ofrecer la información ya indexada.

¿Que ventajas tiene ofrecer servicios y devolver JSON? pues, la principal creo es que pueden llegar a  utilizarla como mejor se les facilite.
Además por supuesto, y como comenta ecamacho,  quiero utilizarlo para probar ciertos frameworks.

Por ello inicio esta serie de post's, en ellos iré describiendo los pasos para crear la versión inicia de esta API.
He decidio utilizar grails por muchas razones, pero, la principal, es la facilidad con la que se crean servicios  JSON.

Escenario inicial.
En este primer post, veremos como utilizar grails para operar sobre una base de datos ya existente (legacy para los letrados) y, como generar  de manera sencilla un servicio que devuelva información en formato JSON.

Básicamente haremos lo siguiente:

  • Conocer los detalles de la base de datos/tabla a utilizar
  • Crear el proyecto.
  • Modificar el archivo BuildConfig.groovy
  • Modificar el archivo DataSource.groovy
  • Crear nuestra: Clase de dominio
  • Hacer magia con grails: generate-all
  • Crear un Controller propio
  • Listo.

¡¡¡ Manos a la obra !!!

Conocer los detalles de la base de datos/tabla a utilizar.

Jarhalla actualmente utiliza MySql para almacenar la información.
Iniciaremos con una tabla sencilla Repos:
mysql> desc REPOS;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| ID_REPO | int(11) | NO | PRI | NULL | auto_increment | 
| NAME_SHORT | varchar(45) | YES | | NULL | | 
| URL_HOME | varchar(255) | YES | | NULL | | 
| URL_ICON | varchar(255) | YES | | NULL | | 
| LAST_MODIF | varchar(8) | YES | | NULL | | 
+------------+--------------+------+-----+---------+----------------+
El campo id es ID_REPO, y es autoincremental.

------------------------------------  3 registros con los que pueden iniciar su tabla ----------------
insert into repos (NAME_SHORT, URL_HOME, URL_ICON, LAST_MODIF) values ('Maven Repo 1','http://repo1.maven.org/maven2/','http://maven.apache.org/images/apache-maven-project-2.png','01/12/10');
insert into repos (NAME_SHORT, URL_HOME, URL_ICON, LAST_MODIF) values ('Spring Source','http://www.springsource.com/repository/app/','http://www.springsource.org/sites/all/themes/dotorg09/images/dotorg09_logo.png','01/12/10');
insert into repos (NAME_SHORT, URL_HOME, URL_ICON, LAST_MODIF) values ('CodeHaus','http://repository.codehaus.org/','http://media.codehaus.org/images/unity-codehaus-logo-only.png','01/12/10');    
----------------------------------------------------------------------------------------------------

Crear el proyecto.
Ya conocemos la tabla que vamos a manipular, entonces, lo que sigue según nuestro plan es crear la aplicación,

Si aún no tienes instalado grails. aquí puedes conocer a detalle los pasos.

En nuestro idioma si requieres documentación para iniciar con grails y groovy:
Para crear la aplicación, únicamente ejecutamos:

rugi$ grails create-app api-jarhalla

Al finalizar de la ejecución de  los scripts de grails obtenemos algo como lo siguiente en los mensajes de la consola:
------------------------------------------

grails tomcat

Created Grails Application at
/path/al/directorio/donde/ejecutaste/laCreacion/api-jarhalla
Ya tenemos lista la aplicación.
La estructura que crea es algo ya conocido, pero, recordemosla:
 |-api-jarhalla
 |---grails-app
 |-----conf
 |-------hibernate
 |-------spring
 |-----controllers
 |-----domain
 |-----i18n
 |-----services
 |-----taglib
 |-----utils
 |-----views
 |-------layouts
 |---lib
 |---scripts
 |---src
 |-----groovy
 |-----java
 |---test
 |-----integration
 |-----unit
 |---web-app
 |-----META-INF
 |-----WEB-INF
 |-------tld
 |-----css
 |-----images
 |-------skin
 |-----js
 |-------prototype
Todas las instrucciones que ejecutemos serán dentro de la carpeta creada.
/path/al/directorio/donde/ejecutaste/laCreacion/api-jarhalla

Modificar el archivo BuildConfig.groovy
Antes de ejecutar nuestro proyecto por primera vez vamos a agregar las dependencias extras que requerimos, en este caso, necesitamos el driver para MySQL.

Si abrimos el archivo BuildConfig.groovy, ubicado en: /grails-app/conf/ veremos algo así:
--------------------------------------------------------------------------------------------
grails.project.class.dir = "target/classes"
grails.project.test.class.dir = "target/test-classes"
grails.project.test.reports.dir = "target/test-reports"
//grails.project.war.file = "target/${appName}-${appVersion}.war"
grails.project.dependency.resolution = {
 // inherit Grails' default dependencies
 inherits("global") {
 // uncomment to disable ehcache
 // excludes 'ehcache'
 }
 log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
 repositories {
 grailsPlugins()
 grailsHome()
 grailsCentral()
 // uncomment the below to enable remote dependency resolution
 // from public Maven repositories
 //mavenLocal()
 //mavenCentral()
 //mavenRepo "http://snapshots.repository.codehaus.org"
 //mavenRepo "http://repository.codehaus.org"
 //mavenRepo "http://download.java.net/maven/2/"
 //mavenRepo "http://repository.jboss.com/maven2/"
 }
 dependencies {
 // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.
 // runtime 'mysql:mysql-connector-java:5.1.13'
 }
}

--------------------------------------------------------------------------------------------
Básicamente aquí se definen las dependencias del proyecto y los respectivos repositorios dónde debe encontrarlas.
Vamos a descomentar 3 líneas, dos que activan la búsqueda en repositorios maven , y una última que activa la dependendia que, precisamente, necesitamos:  el conector de mysql!.

Así, nuestro BuildConfig.groovy queda:
--------------------------------------------------------------------------------------------------------------------------------------

grails.project.class.dir = "target/classes"
grails.project.test.class.dir = "target/test-classes"
grails.project.test.reports.dir = "target/test-reports"
//grails.project.war.file = "target/${appName}-${appVersion}.war"
grails.project.dependency.resolution = {
 // inherit Grails' default dependencies
 inherits("global") {
 // uncomment to disable ehcache
 // excludes 'ehcache'
 }
 log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
 repositories {
 grailsPlugins()
 grailsHome()
 grailsCentral()

 // uncomment the below to enable remote dependency resolution
 // from public Maven repositories
 mavenLocal()
 mavenCentral()
 //mavenRepo "http://snapshots.repository.codehaus.org"
 //mavenRepo "http://repository.codehaus.org"
 //mavenRepo "http://download.java.net/maven/2/"
 //mavenRepo "http://repository.jboss.com/maven2/"
 }
 dependencies {
 // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.

 runtime 'mysql:mysql-connector-java:5.1.13'
 }
}
--------------------------------------------------------------------------------------------------------------------------------------
Modificar el archivo DataSource.groovy
El siguiente paso es, modificar el archivo DataSource.groovy ubicado también en: /grails-app/conf/ 
Aquí indicaremos los datos requeridos para acceder a la base de datos que contiene la tabla que queremos utilizar.

Si abrimos el archivo veremos algo así:
--------------------------------------------------------------------------------------------------------------------------------------
dataSource {
 pooled = true
 driverClassName = "org.hsqldb.jdbcDriver"
 username = "sa"
 password = ""
}
hibernate {
 cache.use_second_level_cache = true
 cache.use_query_cache = true
 cache.provider_class = 'net.sf.ehcache.hibernate.EhCacheProvider'
}
// environment specific settings
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"
 }
 }
 production {
 dataSource {
 dbCreate = "update"
 url = "jdbc:hsqldb:file:prodDb;shutdown=true"
 }
 }
}
--------------------------------------------------------------------------------------------------------------------------------------
¿Qué modificamos?
Pues, únicamente requerimos indicar los datos de nuestra origen de datos (data source), el driver que debemos invocar, y, de momento, la configuración para el ambiente de desarrollo (development enviroment), aquí, para fines prácticos (no queremos hacer CREATE y mucho menos DELETE ¿cierto?), elegimos dbCreate = 'update'

Nuestro archivo DataSource.groovy queda así:
--------------------------------------------------------------------------------------------------------------------------------------
dataSource {
 pooled = true
 driverClassName = "com.mysql.jdbc.Driver"
 username = "miUsuario"
 password = "miContrasenya"
}
hibernate {
 cache.use_second_level_cache = true
 cache.use_query_cache = true
 cache.provider_class = 'net.sf.ehcache.hibernate.EhCacheProvider'
}
// environment specific settings
environments {
 development {
 dataSource {
 dbCreate = "update" // one of 'create', 'create-drop','update'
 url = "jdbc:mysql://localhost/JARHALLA"
 }
 }
 test {
 dataSource {
 dbCreate = "update"
 url = "jdbc:hsqldb:mem:testDb"
 }
 }
 production {
 dataSource {
 dbCreate = "update"
 url = "jdbc:hsqldb:file:prodDb;shutdown=true"
 }
 }
}
--------------------------------------------------------------------------------------------------------------------------------------
Ahora, ya que hemos configurado todo lo requerido, pasamos a:

Crear nuestra: Clase de dominio

Sólo necesitamos ejecutar:
grails create-domain-class org.jarhalla.Repository
Al hacer esto  agregamos un par de carpetas al proyecto:
api-jarhalla/grails-app/domain/org/jarhalla
api-jarhalla/test/unit/org/jarhalla

El archivo que nos interesa, como bien estas imaginando se encuentra en:
api-jarhalla/grails-app/domain/org/jarhalla/Repository.groovy

Si lo abrimos veremos esto:
--------------------------------------------------------------------------------------------------------------------------------------
package org.jarhalla

class Repository {
    static constraints = {

    }

}
--------------------------------------------------------------------------------------------------------------------------------------
Ahora, únicamente nos hace falta hacer el mapeo de nuestra tabla, adicionalmente, grails requiere (para poder hacer parte de su mágia) que indiquemos cual es el campo id

Veamos cómo queda nuestro objeto de dominio:
--------------------------------------------------------------------------------------------------------------------------------------
package org.jarhalla

class Repository {

 String nameShort;
 String urlHome
 String urlIcon;
 String lastModif
 static constraints = {
  nameShort(maxSize:45)
 urlHome(maxSize:255) 
 urlIcon(maxSize:255)
 lastModif(maxSize:8) 
 }

 static mapping = {
  table 'REPOS'
 version false 
 columns {
 id column:'ID_REPO'
  nameShort column:'NAME_SHORT'
 urlHome column:'URL_HOME'
 urlIcon column:'URL_ICON'
 lastModif column:'LAST_MODIF'
 }
 }
}
--------------------------------------------------------------------------------------------------------------------------------------
Comentemos un poco el código, en el bloque: static constraints , aqui se definen las validaciones de nuestro objeto de dominio.
Algunas de estas validaciones son:
  • blank
  • creditCard
  • display
  • email
  • inList
  • matches
  • max
  • maxSize
  • min
  • minSize
El siguiente bloque es él que nos interesa, en static mapping se define el mapeo con la tabla que será representada por nuestro objeto de dominio.

Definimos la tabla con: table 'REPOS',  la siguiente línea: versión false sirve para indicarle a grails que: no tenemos un campo version en nuestra tabla.
Grails utiliza este campo para realizar parte de su mágia (ver Pessimistic and optimistic locking).

Dentro del mapeo la primera línea en especial, sirve para indicarle a grails cual será nuestro campo id:

id column:'ID_REPO'

El resto de los campos no tiene mayor problema.

Ahora (parafrasando a un maestro de la comedia en MX), ha llegado la hora "cuchicuchesca",  el momento "chimenguenchón", básicamente... el por qué de este post!!

Hacer magia con grails: generate-all

Aplicamos esta instrucción sobre nuestra clase de dominio.
rugi$ grails generate-all org.jarhalla.Repository

Esta operación, ha creado por nosotros un controlador (RepositoryController) en la carpeta:
grails-app/controllers/org/jarhalla/
Ademas, ha creado los archivos gsp necesarios para operar sobre nuestro objeto de dominio.
grails-app/views/repository/
Los archivos creados son: create.gsp, edit.gsp, list.gsp, show.gsp

Ahora, probemos.. es la hora.

rugi$ grails run-app

Si todo sale bien,  veremos el siguiente mensaje indicandonos que la aplicación está ya en ejecución. 

Server running. Browse to http://localhost:8080/api-jarhalla

Y si abrimos esa URL, debemos ver algo como lo siguiente:
app en ejecucion. index

Veamos el listado
http://localhost:8080/api-jarhalla/repository/list
List

¿Recuerdan que dentro de la configuración indicamos cual es el campo id?
Pues, ese parámetro sirve justamente para que podamos hacer algo como lo siguiente:
http://localhost:8080/api-jarhalla/repository/show/1

show

En este punto, por lo general, las tablas legacy, únicamente se utilizan para lectura de datos, así que,
después de ver la mágia funcionar (si es que es tu caso), elimina los gsp's: create.gsp y edit.gsp

Parte de la mágia la realiza el controlador RepositoryController.groovy

Si abrimos el archivo veremos algo así:
--------------------------------------------------------------------------------------------------------------------------------------
package org.jarhalla

class RepositoryController {

 static allowedMethods = [save: "POST", update: "POST", delete: "POST"]

 def index = {
 redirect(action: "list", params: params)
 }

 def list = {
 params.max = Math.min(params.max ? params.int('max') : 10, 100)
 [repositoryInstanceList: Repository.list(params), repositoryInstanceTotal: Repository.count()]
 }

  def create = {
 def repositoryInstance = new Repository()
 repositoryInstance.properties = params
 return [repositoryInstance: repositoryInstance]
 }

 def save = {
 def repositoryInstance = new Repository(params)
 if (repositoryInstance.save(flush: true)) {
 flash.message = "${message(code: 'default.created.message', args: [message(code: 'repository.label', default: 'Repository'), repositoryInstance.id])}"
 redirect(action: "show", id: repositoryInstance.id)
 }
 else {
 render(view: "create", model: [repositoryInstance: repositoryInstance])
 }
 }

 def show = {
 def repositoryInstance = Repository.get(params.id)
 if (!repositoryInstance) {
 flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'repository.label', default: 'Repository'), params.id])}"
 redirect(action: "list")
 }
 else {
 [repositoryInstance: repositoryInstance]
 }
 }

 def edit = {
 def repositoryInstance = Repository.get(params.id)
 if (!repositoryInstance) {
 flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'repository.label', default: 'Repository'), params.id])}"
 redirect(action: "list")
 }
 else {
 return [repositoryInstance: repositoryInstance]
 }
 }

  def update = {
 def repositoryInstance = Repository.get(params.id)
 if (repositoryInstance) {
 if (params.version) {
 def version = params.version.toLong()
 if (repositoryInstance.version > version) {
 
 repositoryInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [message(code: 'repository.label', default: 'Repository')] as Object[], "Another user has updated this Repository while you were editing")
 render(view: "edit", model: [repositoryInstance: repositoryInstance])
 return
 }
 }
 repositoryInstance.properties = params
 if (!repositoryInstance.hasErrors() && repositoryInstance.save(flush: true)) {
 flash.message = "${message(code: 'default.updated.message', args: [message(code: 'repository.label', default: 'Repository'), repositoryInstance.id])}"
 redirect(action: "show", id: repositoryInstance.id)
 }
 else {
 render(view: "edit", model: [repositoryInstance: repositoryInstance])
 }
 }
 else {
 flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'repository.label', default: 'Repository'), params.id])}"
 redirect(action: "list")
 }
 }

 def delete = {
 def repositoryInstance = Repository.get(params.id)
 if (repositoryInstance) {
 try {
 repositoryInstance.delete(flush: true)
 flash.message = "${message(code: 'default.deleted.message', args: [message(code: 'repository.label', default: 'Repository'), params.id])}"
 redirect(action: "list")
 }
 catch (org.springframework.dao.DataIntegrityViolationException e) {
 flash.message = "${message(code: 'default.not.deleted.message', args: [message(code: 'repository.label', default: 'Repository'), params.id])}"
 redirect(action: "show", id: params.id)
 }
 }
 else {
 flash.message = "${message(code: 'default.not.found.message', args: [message(code: 'repository.label', default: 'Repository'), params.id])}"
 redirect(action: "list")
 }
 }
}
--------------------------------------------------------------------------------------------------------------------------------------
Nuevamente, si es tu caso y requieres únicamente exponer el contenido de tu tabla, elimina las operaciones : create, edit, update, delete.

Analizar a detalle cada operación, va más allá de las intenciones de este primer post.
Como cualquier otra tecnología/framework, es mejor ir comenzando a familiarizarnos con ella realizando operaciones sencillas y de ahi, ir subiendo el nivel de complejidad.

Así pues,  nuestro penúltimo paso es:
Crear un Controller propio

Vamos a crear un controlador que concentre los servicios que queremos exponer, para este ejercicio de momento serán:
  • Obtener la información de los repositorios.
  • Obtener un listado de jars segun ciertos criterios de busqueda.
  • Obtener un listado de clases segun ciertos criterios de busqueda.
  • Obtener el detalle de un jar
En este primer post, iniciamos con el primer servicio.
Para crear el controlador, sólo necesitamos hacer lo siguiente:
rugi$ grails create-controller org.jarhalla.RepoV1
Esto nos crea un archivo llamado: RepoV1Controller.groovy

Y, se encuentra en la carpeta:
    api-jarhalla/grails-app/controllers/org/jarhalla

Si lo abrimos veremos algo como lo siguiente:
--------------------------------------------------------------------------------------------------------------------------------------
package org.jarhalla

class RepoV1Controller {

 def index = { }
}
--------------------------------------------------------------------------------------------------------------------------------------
Para lograr nuestro objetivo, devolver JSON, debemos indicarle a grails que requerimos de los convertes.
Para ello, únicamente agregamos esta línea:
import grails.converters.*

El siguiente paso es definir nuestra operacion, le llamaremos repos

Recordemos que nuestra clase de dominio se llama: Repository, recordemos tambien que grails genera cierta cantidad de operaciones para realizar querys (Querying with GORM)

Veremos dos en este ejemplo:  list() y findAll('something_query')
def repos={
 def l = Repository.list()
 render l as JSON
}

Nuestro controller  queda:
--------------------------------------------------------------------------------------------------------------------------------------
package org.jarhalla

class RepoV1Controller {

 def index = {
 render (contentType:"text/json"){
 success = "ok" 
 } 
 }

 def repos={
 def l = Repository.list()
 render l as JSON
 }
}
--------------------------------------------------------------------------------------------------------------------------------------
(He agregado una sencilla respuesta para la operación index)
Ahora, asegurate de que la aplicación está en ejecución y puedes probarlo sin problema:

http://localhost:8080/api-jarhalla/repoV1/repos
Esto ya debe devolver información en formato json:
json

Ahora,  Repository.list() devuelve tal cual el contenido del nuestra tabla, pero,  muy probablemente existirán registros en nuestra tabla legacy que no debemos devolver, ya sea por que son obsoletos o por alguna otra razón.

En este tipo de escenarios, es mejor utilizar el método findAll.

Aqui un ejemplo:

   def list = {
     def l = Repository.findAll("from Repository as r where r.id in (1,2)")
     render l as JSON
   }
Observen que, la sintaxis del query es sobre nuestra clase de dominio, no sobre los nombres de los campos de la tabla.

Aquí mas información para  conocer más sobre findAll.

Y, voilá!!

Con esto terminamos nuestro ejemplo de utilización de tablas legacy para ofrecer un servicio JSON utilizando grails.

Si haz llegado hasta aquí (que paciente eres!!) seguramente tienes algunas preguntas en la mente:
  • ¿Que pasa si nuestra llave es compuesta?
  • ¿Que significado tienen los parámetros que no configuramos?
  • ¿Que ocurrío cuando ejecutamos generate-all?
  • ¿Es correcto dejar todo en un Controller?
  • ¿Quién ganará el próximo mundial?
Si no puedes esperar a los siguientes post's, te comparto los siguientes enlaces:

Mastering Grails: Grails and legacy databases
Manual de desarrollo web con grails.
Presentan al sucesor del Pulpo Paul

Espero pronto continuar con el siguiente post.

Saludos!!!

---
RuGI
Isaac Ruiz Guerra

No hay comentarios:

Publicar un comentario