viernes, 30 de agosto de 2013

Ejemplo lista de tareas con Marionette

Marionette
Como comentaba en el ejemplo de una aplicación de una lista de tareas en una aplicación javascript Backbone es una herramienta que nos puede ayudar mucho a evitar que el código se nos convierta en difícil de manejar facilitándonos como estructurarlo con los modelos, colecciones y vistas y permitiéndonos separar el modelo de la vista actualizando estas últimas a través de los eventos que produce el modelo y escuchados en las vistas.

Backbone es una herramienta que nos da bastante libertad en cuanto a como queremos hacer las cosas y de lo que ofrece podemos usar solo lo que queramos. En algunos casos podemos considerar que Backbone ya es de por si una solución suficiente pero pero en otros podemos necesitar algo que nos facilite la tarea un poco más, de hecho hay muchas herramientas de terceros que proporciona multitud de funcionalidades adicionales sobre Backbone.

Usando Backbone por si solo a medida que vayamos haciendo varias aplicaciones nos daremos cuenta que debemos escribir código que empezaremos a considerar repetitivo, tampoco nos ayudará en desasociar los modelos de las vistas cuando eliminemos estas de la aplicación y si no lo hacemos correctamente tendremos fugas de memoria o vistas zombies que siguen escuchando eventos de los modelos cuando esperamos que se hubiesen destruido, también debemos encargarnos de gestionar donde deben visualizarse las vistas. Marionette es una de esas herramientas que nos puede ayudar a facilitarnos las tareas anteriores que en Backbone debemos hacer manualmente, además de ofrecernos algunas otras funcionalidades adicionales, pero también sin obligarnos a usar lo que no queramos siguiendo la misma filosofía de Backbone.

En esta entrada implementaré el mismo ejemplo de la lista de tareas que hice con Backbone pero esta vez implementado con Marionette aunque revisándolo incluiré un par de funcionalidades adicionales como externalizar del html las plantillas de las vistas y como internacionalizar los textos que aparecen en ellas si necesitamos soportar múltiples idiomas. Para ello utilizaré una de las versiones «bundled» que ofrece Marionette y un par de dependencias que necesita (wreq y babysiter).

El modelo y la colección de tareas es muy parecido aunque se han simplificado un poco al moverse el moverse el código que convertía el modelo a json a la vista, donde Marionette ofrece y llama al método serializeData cuando necesita y posteriormente el método render con los datos obtenidos, en el método render en definitiva se usa Mustache para producir el html de la vista. En realidad, el convertir el modelo al json que necesita a la vista está mejor colocado en la vista, ya que la vista es la que conoce los datos que necesita mostrar del modelo. Otra cosa muy útil que nos ofrece Marionette es la sección con los elementos ui de las vistas, esto evitará que tengamos los sectores de jquery repartidos por varios sitios de la vista y hará el código más simple y legible. Las secciones de las vistas modelEvents y collectionEvents sirven para los mismo que hacíamos en el método initialize con .on o .listenTo de Backbone.

Los objetos Marionette.ItemView sirven para visuaslizar un modelo (una tarea) y Marionette.CollectionView sirve para mostrar una colección de elementos (una lista de tareas). El objeto Marionette.Layout se utiliza para componer una vista con varias secciones (tareas y estado) donde podremos colocar las vistas de la aplicación, aparte de estas secciones un Layout es como una vista Marionette.ItemView con su plantilla. Con Marionette.Controller construiremos una clase que podemos utilizar para ofrecer la interfaz de la aplicación al exterior del módulo con únicamente los métodos necesarios (sin initialize, render y los métodos onXxx del mismo ejemplo con Backbone). Y hasta aquí, son los cambios más importantes en cuanto al cambio de Backbone a Marionette.

A continuación el código del módulo tareas que contiene la mayor parte del código javascript de la aplicación y el módulo main que no cambia en nada.

define('tareas', [ 'jquery', 'underscore', 'backbone', 'backbone.marionette', 'mustache', 'plantillas', 'i18n!i18n/nls/mensajes' ], function($, _, Backbone, Marionette, Mustache, plantillas, mensajes) {
function trim(string) {
return string.replace(/^\s+|\s+$/gi, '');
}
function render(plantilla, datos, mensajes) {
var d = datos || {};
var m = mensajes || {};
var vista = _.extend(d, {
message: m
});
var f = plantillas[plantilla];
var p = f();
return p(vista);
}
var Tarea = Backbone.Model.extend({
urlRoot : 'rest/tareas/tarea',
defaults : {
id : null,
descripcion : '',
completada : false
},
toJSON : function() {
return {
id : this.get('id'),
descripcion : this.get('descripcion'),
completada : this.get('completada')
};
}
});
var Tareas = Backbone.Collection.extend({
url: 'rest/tareas',
model: Tarea,
findCompletadas: function() {
return this.models.filter(function(tarea) {
return tarea.get('completada');
});
},
removeCompletadas: function() {
_.each(this.findCompletadas(), function(tarea) {
tarea.destroy();
});
}
});
var TareaView = Marionette.ItemView.extend({
tagName: 'li',
ui: {
checkbox: "input[name='completada']"
},
modelEvents: {
'change': 'render'
},
events: {
"change input[name='completada']" : 'onChangeCompletada'
},
serializeData: function() {
var completada = this.model.get('completada');
var data = this.model.toJSON();
_.extend(data, {
attrs: {
checked : (completada) ? 'checked="checked"' : null,
completada : (completada) ? 'completada' : null
}
});
return data;
},
template: function(data) {
return render('tarea', data);
},
// Eventos
onChangeCompletada: function() {
var completada = this.ui.checkbox.is(':checked');
this.model.set('completada', completada);
this.model.save();
}
});
var TareasView = Marionette.CollectionView.extend({
tagName: 'ul',
className: 'tareas',
itemView: TareaView
});
var EstadoView = Marionette.ItemView.extend({
collectionEvents : {
'all': 'render'
},
serializeData: function() {
var completadas = this.options.collection.findCompletadas().length;
var total = this.collection.length;
return {
completadas : completadas,
total : total
};
},
template: function(data) {
var m = {
'COMPLETADAS_tareas_de_TOTAL_completadas': Mustache.render(mensajes.COMPLETADAS_tareas_de_TOTAL_completadas, data),
'Muy_bien_has_completado_todas_las_tareas': mensajes.Muy_bien_has_completado_todas_las_tareas,
};
return render('estado', data, m);
},
render: function() {
this.$el.html(this.template(this.serializeData()));
}
});
var AppLayout = Marionette.Layout.extend({
ui: {
tareaInput: "input[name='nuevaTarea']",
limpiar: "input[name='limpiar']"
},
events: {
"keypress input[name='nuevaTarea']": 'onKeypressNuevaTarea',
"click input[name='limpiar']": 'onClickLimpiar'
},
collectionEvents : {
'all': 'update'
},
regions: {
tareas: "#tareas",
estado: "#estado"
},
template: function() {
var m = {
'Lista_de_tareas': mensajes.Lista_de_tareas,
'Introduce_una_nueva_tarea': mensajes.Introduce_una_nueva_tarea,
'Limpiar': mensajes.Limpiar
};
return render('tareas', null, m);
},
update: function() {
var completadas = this.options.collection.findCompletadas().length;
// Habilitar/deshabilitar el botón de limpiar tareas completadas
if (completadas == 0) {
this.ui.limpiar.attr('disabled', 'disabled');
} else {
this.ui.limpiar.removeAttr('disabled');
}
},
// Eventos
onKeypressNuevaTarea: function(event) {
// Comprobar si la tecla pulsada es el return
if (event.which == 13) {
var descripcion = this.ui.tareaInput.val();
descripcion = trim(descripcion);
// Comprobar si se ha introducido descripción de la tarea
if (descripcion == '') {
return;
}
// Añadir la tarea y limpiar el input
var tarea = new Tarea({
descripcion : descripcion,
completada : false
});
this.options.controller.addTarea(tarea);
this.ui.tareaInput.val('');
}
},
onClickLimpiar: function() {
this.options.controller.removeTareasCompletadas();
}
});
var TareasApp = Marionette.Controller.extend({
initialize: function() {
// Construir el modelo
this.tareas = new Tareas();
// Construir las vistas
this.layout = new AppLayout({
el: this.options.el,
collection: this.tareas,
controller: this
});
this.layout.render();
this.tareasView = new TareasView({
collection: this.tareas
});
this.estadoView = new EstadoView({
collection: this.tareas
});
this.layout.tareas.show(this.tareasView);
this.layout.estado.show(this.estadoView);
this.layout.update();
},
// Métodos
addTarea : function(tarea) {
this.tareas.add(tarea);
tarea.save();
},
removeTareasCompletadas : function() {
this.tareas.removeCompletadas();
},
resetTareas : function(tareas) {
this.tareas.reset(tareas);
},
fetch : function() {
this.tareas.fetch();
}
});
return {
Tarea: Tarea,
Tareas: Tareas,
TareaView: TareaView,
TareasApp: TareasApp
};
});
view raw tareas.js hosted with ❤ by GitHub
define(['tareas'], function(tareas) {
var tareasApp = new tareas.TareasApp({ el: "#tareas" });
// Cargar los datos iniciales de la lista de tareas
// Usar los datos precargados en la página, para evitar una petición
// al servidor, los datos se incluyen en la página html de la aplicación.
//tareasApp.resetTareas(tareas);
// Aunque en la documentación de backbone recomiendan precargar los datos en la
// página, esto impide cachearla, dependiendo
// de la página tal vez sea mejor cachear la página y pedir los datos
// en una petición AJAX.
tareasApp.fetch();
});
view raw main.js hosted with ❤ by GitHub
Para externalizar las plantillas del html podemos usar el plugin de RequireJS text que permite cargar el contenido de archivos como dependencias de un módulo tal como sucede como los archivos javascript. Y para internacionalizar las cadenas de las plantillas el plugin i18n. Los literales de cada idioma se definen en un archivo distinto, en el caso de esta aplicación solo he proporcionado uno, para el idioma inglés en la carperta src/main/webapp/js/i18n/nls/en, cada uno de estos archivos contiene una clave/valor, algunos de los cuales son una plantilla de Mustache para sustituir las variables como en «{{completadas}} tareas de {{total}} completadas». A continuación el código de un plantilla de Mustache que se carga de forma externalizada y el archivo con los literales que se utilizarán en la localización por defecto. Estos dos plugins de RequireJS probablemente nos sean necesarios y útiles en un ejemplo real.

<h2>{{message.Lista_de_tareas}}</h2>
<input type="text" name="nuevaTarea" value="" class="input-xxlarge" placeholder="{{message.Introduce_una_nueva_tarea}}" />
<div id="tareas"></div>
<div id="estado"></div>
<input type="button" name="limpiar" value="{{message.Limpiar}}" class="btn" />
view raw tareas.mustache hosted with ❤ by GitHub
{{#total}}
{{message.COMPLETADAS_tareas_de_TOTAL_completadas}}
{{/total}}
{{^total}}
{{message.Muy_bien_has_completado_todas_las_tareas}}
{{/total}}
view raw estado.mustache hosted with ❤ by GitHub
define({
'root': {
'Lista_de_tareas': 'Lista de tareas',
'COMPLETADAS_tareas_de_TOTAL_completadas': '{{completadas}} tareas de {{total}} completadas',
'Muy_bien_has_completado_todas_las_tareas': '¡Muy bien! has completado todas las tareas',
'Limpiar': 'Limpiar',
'Introduce_una_nueva_tarea': 'Introduce una nueva tarea'
},
'en': true
});
view raw mensajes.js hosted with ❤ by GitHub
El idioma de la aplicación se especifica en el archivo que genera el html de la página.

<script type="text/javascript">
requirejs.config({
shim: {
'underscore': {
exports: '_'
},
'json2': {
exports: 'JSON'
},
'backbone': {
deps: ['jquery', 'underscore', 'json2'],
exports: 'Backbone'
},
'backbone.marionette' : {
deps : [ 'backbone' ],
exports : 'Marionette'
}
},
locale: '${locale}'
});
</script>
view raw Index.tml hosted with ❤ by GitHub
La aplicación tiene el siguiente aspecto:


Para terminar y como prólogo de la siguiente entrada un detalle que comentaré es la cantidad de archivos que se cargan para este ejemplo y eso que no es excesivamente complejo, en total al cargar el ejemplo se hacen 31 peticiones, unas cuantas son de los estilos, fuentes e imágenes pero la mitad son de archivos js y plantillas mustache. En la siguiente entrada explicaré como optimizarlo para conseguir reducir esas 31 peticiones a 13, con una diferencia importante de tiempo de carga y esto en local (probablemente puesto el ejemplo en internet la latencia y la diferencia sería mayor), también conseguiremos reducir algunos kilobytes de peso a la página. La optimización será similar a lo que explique en la Optimización de módulos RequireJS y archivos javascript del ejemplo más sencillo de la Introducción a Backbone pero aplicado a este ejemplo de Marionette que es bastante más complejo. En el código fuente puede verse también como ejecutar pruebas unitarias usando Grunt, Jasmine, Sinon y como integrarlo con Gradle pero la expicación de esto será tema para otra entrada.

Como en el resto de entradas el código fuente completo lo puedes encontrar en mi repositorio de GitHub. Si quieres probarlo en tu equipo lo puedes hacer de forma muy sencilla con los siguientes comandos y sin instalar nada. Si no dispones de git para clonar mi repositorio de GitHub puedes obtener el código fuente del repositorio en un archivo zip con el anterior enlace.

$ git clone git://github.com/picodotdev/elblogdepicodev.git
$ cd elblogdepicodev/MarionetteREST
$ ./gradlew tomcatRun
# Abrir en el navegador http://localhost:8080/MarionetteREST/
view raw git.sh hosted with ❤ by GitHub
Referencia:
Introducción y ejemplo de RequireJS
Introducción y ejemplo de Mustache
Logging en Javascript con log4javascript
Capturar errores de Javascript
Optimizar módulos de RequireJS y archivos Javascript
Patrón de diseño MVC del lado cliente con Backbone.js
Introducción y ejemplo de Backbone.js
Ejemplo de pruebas unitarias en javascript con Jasmine y Sinon
Backbone
Marionette

sábado, 24 de agosto de 2013

Ejemplo del patrón de diseño State

Java
Un patrón de diseño aplicado adecuadamente para resolver un problema puede ayudar enormemente a simplificar el código y facilitar el mantenimiento. Si tenemos un código que es difícil de mantener y entender, hay código duplicado y no tiene ninguna organización puede que aplicar un patrón de diseño nos resuelva el problema en gran parte.

Hace ya un tiempo comente cuales son los principales patrones de diseño y hice una entrada con un ejemplo del patrón de diseño Command. En esta entrada pondré un ejemplo del patrón de diseño State.

El patrón de diseño State nos puede ser de mucha utilidad en los casos que por ejemplo una entidad tenga asociado un grafo de estados con transiciones permitidas y no permitidas entre algunos estados. En función del estado, sus datos y la transición la entidad puede comportarse de forma diferente. Por ejemplo, supongamos que tenemos una entidad Compra que a lo largo de su vida en la aplicación pasa por diferentes estados:
  • creada: la compra se acaba de crear.
  • en espera: se ha hecho una compra y se está esperando que el pago sea correcto.
  • verificada: el pago es correcto y se está esperando a enviar el producto.
  • cancelada: la compra se ha cancelado porque el usuario no quiere ya el producto, no hay existencias u otro motivo.
  • enviada: el pedido ha sido enviado.
Y tiene diferentes transiciones como:
  • comprar: la compra pasa de creada a en espera de verificarla.
  • verificar: la compra pasa de en espera a verificada y esperando a enviarse.
  • cancelar: la compra se puede cancelar excepto una vez que ya se ha enviado.
  • enviar: la compra se envía al usuario y ya no puede cancelarse.
Diagrama de estados

Si diseñamos este flujo de estados sin el patrón State probablemente acabemos con una clase con un montón de condiciones y métodos de bastantes líneas sin una organización clara a simple vista. Para evitarlo aplicaremos el patrón State a este pequeño flujo de estados. En cuanto a código este patrón se basa en dos ideas:
  • Cada estado será representado una clase.
  • Cada una de estas clases contendrá un método por cada posible transición.
Y estas dos simples ideas son suficientes para guiar la tarea de codificación. Por lo tanto tendremos los siguientes clases que representarán a los estados: CreadaCompraState, EnEsperaCompraState, VerificadaCompraState, CanceladaCompraState y EnvidaCompraState.

package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import java.math.BigDecimal;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaEnvio;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaPago;
public interface CompraState {
void comprar(BigDecimal precio, FormaPago formaPago, FormaEnvio formaEnvio);
void verificar();
void cancelar();
void enviar();
}

Según el diagrama de estados y transiciones no todas las transiciones son posibles, una compra en espera no puede enviarse. Pero en el código estamos haciendo que todos los estados tengan todos los métodos que representan todas las transiciones, la forma de hacer en el código que una transición no sea posible para un determinado estado es lanzando una excepción en su correspondiente método, el método enviar del estado en espera, lanzará una excepción ya que es este estado aún la compra no puede enviarse. La clase abstracta AbstractState implementará la interfaz CompraState y lanzará una excepción en todos los métodos, las clases que extiendan de esta podrán redefinir los métodos que necesiten utilizando la propiedad de la programación orientada a objetos del polimorfismo, esta clase abstracta nos permitirá implementar en cada clase de estado únicamente los métodos con las transiciones válidas. Las clases nos podrían quedar de la siguiente forma:

package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import java.math.BigDecimal;
import java.text.MessageFormat;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaEnvio;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaPago;
public abstract class AbstractCompraState implements CompraState {
private Compra compra;
public AbstractCompraState(Compra compra) {
this.compra = compra;
}
public Compra getCompra() {
return compra;
}
@Override
public void comprar(BigDecimal precio, FormaPago formaPago, FormaEnvio formaEnvio) {
throw new IllegalStateException(MessageFormat.format("La compra en estado {0} no puede comprarse", compra.getEstado().getClass().getSimpleName()));
}
@Override
public void verificar() {
throw new IllegalStateException(MessageFormat.format("La compra en estado {0} no puede verificarse", compra.getEstado().getClass().getSimpleName()));
}
@Override
public void cancelar() {
throw new IllegalStateException(MessageFormat.format("La compra en estado {0} no puede cancelarse", compra.getEstado().getClass().getSimpleName()));
}
@Override
public void enviar() {
throw new IllegalStateException(MessageFormat.format("La compra en estado {0} no puede enviarse", compra.getEstado().getClass().getSimpleName()));
}
}
package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import java.math.BigDecimal;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaEnvio;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra.FormaPago;
import es.com.blogspot.elblogdepicodev.pattern.state.CompraStateFactory.Estado;
public class CreadaCompraState extends AbstractCompraState {
public CreadaCompraState(Compra compra) {
super(compra);
}
@Override
public void comprar(BigDecimal precio, FormaPago formaPago, FormaEnvio formaEnvio) {
getCompra().setPrecio(precio);
getCompra().setFormaPago(formaPago);
getCompra().setFormaEnvio(formaEnvio);
getCompra().setEstado(Estado.EN_ESPERA);
}
@Override
public void cancelar() {
getCompra().setEstado(Estado.CANCELADA);
}
}
package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
import es.com.blogspot.elblogdepicodev.pattern.state.CompraStateFactory.Estado;
public class VerificadaCompraState extends AbstractCompraState {
public VerificadaCompraState(Compra compra) {
super(compra);
}
@Override
public void enviar() {
getCompra().setEstado(Estado.ENVIADA);
}
}
package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
public class EnviadaCompraState extends AbstractCompraState {
public EnviadaCompraState(Compra compra) {
super(compra);
}
}
package es.com.blogspot.elblogdepicodev.pattern.state.compraState;
import es.com.blogspot.elblogdepicodev.pattern.state.Compra;
public class CanceladaCompraState extends AbstractCompraState {
public CanceladaCompraState(Compra compra) {
super(compra);
}
}
Para que los métodos puedan acceder y manipular los datos de la compra se les pasa como parámetro en el constructor. La factoría CompraStateFactory encapsula la lógica para construir cada uno de los estados. La implementación de cada uno de los estado podría ser la siguiente.

package es.com.blogspot.elblogdepicodev.pattern.state;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.CanceladaCompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.CompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.CreadaCompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.EnEsperaCompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.EnviadaCompraState;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.VerificadaCompraState;
public class CompraStateFactory {
public enum Estado {
CREADA, EN_ESPERA, VERIFICADA, CANCELADA, ENVIADA
}
public static CompraState buildState(Estado estado, Compra compra) {
CompraState cs = null;
switch (estado) {
case CREADA:
cs = new CreadaCompraState(compra);
break;
case EN_ESPERA:
cs = new EnEsperaCompraState(compra);
break;
case VERIFICADA:
cs = new VerificadaCompraState(compra);
break;
case CANCELADA:
cs = new CanceladaCompraState(compra);
break;
case ENVIADA:
cs = new EnviadaCompraState(compra);
break;
default:
throw new IllegalArgumentException();
}
return cs;
}
}
Finalmente, la clase Compra podría ser de la siguiente forma:

package es.com.blogspot.elblogdepicodev.pattern.state;
import java.math.BigDecimal;
import es.com.blogspot.elblogdepicodev.pattern.state.CompraStateFactory.Estado;
import es.com.blogspot.elblogdepicodev.pattern.state.compraState.CompraState;
public class Compra {
public enum FormaPago {
PAYPAL, TARJETA_CREDITO
}
public enum FormaEnvio {
UPS, TNT, SEUR
}
private CompraState estado;
private BigDecimal precio;
private FormaPago formaPago;
private FormaEnvio formaEnvio;
public Compra() {
setEstado(Estado.CREADA);
}
public CompraState getEstado() {
return estado;
}
public void setEstado(CompraState estado) {
this.estado = estado;
}
public void setEstado(Estado estado) {
this.estado = CompraStateFactory.buildState(estado, this);
}
public BigDecimal getPrecio() {
return precio;
}
public void setPrecio(BigDecimal precio) {
this.precio = precio;
}
public FormaPago getFormaPago() {
return formaPago;
}
public void setFormaPago(FormaPago formaPago) {
this.formaPago = formaPago;
}
public FormaEnvio getFormaEnvio() {
return formaEnvio;
}
public void setFormaEnvio(FormaEnvio formaEnvio) {
this.formaEnvio = formaEnvio;
}
}
view raw Compra.java hosted with ❤ by GitHub

El patrón State puede facilitarnos bastante la vida como programadores pero si el diagrama de estados fuese más complejo, con mucha lógica de negocio y el flujo dependiense de información independiente de la compra quizá deberíamos evaluar su si un motor de procesos (BPMS) como Activiti y un sistema de reglas de negocio (BRMS) como Drools sería más adecuado.

El código fuente completo de ejemplo los puedes obtener de mi repositorio de github con los siguiente comandos:

$ git clone git://github.com/picodotdev/elblogdepicodev.git
$ cd elblogdepicodev/PatronState
$ ./gradlew test
view raw git.sh hosted with ❤ by GitHub
Referencia:
Patrones de diseño en la programación orientada a objetos
Ejemplo del patrón de diseño Command y programación concurrente en Java
Ejemplo del patrón de diseño No Operation

viernes, 16 de agosto de 2013

Pruebas unitarias de código que accede a base de datos

Hibernate
Una de las dificultades que nos solemos encontrar a la hora de hacer pruebas unitarias es como probar el código que accede a la base de datos. El problema es que ese código necesita de una base de datos para ejecutarse, si se usa una base de datos como MySQL o PosgreSQL más que una prueba unitaria puede considerarse una prueba de integración y las pruebas pasan a depender de ese sistema externo con lo que las pruebas no son autónomas.

La base de datos H2 es una base de datos que puede ser embebida en una aplicación. Esta característica hace que pueda servir como base de datos contra la que lanzar los teses sin necesidad de una base de datos externa, además puede ejecutarse en memoria y sin comunicación de red lo que hace que sea muy rápida y los teses también. Tampoco es una solución perfecta ya que H2 puede tener un comportamiento diferente de la base de datos real pero en la mayoría de los casos nos servirá perfectamente.

Siguiendo los artículos anteriores en los que explicaba como acceder a una base de datos en una aplicación Java «standalone» y como hacer búsquedas en entidades de comino usando sin utilizar likes veamos como sería hacer una prueba unitaria de ese código que accede a la base de datos usando JUnit.

El método beforeClass inicializa la persistencia y crea el DAO. El método before borra los datos que haya creado un test anterior y crear los datos de prueba que una prueba puede esperar que existan, en este caso se trata de un único producto. Las pruebas consisten en probar el método findAll y search del DAO (el método removeAll puede considerarse probado en el before aunque podría crearse un caso de prueba específico):

package es.com.blogspot.elblogdepicodev.hibernate;
import java.util.Date;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericDAO;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericDAOImpl;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericSearchDAO;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericSearchDAOImpl;
public class HibernateTest {
private static EntityManagerFactory entityManagerFactory;
private static EntityManager entityManager;
private static GenericDAO dao;
private static GenericSearchDAO sdao;
private static Producto producto;
@BeforeClass
public static void beforeClass() {
entityManagerFactory = Persistence.createEntityManagerFactory("h2");
entityManager = entityManagerFactory.createEntityManager();
dao = new GenericDAOImpl(Producto.class, entityManager);
sdao = new GenericSearchDAOImpl(Producto.class, entityManager);
}
@AfterClass
public static void afterClass() {
entityManager.close();
entityManagerFactory.close();
}
@Before
public void before() throws Exception {
entityManager.getTransaction().begin();
producto = new Producto(
"Tapestry 5 - Rapid web application development in Java",
"Rapid web application development in Java is a comprehensive guide, introducing Apache Tapestry and its innovative approach to building modern web applications. The book walks you through Tapestry 5, from a simple Hello World application to rich Ajax-enabled applications. Written by a core committer this book provides deep insight into the architecture of Tapestry 5. It not only shows you how to achieve specific goals but also teaches you the \"why\". You learn how to build modern, scalable Web 2.0 application with a component-oriented approach. This book also shows how Tapestry brings scripting language productivity within reach of Java developers without sacrificing any of Java's inherent speed and power.",
10l, new Date());
dao.persist(producto);
entityManager.getTransaction().commit();
}
@After
public void after() throws Exception {
entityManager.getTransaction().begin();
dao.removeAll();
entityManager.getTransaction().commit();
}
@Test
public void findAll() {
Assert.assertEquals(1, dao.findAll().size());
}
@Test
public void countAll() {
Assert.assertEquals(1, dao.countAll());
}
@Test
public void remove() {
entityManager.getTransaction().begin();
dao.remove(producto);
entityManager.getTransaction().commit();
Assert.assertEquals(0, dao.countAll());
}
@Test
public void removeAll() {
entityManager.getTransaction().begin();
dao.removeAll();
entityManager.getTransaction().commit();
Assert.assertEquals(0, dao.countAll());
}
@Test
public void search() throws InterruptedException {
sdao.indexAll();
Assert.assertEquals(1, sdao.search("Tapestry", "nombre", "descripcion").size());
}
}
En las pruebas unitarias al código que use el DAO puede proporcionársele un «stub» de este haciendo que devuelva los datos que necesitemos y sin necesidad de una base de datos como H2 pero para probar el DAO hay que disponer de una base de datos. Como es código que también puede contener fallos es bueno que también esté cubierto con algunas pruebas. Con Hibernate todo será más sencillo ya que con este además de abstraernos de la base de datos específica puede crear el esquema con las tablas y campos a partir del modelo de forma que antes de pasar los teses no necesitemos lanzar un script con sentencias SQL para crear las tablas.

Usar H2 como la base de datos contra que lanzar las pruebas unitarias es una solución imperfecta ya que idealmente la base de datos debería ser igual a la base de datos que vaya a usar la aplicación real (probablemente MySQL o PosgreSQL). H2 tiene la ventaja de que los teses se ejecutarán más rápido que con un sistema real externo y que las pruebas son más autónomas ya que no depende de ese sistema externo, aún así si hacemos uso de elementos nativos de la base de datos y queremos que queden cubiertos con pruebas no nos quedará más remedio que disponer de una base de datos «más real» que H2 para ellas aunque como decía estas pasarán a ser más de integración que unitarias.

Como el resto de ejemplos que escribo en el blog el código fuente lo puedes encontrar en mi repositorio de GitHub. Para probar este código en tu equipo basta con ejecutar:

$ git clone git://github.com/picodotdev/elblogdepicodev.git
$ cd elblogdepicodev/HelloWorldHibernate
$ ./gradlew test
view raw git.sh hosted with ❤ by GitHub
Referencia:
Código fuente acceso a base de datos con Hibernate y JPA http://stackoverflow.com/questions/82949/before-and-after-suite-execution-hook-in-junit-4-x

viernes, 9 de agosto de 2013

Búsquedas de texto completo en objetos de dominio

Hibernate
Si hace un tiempo hablaba de un problema muy común que nos encontramos al desarrollar una aplicación como los es internacionalizar los textos de algunos campos de una entidad de dominio, el hacer búsquedas de texto completo sobre esos campos más allá del like que nos ofrece el lenguaje SQL de las base de datos también es un problema a resolver.

La primera solución que se nos puede ocurrir es hacer las búsquedas empleando el like del lenguaje SQL de la base de datos relacional que usemos. Sin embargo, el like de SQL tiene varias limitaciones y además es lento, supone una carga para la base de datos y las coincidencias son muy limitadas no teniendo en consideración peculiaridades del idioma como tildes y aproximaciones. Para tratar de resolver estos problemas podemos usar la librería Hibernate Search que a partir de las clases de dominio y de las propiedades sobre las que queramos hacer búsquedas creará un indice de Lucene para realizar búsquedas más rápidamente y con mejores resultados.

Hibernate Search
Con Hibernate Search que se apoya en Lucene podemos obtener resultados que son aproximaciones a la palabra búsqueda, por ejemplo, si buscamos «refactor» podemos obtener coincidencias para las palabras «refactors», «refactored» y «refactoring» en el lenguaje inglés. Cada lenguaje tiene sus propias reglas de análisis para buscar estas aproximaciones y hay analizadores para la mayoría de lenguajes. Además, podemos obtener una aproximación de las coincidencias encontradas totales al estilo de como hace Google.

Continuando el ejemplo de como usar Hibernate en una aplicación «standalone» vamos a ver que hay que hacer para crear el índice y como realizar una búsqueda empleando Hibernate Search.

package es.com.blogspot.elblogdepicodev.hibernate;
import java.util.Date;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericDAO;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericDAOImpl;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericSearchDAO;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericSearchDAOImpl;
public class HibernateSearch {
public static void main(String[] args) throws Exception {
// Obtener la factoría de sesiones
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("h2");
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
GenericDAO dao = new GenericDAOImpl(Producto.class, entityManager);
GenericSearchDAO sdao = new GenericSearchDAOImpl(Producto.class, entityManager);
entityManager.getTransaction().begin();
dao.persist(new Producto(
"Tapestry 5",
"Rapid web application development in Java is a comprehensive guide, introducing Apache Tapestry and its innovative approach to building modern web applications. The book walks you through Tapestry 5, from a simple Hello World application to rich Ajax-enabled applications. Written by a core committer this book provides deep insight into the architecture of Tapestry 5. It not only shows you how to achieve specific goals but also teaches you the \"why\". You learn how to build modern, scalable Web 2.0 application with a component-oriented approach. This book also shows how Tapestry brings scripting language productivity within reach of Java developers without sacrificing any of Java's inherent speed and power.",
10l, new Date()));
dao.persist(new Producto(
"Raspberry Pi",
"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.",
20l, new Date()));
dao.persist(new Producto(
"Raspberry Pi User Guide",
"The essential guide to getting started with Raspberry Pi computing and programming. Originally conceived of as a fun, easy way for kids (and curious adults) to learn computer programming, the Raspberry Pi quickly evolved into a remarkably robust, credit-card-size computer that can be used for everything from playing HD videos and hacking around with hardware to learning to program! Co-authored by one of the creators of the Raspberry Pi, this book fills you in on everything you need to know to get up and running on your Raspberry Pi, in no time.",
30l, new Date()));
entityManager.getTransaction().commit();
// Realizar la indexación inicial
sdao.indexAll();
entityManager.getTransaction().begin();
String[] terminos = new String[] { "Tapestry", "Pi", "guide" };
for (String termino : terminos) {
// Realizar la búsqueda de texto completo
List<Producto> productos = sdao.search(termino, "nombre", "descripcion");
print(termino, productos);
}
entityManager.getTransaction().commit();
} catch (Exception e) {
e.printStackTrace();
entityManager.getTransaction().rollback();
}
entityManager.close();
entityManagerFactory.close();
}
private static void print(String termino, List<Producto> productos) {
System.out.printf("Resultados para «%1$s»\n", termino);
if (productos.isEmpty()) {
System.out.println("No se han encontrado resultados\n");
} else {
System.out.printf(" # %1$-20s %2$8s %3$s\n", "Nombre", "Cantidad", "Fecha");
int i = 1;
for (Producto producto : productos) {
System.out.printf("%1$3s %2$-20s %3$8s %4$tR\n", i, producto.getNombre(), producto.getCantidad(), producto.getFecha());
++i;
}
}
}
}
package es.com.blogspot.elblogdepicodev.hibernate.dao;
import java.util.List;
import javax.persistence.EntityManager;
import org.apache.lucene.search.Query;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.dsl.QueryBuilder;
public class GenericSearchDAOImpl<T> implements GenericSearchDAO<T> {
private Class<T> clazz;
private FullTextEntityManager fullTextEntityManager;
public GenericSearchDAOImpl(Class<T> clazz, EntityManager entityManager) {
this.clazz = clazz;
this.fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
}
@Override
public void indexAll() throws InterruptedException {
fullTextEntityManager.createIndexer().startAndWait();
}
@Override
public List<T> search(String q, String... campos) {
QueryBuilder qb = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(clazz).get();
Query query = qb.keyword().onFields(campos).matching(q).createQuery();
javax.persistence.Query persistenceQuery = fullTextEntityManager.createFullTextQuery(query, clazz);
return persistenceQuery.getResultList();
}
}
SQL
Otra alternativa a Hibernate Search es si la base de datos soporta «full text search», es decir, el motor de la base de datos soporta en la sintaxis de las sentencias SQL búsquedas de texto completo. En MySQL es posible pero hasta la versión 5.5 solo si la tabla está definida con MyISAM aunque a partir de la versión 5.6 es posible hacerlo con InnoDB que es el modelo de almacenamiento recomendado. La sintaxis de la sentencia SQL para MySQL sería:

select * from articles where match(title, body) against ('MySQL');
view raw mysql.sql hosted with ❤ by GitHub
En PostgreSQL la sintaxis «full text search» es diferente:

select * from articles where to_tsvector(title, body) @@ to_tsquery('PostgreSQL');
view raw postgresql.sql hosted with ❤ by GitHub

Aunque con soluciones específicas del motor de la base de datos como esta perdemos la abstracción de la base de datos que proporciona Hibernate nos evitamos el tener que mantener el índice de Lucene con Hibernate Search.

Elasticsearch
Otra posibilidad muy interesante y tan buena o mejor que las anteriores es utilizar elasticsearch aunque al igual que con Hibernate Search debamos mantener e índice y los datos sincronizados pero eso probablemente sea tema para otra entrada :).

Referencia:
Código fuente búsqueda con Hibernate Search
Internacionalización (i18n) de campos con Hibernate
http://wiki.apache.org/solr/LanguageAnalysis
http://eraunatonteria.wordpress.com/tag/full-text-search/
http://dev.mysql.com/doc/refman/5.0/es/fulltext-search.html

viernes, 2 de agosto de 2013

Acceso a base de datos con Hibernate y JPA

Hibernate
Si usamos Hibernate como la capa de persistencia de nuestra aplicación es muy probable que lo usemos a través de una aplicación web. Sin embargo, en algún caso como una migración de datos, la generación de algún informe o extracción de datos que no queramos o no nos sea posible hacerla en la aplicación web por el tiempo que tarda o la carga que supone para la aplicación web nos puede interesar usar Hibernate desde una aplicación Java normal lanzada desde linea de comandos.

Para ello deberemos obtener el objeto Session si usamos hibernate directamente o el EntityManager si usamos hibernate a través de JPA. La API de JPA es una capa de abstracción para varias librerías de persistencia, ORM, similares a Hibernate. Si Hibernate nos abstrae de la base de datos, JPA nos abstrae del framework de persistencia que vayamos a usar de forma que podamos reemplazarlo por otro sin realizar ninguna modificación al código JPA. Si ya sabemos usar Hibernate usar JPA no nos supondrá mucha dificultad ya que JPA se basa en gran medida en Hibernate.

En el siguiente ejemplo se puede ver como obtener un EntityManager, eliminar todos los objetos y hacer una búsqueda.

package es.com.blogspot.elblogdepicodev.hibernate;
import java.util.Date;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericDAO;
import es.com.blogspot.elblogdepicodev.hibernate.dao.GenericDAOImpl;
public class HibernateJPA {
public static void main(String[] args) {
// Obtener la factoría de sesiones
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("h2");
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
// Crear una transacción e intertar objetos
entityManager.getTransaction().begin();
GenericDAO dao = new GenericDAOImpl(Producto.class, entityManager);
// Borrar los datos que hubiese
dao.removeAll();
dao.persist(new Producto(
"Tapestry 5",
"Rapid web application development in Java is a comprehensive guide, introducing Apache Tapestry and its innovative approach to building modern web applications. The book walks you through Tapestry 5, from a simple Hello World application to rich Ajax-enabled applications. Written by a core committer this book provides deep insight into the architecture of Tapestry 5. It not only shows you how to achieve specific goals but also teaches you the \"why\". You learn how to build modern, scalable Web 2.0 application with a component-oriented approach. This book also shows how Tapestry brings scripting language productivity within reach of Java developers without sacrificing any of Java's inherent speed and power.",
10l, new Date()));
dao.persist(new Producto(
"Raspberry Pi",
"The Raspberry Pi is a credit-card sized computer that plugs into your TV and a keyboard. It’s a capable little PC which can be used for many of the things that your desktop PC does, like spreadsheets, word-processing and games. It also plays high-definition video. We want to see it being used by kids all over the world to learn programming.",
20l, new Date()));
dao.persist(new Producto(
"Raspberry Pi User Guide",
"The essential guide to getting started with Raspberry Pi computing and programming. Originally conceived of as a fun, easy way for kids (and curious adults) to learn computer programming, the Raspberry Pi quickly evolved into a remarkably robust, credit-card-size computer that can be used for everything from playing HD videos and hacking around with hardware to learning to program! Co-authored by one of the creators of the Raspberry Pi, this book fills you in on everything you need to know to get up and running on your Raspberry Pi, in no time.",
30l, new Date()));
entityManager.getTransaction().commit();
// Hacer una búsqueda y mostrar resultados
List<Producto> productos = dao.findAll();
System.out.printf(" # %1$-20s %2$8s %3$s\n", "Nombre", "Cantidad", "Fecha");
int i = 1;
for (Producto producto : productos) {
System.out.printf("%1$3s %2$-20s %3$8s %4$tR\n", i, producto.getNombre(), producto.getCantidad(), producto.getFecha());
++i;
}
} catch (Exception e) {
entityManager.getTransaction().rollback();
}
entityManager.close();
entityManagerFactory.close();
}
}
En el ejemplo he usado un dao genérico que nos puede servir para cualquier tipo de entidad, aunque el dao contenga operaciones básicas como probablemente sean comunes a todas las entidades que tengamos nos será de gran utilidad. A continuación las anotaciones que debemos usar para la entidad a persistir, nada distinto de lo que haríamos en una aplicación web.

package es.com.blogspot.elblogdepicodev.hibernate;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.apache.solr.analysis.LowerCaseFilterFactory;
import org.apache.solr.analysis.SnowballPorterFilterFactory;
import org.apache.solr.analysis.StandardTokenizerFactory;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.Analyzer;
import org.hibernate.search.annotations.AnalyzerDef;
import org.hibernate.search.annotations.DateBridge;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Parameter;
import org.hibernate.search.annotations.Resolution;
import org.hibernate.search.annotations.Store;
import org.hibernate.search.annotations.TokenFilterDef;
import org.hibernate.search.annotations.TokenizerDef;
@Entity
@Indexed
@AnalyzerDef(name = "textoanalyzer", tokenizer = @TokenizerDef(factory = StandardTokenizerFactory.class), filters = {
@TokenFilterDef(factory = LowerCaseFilterFactory.class),
@TokenFilterDef(factory = SnowballPorterFilterFactory.class, params = { @Parameter(name = "language", value = "English") }) })
public class Producto implements Serializable {
private static final long serialVersionUID = 4301591927955774037L;
@Id
@GeneratedValue
private Long id;
@Column(name = "nombre")
@Field(name = "nombre", index = Index.YES, analyze = Analyze.YES)
@Analyzer(definition = "textoanalyzer")
private String nombre;
@Column(name = "descripcion", length = 5000)
@Field(name = "descripcion", index = Index.YES, analyze = Analyze.YES)
@Analyzer(definition = "textoanalyzer")
private String descripcion;
@Column(name = "cantidad")
private Long cantidad;
@Column(name = "fecha")
@Field(index = Index.YES, analyze = Analyze.NO, store = Store.YES)
@DateBridge(resolution = Resolution.DAY)
private Date fecha;
public Producto() {
}
public Producto(String nombre, String descripcion, Long cantidad, Date fecha) {
this.nombre = nombre;
this.descripcion = descripcion;
this.cantidad = cantidad;
this.fecha = fecha;
}
public Long getId() {
return id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getDescripcion() {
return descripcion;
}
public void setDescripcion(String descripcion) {
this.descripcion = descripcion;
}
public Long getCantidad() {
return cantidad;
}
public void setCantidad(Long cantidad) {
this.cantidad = cantidad;
}
public Date getFecha() {
return fecha;
}
public void setFecha(Date fecha) {
this.fecha = fecha;
}
}
view raw Producto.java hosted with ❤ by GitHub
Si JPA no tuviese alguna funcionalidad que si tuviese hibernate desde JPA podemos obtener el objeto Session con el que acceder a la API de hibernate.

Session session = (Session)entityManager.getDelegate();
Ejecutando la aplicación HibernateJPA obtendremos lo siguiente en la consola:

Como comentaba usar Hibernate en una aplicación Java normal no será lo habitual pero este ejemplo es el primer paso para que explique con un ejemplo simple como resolver otro de los problemas que suele ser habitual en muchas aplicaciones que es como hacer búsquedas sobre los objetos de dominio y no me refiero a usar likes de SQL que tienen varias limitaciones aparte de ser lentas si la tabla de búsqueda tiene muchos registros sino usando Hibernate Search.

Para probar los ejemplos en tu equipo puedes hacerlo con los siguientes comandos:

$ git clone git://github.com/picodotdev/elblogdepicodev.git
$ cd elblogdepicodev/HelloWorldHibernate
$ ./gradlew --daemon build copyToLib
$ ./HibernateJPA.sh
view raw git.sh hosted with ❤ by GitHub
Si te ha interesado esta entrada quizá también te interese como hacer internacionalización (i18n) de campos con Hibernate y como hacer búsquedas de texto completo en objetos de dominio.

Referencia:
Código fuente acceso a base de datos con Hibernate y JPA
http://docs.jboss.org/hibernate/core/3.6/quickstart/en-US/html/hibernate-gsg-tutorial-jpa.html
http://www.manning.com/bauer2/
http://www.apress.com/9781430219569