sábado, 29 de junio de 2013

Hemeroteca #3

El blog de pico.dev
Mitad del año y otros seis meses más que han pasado, ya queda menos para las esperadas vacaciones ;). Como he empezado a hacer desde hace un tiempo en esta entrada quiero recapitular las 25 entradas que he escrito durante este primer semestre de 2013, una por semana publicada normalmente durante la tarde de los viernes. Esta será la cuarta Hemeroteca aunque lleve el número #3. Si te has suscrito a mi blog hace poco es buen momento para revisar alguna entrada anterior que he escrito y si hace un tiempo que estabas suscrito puedes releer alguna entrada que ya vistes y que en su momento te interesó o que por falta de tiempo dejaste de leer. Muchas de las entradas que he escrito durante este semestre han estado relacionados con la programación de una o de otra manera.


Articulos sobre Apache Tapestry

En mi blog no podía faltar unas cuantas entradas sobre Apache Tapestry en las que he tratado asuntos importantes que toda aplicación debe abordar y que el framework empleado para desarrollar debe facilitar. Desde servicios REST pasando por la seguridad, persistencia en base de datos, una típico mantenimiento CRUD hasta pruebas unitarias y de integración.

Ejemplo lista de tareas con Backbone, RESTEasy y Tapestry
Seguridad en aplicación web con Apache Tapestry
Seguridad en aplicación web con Apache Tapestry (II)
Persistencia con JPA y Apache Tapestry
Mantenimiento CRUD en Apache Tapestry
Pruebas unitarias y de integración en Apache Tapestry
Skinning de web usando Apache Tapestry

Serie de artículos sobre Javascript

Esta es otra serie de artículos que me ha ocupado varias semanas, en ella explico varias cosas del estado del arte actual sobre Javascript, como es la programación mediante módulos con RequireJS, motores de plantillas con Mustache, logging hasta los tan de moda frameworks MVC (Modelo Vista Controlador) y las pruebas unitarias también para el código javascript.

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
Introducción y ejemplo de Backbone.js
Ejemplo de pruebas unitarias en javascript con Jasmine y Sinon

Serie de artículos sobre REST y RESTEasy

Los servicios REST también están siendo ampliamente utilizados y es que permiten a otras partes consumir los servicios que proporcionemos como si fuesen servicios web o permiten proporcionar una interfaz usando correctamente la semántica del protocolo http.

Ejemplo sencillo de servicio web con RESTEasy
Cliente javascript y java de servicio web REST con RESTEasy
Devolver xml, json o html con RESTEasy
Integración de Apache Tapestry con RESTEasy

Programación

Modificar la base de datos con Liquibase

Anteriores hemerotecas

Si quieres encontrar más artículos como estos puedes visitar las anteriores ediciones de la hemeroteca de este blog:
Hemeroteca #2
Hemeroteca #1
Hemeroteca #0

viernes, 21 de junio de 2013

Pruebas unitarias y de integración en Apache Tapestry

Apache Tapestry
Realizar teses unitarios, de integración y funcionales del código de la aplicación que desarrollamos es necesario para tener cierta seguridad de que lo codificado funciona como se espera al menos bajo las circunstancias de las pruebas. Una vez que tenemos un conjunto de pruebas y necesitamos hacer cambios a código existente las pruebas nos sirven para evitar introducir nuevos defectos, tendremos seguridad de que lo modificado sigue funcionando como antes y no dejaremos de hacer algo por miedo a introducir nuevos errores.

A estas alturas supongo que todos estaremos de acuerdo en que las pruebas son de gran utilidad y necesarias. Además, de lo anterior los teses nos sirven como documentación en forma de código de como se puede usar los objetos bajo prueba. Y por otra parte si usamos un lenguaje dinámico, que tan de moda están en estos momentos, en el que el compilador no suele ayudar en tiempo de desarrollo y solo nos encontramos con los errores en tiempo de ejecución porque hemos puesto mal el nombre de un variable, de método o el número de parámetros son incorrectos las pruebas nos ayudarán a detectarlos al menos en el entorno de integración continua y no en producción, aunque muy posiblemente no siempre porque es raro que tengamos el 100% del código cubierto con teses. Si en Java es necesario tener teses en un lenguaje dinámico como Groovy es vital si no queremos tener errores en producción por temas de «compilación».

Si desarrollamos código de pruebas debemos tratarlo como un ciudadano de primera clase, esto es, con la misma importancia que el resto del código de la aplicación en el que deberíamos aplicar muchas de las ideas explicadas en el libro Clean Code de forma que el código sea legible y más fácilmente mantenible. No hacerlo puede que haga que las pruebas con el tiempo dejen de tener utilidad y peor aún supongan un problema más.

Para realizar pruebas en Apache Tapestry hay algo de documentación en la propia página del proyecto y en algunas librerías relacionadas pero está esparcida por varias páginas y para alguien que está empezando no es sencillo documentarse e iniciar un proyecto haciendo pruebas desde un inicio de forma rápida. En esta entrada explicaré varias formas de hacer pruebas unitarias, de integración y funcionales y como ejecutarlas de forma cómoda haciendo uso de Gradle, Geb, Spock entre otras herramientas.

Pruebas unitarias

En Tapestry realizar pruebas unitarias consiste en probar las páginas y componentes (las páginas en realidad son también componentes y se pueden probar de la misma forma). Dado que las clases de los componentes y páginas son simples POJO (Plain Old Java Object) que no heredan de ninguna clase, no tienen necesidad de implementar ninguna interfaz y no son abstractas una forma de probarlas es tan simple como crear una una instancia, inyectar las dependencias de las que haga uso el SUT (Sujeto bajo prueba, Subject Under Test) y comprobar los resultados. Este es el caso de la prueba realizada en HolaMundoTest, en la que se prueba el método beginRender. Si el componente tuviese otros métodos podrían probarse de forma similar. En este ejemplo se realizan las siguientes cosas:
  • Se crean las dependencias de las que haga el sujeto bajo prueba, en este caso un mock del servicio MensajeService que devolverá el mensaje que emitirá el componente. En un ejemplo real podría tratarse de un servicio que accediese a base de datos o se conectase con un servicio externo. El mock se crea haciendo uso de la librería Mockito.
  • Se crea la instancia del componente, como la clase del componente no es abstracta es tan sencillo como hacer un new.
  • Se inyectan las dependencias. El nombre al que saludará el componente y el mock que devolverá el mensaje que deseamos en la prueba. Para poder inyectar las propiedades estas están definidas en el ámbito package, las propiedades de un componente pueden definirse en el ámbito private pero entonces necesitaríamos definir al menos métodos set para asignar valores a esas propiedades.
  • Se crea una instancia de un objeto que necesita como parámetro el método bajo prueba beginRender y se le pasa como parámetro.
  • El método bajo prueba se ejecuta.
Finalmente, se comprueba el resultado de la ejecución con un Assert. Como conocemos los datos que ha usado el objeto (los inyectados en las dependencias) bajo prueba conocemos el resultado que debería producir y es lo que comprobamos. Un ejemplo de este tipo de test es HolaMundoTest que prueba el componente HolaMundo.

package es.com.blogspot.elblogdepicodev.test.tapestry.components;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.internal.services.MarkupWriterImpl;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import es.com.blogspot.elblogdepicodev.test.tapestry.services.MensajeService;
public class HolaMundoTest {
@Test
public void conNombre() {
// Dependencias
MensajeService mensajeService = Mockito.mock(MensajeService.class);
Mockito.when(mensajeService.getMensaje()).thenReturn("Hola mundo {0}!!!");
// Crear el componente
HolaMundo componente = new HolaMundo();
// Inyectar las dependencias
componente.nombre = "picodotdev";
componente.mensajeService = mensajeService;
// Ejecutar el sujecto bajo prueba
MarkupWriter writer = new MarkupWriterImpl();
componente.beginRender(writer);
// Comprobar el resultado
Assert.assertEquals("Hola mundo picodotdev!!!", writer.toString());
}
}

Pruebas unitarias incluyendo código HTML

En un framework web aparte el comprobar el funcionamiento del código java (u otro lenguaje) es solo una parte de lo que nos puede interesar probar. Un framework web nos puede interesar tener pruebas del código html que se genera, en el caso de Tapestry los componentes o páginas que generan su html con las plantillas .tml o como en el caso anterior en el método beginRender. Las páginas pueden probarse de forma sencilla haciendo uso de la clase TapestryTester, aunque como las páginas puede incluir muchos componentes (y tendríamos que inyectar muchas dependencias y mocks) no es lo mejor para hacer pruebas unitarias, las pruebas de las páginas enteras es mejor dejarlo para pruebas funcionales y realizar pruebas unitarias sobre los componentes individuales.

Dado que en Tapestry un componente no puede ser usado sino no es dentro de una página, para probar el html de un componente generado con plantillas .tml de forma unitaria debemos crear un página de pruebas en la que incluimos únicamente ese componente. El componente HolaMundo no tiene una plantilla .tml que genera el html pero esto es indiferente para las pruebas, independientemente de si el componente genera el html con el método beginRender o con un .tml podemos hacer la prueba de la misma forma.

Las cosas que tendríamos que hacer son:
  • Crear una página de pruebas en la que insertaremos el componente que queramos probar. Para el ejemplo la página de pruebas es HolaMundoTest.
  • En la prueba unitaria, HolaMundoTesterTest, disponemos una instancia de TapestryTester. Dado que la creación del TapestryTester va a ser igual para todos los teses que tuviésemos creamos una clase abstracta de la que heredarán todos, AbstractTest. Para crear el TapestryTester necesitaremos indicar el paquete de la aplicación, el nombre de la aplicación, el directorio del contextRoot y los módulos adicionales a cargar. El módulo adicional de pruebas TestModule añadirá las páginas que se usarán para hacer las pruebas como si se tratase de una librería adicional de componentes, esto se hace en el método contributeComponentClassResolver.
  • Crear los mocks y dependencias que use el componente. En el método before de la prueba se crea el mock del servicio que usa el componente. Con la anotación @ForComponents de la librería Tapestry Testify sobre la propiedad mensajeService del test hacemos que los componentes de la prueba que usen un servicio de interfaz MensajesService se les inyecte la referencia del mock que hemos creado. En el caso de que el componente tenga parámetros la forma de pasarle el valor que deseamos se consigue inyectando el objeto primeramente en la página de prueba que hace uso del componente y posteriormente hacemos que la página le pase el valor en el momento que lo usa. Dado que se trata de un String, y Tapestry hace las inyecciones de los servicios en función del tipo, debemos darle un nombre único para que el contenedor de dependencias de Tapestry distinga que String queremos inyectar.
  • Ejecutar la prueba consistirá en renderizar la página con renderPage.
Finalmente, la comprobación la realizaremos mediante asserts sobre valores devueltos por objeto Document que representa al DOM de la página. Un ejemplo de este tipo de test es HolaMundoTesterTest.
package es.com.blogspot.elblogdepicodev.test.tapestry.test.pages;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.Service;
import org.apache.tapestry5.ioc.annotations.Inject;
public class HolaMundoTest {
// Parámetro que recibirá el componete.
// El valor se obtiene como si se tratase de un servicio mediante
// la anotación @Inject/@Service, como el tipo es String para distinguir
// entre varios se le da un nombre.
@Inject
@Service("nombre")
@Property
private String nombre;
}
<!DOCTYPE html>
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
<head></head>
<body>
<div id="componenteSinNombre"><t:holaMundo/></div>
<div id="componenteConNombre"><t:holaMundo nombre="nombre"/></div>
</body>
</html>
package es.com.blogspot.elblogdepicodev.test.tapestry.test;
import com.formos.tapestry.testify.core.TapestryTester;
import com.formos.tapestry.testify.junit4.TapestryTest;
import es.com.blogspot.elblogdepicodev.test.tapestry.test.services.TestModule;
public abstract class AbstractTest extends TapestryTest {
private static final TapestryTester SHARED_TESTER = new TapestryTester("es.com.blogspot.elblogdepicodev.test.tapestry", "app", "src/main/webapp", TestModule.class);
public AbstractTest() {
super(SHARED_TESTER);
}
}
package es.com.blogspot.elblogdepicodev.test.tapestry.test.services;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.services.LibraryMapping;
public class TestModule {
public static void contributeComponentClassResolver(Configuration<LibraryMapping> configuration) {
configuration.add(new LibraryMapping("test", "es.com.blogspot.elblogdepicodev.test.tapestry.test"));
}
}
view raw TestModule.java hosted with ❤ by GitHub
package es.com.blogspot.elblogdepicodev.test.tapestry.components;
import org.apache.tapestry5.dom.Document;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import com.formos.tapestry.testify.core.ForComponents;
import es.com.blogspot.elblogdepicodev.test.tapestry.services.MensajeService;
import es.com.blogspot.elblogdepicodev.test.tapestry.test.AbstractTest;
public class HolaMundoTesterTest extends AbstractTest {
@ForComponents("nombre")
private String nombre;
@ForComponents
private MensajeService mensajeService;
@Before
public void before() {
// Crear el mock del servicio
mensajeService = Mockito.mock(MensajeService.class);
Mockito.when(mensajeService.getMensaje()).thenReturn("Hola mundo {0}!!!");
}
@Test
public void sinNombre() {
Document doc = tester.renderPage("test/HolaMundoTest");
Assert.assertEquals("Hola mundo Tapestry!!!", doc.getElementById("componenteSinNombre").getChildMarkup());
}
@Test
public void conNombre() {
nombre = "picodotdev";
Document doc = tester.renderPage("test/HolaMundoTest");
Assert.assertEquals("Hola mundo picodotdev!!!", doc.getElementById("componenteConNombre").getChildMarkup());
}
}
Para probar el componente con un parámetro y sin parámetro en la página de prueba el componente se usa dos veces. Según la prueba se obtiene el elemento id que lo contenía (componenteSinNombre, componenteConNombre) para comprobar el resultado.

Pruebas unitarias incluyendo código HTML con XPath

En el caso anterior para hacer las comprobaciones se hace uso del objeto Document el cual se va navegando con su API. Obtener la información necesaria para realizar las comprobaciones no es tarea simple si el html es complejo, el código Java necesario para ello puede complicarse y ser de varias lineas para obtener un simple dato. Con el objetivo de tratar de aliviar este problema se puede hacer uso de la librería Tapestry XPath mediante la cual podremos hacer uso de expresiones XPath sobre el objeto Document que obtenemos como resultado.

package es.com.blogspot.elblogdepicodev.test.tapestry.components;
import org.apache.tapestry5.dom.Document;
import org.jaxen.JaxenException;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import com.formos.tapestry.testify.core.ForComponents;
import com.formos.tapestry.xpath.TapestryXPath;
import es.com.blogspot.elblogdepicodev.test.tapestry.services.MensajeService;
import es.com.blogspot.elblogdepicodev.test.tapestry.test.AbstractTest;
public class HolaMundoXPathTesterTest extends AbstractTest {
@ForComponents("nombre")
private String nombre;
@ForComponents
private MensajeService mensajeService;
@Before
public void before() {
mensajeService = Mockito.mock(MensajeService.class);
Mockito.when(mensajeService.getMensaje()).thenReturn("Hola mundo {0}!!!");
}
@Test
public void sinNombre() throws JaxenException {
Document doc = tester.renderPage("test/HolaMundoTest");
String text = TapestryXPath.xpath("id('componenteSinNombre')").selectSingleElement(doc).getChildMarkup();
Assert.assertEquals("Hola mundo Tapestry!!!", text);
}
@Test
public void conNombre() throws JaxenException {
nombre = "picodotdev";
Document doc = tester.renderPage("test/HolaMundoTest");
String text = TapestryXPath.xpath("id('componenteConNombre')").selectSingleElement(doc).getChildMarkup();
Assert.assertEquals("Hola mundo picodotdev!!!", text);
}
}

Pruebas de integración

Si es posible es mejor realizar pruebas unitarias utilizando alguno de los casos anteriores principalmente porque son más sencillas, pequeñas y menos frágiles (menos propensas a empezar a fallar ante cambios) pero sobre todo porque se ejecutan mucho más rápido y de esta manera podemos lanzarlas muy a menudo en nuestro entorno local según desarrollamos. Si tardasen en ejecutarse mucho al final por no estar parados esperando a que se ejecutasen las pruebas acabaríamos por no ejecutarlas, si este es el caso es recomendable hacer que se ejecuten al menos en un entorno de integración continua (usar Jenkins es una buena opción).

Sin embargo, también hay casos en los que nos puede interesar hacer pruebas funcionales sobre la aplicación probando no pequeñas partes de forma individual sino todas en conjunto. Si vemos necesario realizar este tipo de pruebas funcionales o de aceptación conviene realizarlas sobre las partes importantes o vitales de la aplicación sin querer volver a probar lo ya probado de modo unitario con este tipo de pruebas. Como decía son lentas y frágiles ante cambios y si tenemos muchas nos veremos obligados a dedicar mucho esfuerzo a mantenerlas que puede que no compense.

Para realizar este tipo de pruebas en Tapestry en el siguiente ejemplo haremos uso de Gradle, el plugin de tomcat y el framework de pruebas Geb junto con Spock. Para hacer las pruebas con Geb usaremos el lenguaje Groovy. Tradicionalmente hacer pruebas funcionales o de aceptación era una tarea no sencilla comparada con las pruebas unitarias, con la ayuda de Geb y Spock realizaremos pruebas funcionales de una forma bastante simple y manejable.

Con Geb los teses de denominan especificaciones. Haremos una prueba de la página Index de la aplicación que contiene poco más que el componente HolaMundo. Para ello:
  • Crearemos las especificación. Una especificación es una clase que hereda de GebSpec. Combinando Geb con Spock y su DSL (Lenguaje específico de dominio, Domain Specific Language) el test del ejemplo se divide en varias partes.
  • La parte when se encargará de ejercitar el sujeto bajo prueba, en este caso la página Index.
  • En la parte then realizaremos las comprobaciones que determinarán si el test se ejecutó de forma correcta.
Junto con la especificación del test podemos definir como es la página que va a probar el test, esto simplificará enormemente el código del test y es lo que hace que Geb simplifique mucho las pruebas funcionales. Si tuviésemos varios test estos pueden compartir todos ellos las definiciones de las páginas. La página se define creando una clase que extiende de Page. En el caso del ejemplo:
  • La propiedad estática url, indica la URL de la página a probar. La aplicación debe estar arrancada previamente a pasar las pruebas de integración o funcionales.
  • La propiedad estática at, es una comprobación que realizará Geb para determinar si la página que se obtiene con la URL es la que se espera.
  • Y ahora viene lo mejor, en la propiedad estática content, podemos definir los elementos relevantes de la página para la prueba que luego en la especificación del test Geb podremos usar para realizar las comprobaciones. La notación para referirse a los elementos es similar a la utilizada en los selectores de jQuery.
Un ejemplo de este tipo de test es IndexSpec. Otros ejemplos un poco más complejos pueden verse en GoogleSpec y GoogleSearchSpec.

package es.com.blogspot.elblogdepicodev.test.geb
import geb.Page
import geb.spock.GebSpec
class IndexPage extends Page {
static url = 'http://localhost:8080/TapestryTest/Index'
static at = { title.endsWith('Apache Tapestry') }
static content = {
id { $('#id1') }
}
}
class IndexSpec extends GebSpec {
def 'go to index'() {
when:
to IndexPage
then:
id.text() == 'Hola mundo Tapestry!!!'
}
}
package es.com.blogspot.elblogdepicodev.test.geb
import geb.Page
import geb.spock.GebSpec
class GoogleHomePage extends Page {
static url = 'http://google.es/'
static at = { title == 'Google' }
static content = {
searchField { $("input[name=q]") }
searchButton(to: GoogleResultsPage) { $("input[value='Buscar con Google']") }
}
}
class GoogleResultsPage extends Page {
static at = { waitFor { title.endsWith("Buscar con Google") } }
static content = {
results(wait: true) { $("li.g") }
result { index -> return results[index] }
resultLink { index -> result(index).find("h3.r a") }
}
}
class GoogleSearchSpec extends GebSpec {
def 'go to google'() {
when:
to GoogleHomePage
searchField().value "Chuck Norris"
searchButton().click()
then:
at GoogleResultsPage
resultLink(0).text().contains("Chuck")
}
}
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 previamente (salvo java y git). 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/TapestryTest
$ ./gradlew test
$ ./gradlew integrationTest
view raw git.sh hosted with ❤ by GitHub
Referencia:
JUnit 
Spock
Geb
Testify

viernes, 14 de junio de 2013

Mantenimiento CRUD en Apache Tapestry

Apache Tapestry
El scaffolding es una ayuda que permite a partir de una descripción disponer de una funcionalidad sin escribir una línea de código o muy pocas exceptuando las necesarias para la descripción. En funcionalidades que se repiten mucho en una aplicación, no aportan mucho valor y son aburridas de programar puede llegar ahorrar una cantidad considerable de tiempo y disponer de un prototipo funcional muy rápidamente. Es utilizada por varios frameworks web en diferentes lenguajes para hacer los mantenimientos CRUD (Create, Read, Update, Delete) de las tablas de una base de datos relacional:
  • Ruby on Rails
  • Symfony
  • Grails
  • Y quizá algunos otros menos conocidos
El problema de los scaffoldings viene cuando la funcionalidad que proporcionan no es suficiente, no se adapta a lo que necesitamos o no nos gusta lo que se genera y nos vemos en la obligación de personalizarlo, los scaffolding son tan básicos que si tenemos una aplicación un poco compleja probablemente deberemos adaptarlos. En ese caso no nos quedará más remedio que programar lo que necesitemos. Algunos frameworks permiten generar un código inicial a partir del que comenzar la programación. Esto es el caso de Grails y el código generado deja bastante que desear (en mi humilde opinión), el CRUD de una tabla tiene cinco gsp, entre ellas una para el alta y otra para la modificación que son prácticamente iguales con lo que contiene bastante código repetido también en el controlador para las acciones save y update que para luego mantenerlo supone problemas. Una vez que el código está generado y lo hemos personalizado con alguna modificación al salir una nueva versión del framework (y tarde o temprano saldrá) tendremos la duda de si el código que se nos generó con una versión anterior se ha quedado obsoleto en la nueva versión, dudaremos si partir del código inicial que genera la nueva versión e incorporar las cambios que hicimos o si adaptamos el código que tenemos con los nuevos cambios.

Como resultado al final disponer de un framework que tenga scaffolding con el tiempo puede no suponer tanto ahorro de tiempo como puede parecer en un principio. Tapestry no proporciona un scaffolding para hacer estos CRUD aunque si proporciona varios componentes avanzados que ayudan mucho a realizarlos (Grid, BeanEditor). A continuación pondré un ejemplo de una funcionalidad similar a un CRUD realizada con el framework Apache Tapestry. En el ejemplo se podrá mantener una tabla de una base de datos pudiendo dar de alta una nuevos registros, eliminarlos, modificarlos y ver sus datos en una tabla con paginación Ajax. En el se verá que todo esto no supone más de 200 líneas de código Java y 80 líneas de código de presentación en tml. Necesitará solo dos archivos, uno para código Java, otro para el de presentación tml e increiblemente ninguno para código Javascript a pesar de hacer la paginación via Ajax. En estos números no estoy incluyendo varias clases de apoyo como la entidad a persistir o su DAO (Data Access Object) ya que no son propios de un único scaffolding sino que se podría utilizar para todos los que tengamos en la aplicación.

En la entrada sobre persistencia JPA usando Tapestry ya expliqué como construir el servicio y la transaccionalidad. En el siguiente código utilizaré ese servicio DAO para el acceso a la base de datos, no cambia nada. Me centraré en poner el código necesario para la página que necesita proporcionar la funcionalidad del scaffolding.

A continuación el código Java en la que principalmente Tapestry se encarga de llamar al método que actúa de escuchador para cada evento que se produzca en la página, en función del evento se llamará a la operación DAO adecuada y actualizar el estado del controlador.

package es.com.blogspot.elblogdepicodev.tapestry.jpa.pages.admin;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.annotations.Cached;
import org.apache.tapestry5.annotations.Component;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.beaneditor.BeanModel;
import org.apache.tapestry5.corelib.components.Form;
import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.BeanModelSource;
import org.apache.tapestry5.services.TranslatorSource;
import es.com.blogspot.elblogdepicodev.tapestry.jpa.dao.ProductoDAO;
import es.com.blogspot.elblogdepicodev.tapestry.jpa.entities.Producto;
import es.com.blogspot.elblogdepicodev.tapestry.jpa.misc.JPAGridDataSource;
import es.com.blogspot.elblogdepicodev.tapestry.jpa.misc.Pagination;
public class ProductoAdmin {
private enum Modo {
ALTA, EDICION, LISTA
}
@Inject
private ProductoDAO dao;
@Inject
private EntityManager entityManager;
@Inject
@Symbol(SymbolConstants.TAPESTRY_VERSION)
@Property
private String tapestryVersion;
@Inject
private TranslatorSource translatorSource;
@Inject
private BeanModelSource beanModelSource;
@Inject
private Block edicionBlock, listaBlock;
@Inject
private ComponentResources resources;
@Component
private Form form;
private Modo modo;
@Property
private Producto producto;
void onActivate(Long id, Modo modo) {
setModo(modo, (id == null) ? null : dao.findById(id));
}
Object[] onPassivate() {
return new Object[] { (producto == null) ? null : producto.getId(), (modo == null) ? null : modo.toString().toLowerCase() };
}
void setupRender() {
if (modo == null) {
setModo(Modo.LISTA, null);
}
}
void onPrepareForSubmitFromForm() {
onPrepareForSubmitFromForm(null);
}
void onPrepareForSubmitFromForm(Long id) {
if (id != null) {
// Si se envía un id se trata de una edición, buscarlo
producto = dao.findById(id);
}
if (producto == null) {
producto = new Producto();
}
}
Object onCanceledFromForm() {
setModo(Modo.LISTA, null);
return ProductoAdmin.class;
}
void onSuccessFromForm() {
dao.persist(producto);
setModo(Modo.LISTA, null);
}
void onNuevo() {
setModo(Modo.ALTA, null);
}
void onEditar(Long id) {
setModo(Modo.EDICION, dao.findById(id));
}
void onEliminarTodos() {
dao.removeAll();
setModo(Modo.LISTA, null);
}
void onEliminar(Long id) {
producto = dao.findById(id);
dao.remove(producto);
setModo(Modo.LISTA, null);
}
public boolean hasProductos() {
return getSource().getAvailableRows() > 0;
}
public GridDataSource getSource() {
return new JPAGridDataSource<Producto>(entityManager, Producto.class) {
@Override
public List<Producto> find(Pagination pagination) {
return dao.findAll(pagination);
}
};
}
public BeanModel<Producto> getModel() {
BeanModel<Producto> model = beanModelSource.createDisplayModel(Producto.class, resources.getMessages());
model.exclude("id");
model.add("action", null).label("").sortable(false);
return model;
}
public Block getBlock() {
switch (modo) {
case ALTA:
case EDICION:
return edicionBlock;
default:
case LISTA:
return listaBlock;
}
}
// La anotacion @Cached permite cachar el resultado de un método de forma
// que solo se evalúe
// una vez independientemente del número de veces que se llame en la
// plantilla de visualización.
@Cached
public Map<String, String> getLabels() {
Map<String, String> m = new HashMap<String, String>();
switch (modo) {
case ALTA:
m.put("titulo", "Alta producto");
m.put("guardar", "Crear producto");
break;
case EDICION:
m.put("titulo", "Modificación producto");
m.put("guardar", "Modificar producto");
break;
default:
}
return m;
}
private void setModo(Modo modo, Producto producto) {
switch (modo) {
case ALTA:
this.producto = new Producto();
break;
case EDICION:
if (producto == null) {
modo = Modo.ALTA;
this.producto = new Producto();
} else {
this.producto = producto;
}
break;
default:
case LISTA:
this.producto = null;
break;
}
this.modo = modo;
}
}
Ahora el archivo tml que se encarga de generar el html que se enviará al navegador. En el mismo archivo está el código para mostrar el listado y para realizar la edición tanto en el alta como en la modificación. En función del estado de la página se mostrará uno u otro haciendo uso del componente Delegate. Para ver la paginación en funcionamiento sin necesidad de crear muchos objetos solo será necesario crear dos, esto es, las páginas solo contendrán dos elementos.

<!DOCTYPE html>
<html t:type="layout"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
xmlns:p="tapestry:parameter">
Versión: <b>${tapestryVersion}</b><br/>
<t:holaMundo/><br/>
<t:delegate to="block"/>
<t:block id="listaBlock">
<h1>Lista de productos</h1>
<t:grid source="source" row="producto" model="model" rowsPerPage="2" lean="true" inPlace="true" class="table table-bordered table-condensed">
<p:nombreCell>
<t:eventlink event="editar" context="producto.id">${producto.nombre}</t:eventlink>
</p:nombreCell>
<p:actionCell>
<t:eventlink event="eliminar" context="producto.id" class="btn btn-danger">Eliminar</t:eventlink>
</p:actionCell>
<p:empty>
<p>No hay productos.</p>
</p:empty>
</t:grid>
<t:eventlink event="nuevo" class="btn btn-primary">Nuevo producto</t:eventlink>
<t:if test="hasProductos()"><t:eventlink event="eliminarTodos" class="btn btn-danger">Eliminar todos</t:eventlink></t:if>
</t:block>
<t:block id="edicionBlock">
<t:remove>
En otros frameworks la lógica para obtener el título del bloque según se trate de un alta o una modificación,
probablemente se hiciese metiendo lógica en la plantilla de presentación, dado que Tapestry permite llamar a métodos
de la clase Java asociada al componente es mejor dejar esa lógica en el código Java de esta manera la plantilla será más
sencilla y clara además de aprovecharnos del compilador. labels es un método definido en la página admin.producto
que devuelve un mapa.
</t:remove>
<h1>${labels.get('titulo')}</h1>
<t:form t:id="form" context="producto.id" validate="producto" clientValidation="none" class="form-horizontal">
<t:errors class="literal:alert alert-error"/>
<div class="control-group">
<t:label for="nombre" class="control-label"/>
<div class="controls">
<input t:type="textfield" t:id="nombre" value="producto.nombre" size="100" label="Nombre"/>
</div>
</div>
<div class="control-group">
<t:label for="descripcion" class="control-label"/>
<div class="controls">
<input t:type="textarea" t:id="descripcion" value="producto.descripcion" label="Descripción"/>
</div>
</div>
<div class="control-group">
<t:label for="cantidad" class="control-label"/>
<div class="controls">
<input t:type="textfield" t:id="cantidad" value="producto.cantidad" size="4" label="Cantidad"/>
</div>
</div>
<div class="control-group">
<t:label for="fecha" class="control-label"/>
<div class="controls">
<input t:type="textfield" t:id="fecha" type="date" value="producto.fecha" label="Fecha"/>
</div>
</div>
<div class="control-group">
<div class="controls">
<input t:type="submit" class="btn btn-primary" value="prop:labels.get('guardar')"/>
<t:if test="producto.id"><t:eventlink event="eliminar" context="producto.id" class="btn btn-danger">Eliminar</t:eventlink></t:if>
<input t:type="submit" class="btn" value="Cancelar" mode="cancel"/>
</div>
</div>
</t:form>
</t:block>
</html>
Eso es todo lo propio que necesita el scaffolding y sería lo único necesario para realizar otros similares. Sin embargo, este hace uso de unas cuantas clases de utilidad. Entre ellas la clase que proporciona la funcionalidad de paginación al acceder a la lista de entidades. A partir de los datos de paginación que el data source indica (elemento de inicio, de fin y orden) se construye el objeto que contendrá esta información y se le enviará al DAO para que haga la consulta adecuada.

Finalmente, como comenté un una entrada anterior sobre seguridad sobre XSS (Cross Site Scripting) el mantenimiento está protegido ante este tipo de ataques (puedes probar crear un producto con <script>alert(1);</script>), para ello no hemos hecho nada especial.

package es.com.blogspot.elblogdepicodev.tapestry.jpa.misc;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import org.apache.tapestry5.grid.GridDataSource;
import org.apache.tapestry5.grid.SortConstraint;
@SuppressWarnings("rawtypes")
public abstract class JPAGridDataSource<T> implements GridDataSource {
private EntityManager entityManager;
private Class type;
private int start;
private List<T> results;
public JPAGridDataSource(EntityManager entityManager, Class type) {
this.entityManager = entityManager;
this.type = type;
}
@Override
public int getAvailableRows() {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
cq.select(cb.count(cq.from(type)));
return entityManager.createQuery(cq).getSingleResult().intValue();
}
@Override
public void prepare(int start, int end, List<SortConstraint> sort) {
Pagination pagination = new Pagination(start, end, Sort.fromSortConstraint(sort));
this.start = start;
results = find(pagination);
}
public abstract List<T> find(Pagination pagination);
@Override
public Object getRowValue(int i) {
return results.get(i - this.start);
}
@Override
public Class getRowType() {
return type;
}
}
Y otro par de clases muy sencillitas que continenen los datos para que el DAO sepa como se quiere la ordenación y que elementos paginados se quieren.

package es.com.blogspot.elblogdepicodev.tapestry.jpa.misc;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Root;
public class Pagination {
private int start;
private int end;
private List<Sort> sort;
public Pagination(int start, int end, List<Sort> sort) {
this.start = start;
this.end = end;
this.sort = sort;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
public List<Sort> getSort() {
return sort;
}
public void setSort(List<Sort> sort) {
this.sort = sort;
}
@SuppressWarnings("rawtypes")
public List<Order> getOrders(Root root, CriteriaBuilder cb) {
List<Order> orders = new ArrayList<Order>();
for (Sort s : sort) {
Order o = s.getOrder(root, cb);
if (o != null) {
orders.add(o);
}
}
return orders;
}
}
view raw Pagination.java hosted with ❤ by GitHub
package es.com.blogspot.elblogdepicodev.tapestry.jpa.misc;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Root;
import org.apache.tapestry5.grid.SortConstraint;
public class Sort {
private String property;
private Direction direction;
public Sort(String property, Direction direction) {
this.property = property;
this.direction = direction;
}
public String getProperty() {
return property;
}
public void setProperty(String property) {
this.property = property;
}
public Direction getDirection() {
return direction;
}
public void setDirection(Direction direction) {
this.direction = direction;
}
@SuppressWarnings("rawtypes")
public Order getOrder(Root root, CriteriaBuilder builder) {
switch (direction) {
case ASCENDING:
return builder.asc(root.get(property));
case DESCENDING:
return builder.desc(root.get(property));
default:
return null;
}
}
public static List<Sort> fromSortConstraint(List<SortConstraint> sort) {
List<Sort> cs = new ArrayList<Sort>();
for (SortConstraint s : sort) {
String property = s.getPropertyModel().getPropertyName();
Direction direction = Direction.UNSORTED;
switch (s.getColumnSort()) {
case ASCENDING:
direction = Direction.ASCENDING;
break;
case DESCENDING:
direction = Direction.DESCENDING;
break;
default:
}
Sort c = new Sort(property, direction);
cs.add(c);
}
return cs;
}
}
view raw Sort.java hosted with ❤ by GitHub
El resultado de la lista, la modificación de un producto y el alta habiendo errores de validación es este:


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 previamente (salvo java y git). 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/TapestryJPA
$ ./gradlew tomcatRun
# Abrir en el navegador http://localhost:8080/TapestryJPA/admin/producto
view raw git.sh hosted with ❤ by GitHub

Referencia:
Documentación sobre Apache Tapestry

viernes, 7 de junio de 2013

Persistencia con JPA y Apache Tapestry

En Java disponemos de varias opciones para persistir la información a una base de datos relacional, algunas de ellas son:
Apache Tapestry
Hibernate

  • JDBC: es la API que viene integrada en la propia plataforma Java sin necesidad de ninguna librería adicional exceptuando el driver JDBC para acceder a la base de datos. Mediante esta opción se tiene total flexibilidad y evita la abstracción y sobrecarga de los sistemas como Hibernate y JPA. Se trabaja con el lenguaje SQL de forma directa y este lenguaje puede varia en algunos aspectos de una base de datos a otra con lo que para migrar a otra base de datos puede implicar reescribir las SQL de la aplicación. Su utilización de forma directa no es tan habitual aunque en casos que se necesite acceder de forma masiva a los datos puede se útil para evitar la sobrecarga o complejidad que añaden sistemas como Hibernate o JPA.
  • Hibernate: el modelo relacional de las bases de datos es distinto del modelo de objetos del los lenguajes orientados a objetos. Los sistemas ORM como Hibernate tratan de hacer converger el sistema relacional hacia un modelo más similar al modelo de objetos de lenguajes como Java, de forma que trabajar con ellos sea como trabajar con objetos. En ORM como Hibernate normalmente no se trabaja a nivel de SQL como con JDBC sino que se trabaja con objetos (POJO), las consultas devuelven objetos, las relaciones se acceden a través de propiedades y los borrados, actualizaciones y inserciones se realizan usando objetos y métodos. Los objetos POJO incluyen anotaciones que le indican a Hibernate cual es la información a persistir y las relaciones con otros POJO. Como hibernate dispone de esta información en base a ella puede recrear o actualizar las tablas y los campos necesarios según la definición de esas anotaciones. El ORM es encarga de traducir las acciones a las SQL entendidas por el sistema relacional, esto proporciona la ventaja adicional de que el ORM puede generar las sentencias SQL adaptadas a la base de datos utilizada. De esta forma se podría cambiar de una base de datos a otra sin realizar ningún cambio en la aplicación o con pocos cambios comparado con los necesarios usando JDBC. Con Hibernate se puede emplear un lenguaje de consulta similar a SQL pero adaptado al modelo orientado a objetos, el lenguaje es HQL.
  • JPA: es una especificación de Java que define una API común para los sistemas ORM. Con JPA podríamos cambiar de proveedor ORM sin realizar ningún cambio en la aplicación. JPA se ha basado en gran parte en Hibernate y su forma de trabajar es similar, el lenguaje HQL también es similar pero denominado JPQL.
A continuación explicaré como acceder a una base de datos relacional a través de JPA en una aplicación web usando el framework Apache Tapestry. Lo primero que deberemos hacer es crear el archivo persistence.xml donde indicaremos el driver JDBC según la base de datos que utilicemos y la URL de conexión a la base de datos entre otras opciones como el usuario y password de conexión. En el ejemplo he usado H2 como base de datos ya que puede embeberse en una aplicación sin necesidad de tener un sistema externo como ocurren en el caso de MySQL y PostgreSQL. De esta forma este ejemplo puede probarse sin necesidad de instalar previamente ninguna base de datos relacional.

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<persistence-unit name="h2">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="hibernate.connection.driver_class" value="org.h2.Driver"/>
<property name="hibernate.connection.url" value="jdbc:h2:mem:test"/>
<property name="hibernate.connection.username" value="sa"/>
<property name="hibernate.connection.password" value="sa"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<!-- Hibernate Search -->
<property name="hibernate.search.default.directory_provider" value="filesystem"/>
<property name="hibernate.search.default.indexBase" value="indexes"/>
</properties>
</persistence-unit>
</persistence>
view raw persistence.xml hosted with ❤ by GitHub
En la aplicación es necesario incluir unas pocas dependencias para acceder a la API JPA.

dependencies {
compile 'org.apache.tapestry:tapestry5-annotations:5.3.7'
compile 'org.apache.tapestry:tapestry-core:5.3.7'
compile 'org.apache.tapestry:tapestry-beanvalidator:5.3.7'
compile 'org.apache.tapestry:tapestry-jpa:5.3.7'
compile 'org.hibernate:hibernate-core:4.2.1.Final'
compile 'org.hibernate:hibernate-validator:5.0.1.Final'
compile 'org.hibernate:hibernate-entitymanager:4.2.1.Final'
compile 'com.h2database:h2:1.3.171'
providedCompile 'javax.servlet:servlet-api:2.5'
}
view raw build.gradle hosted with ❤ by GitHub
Una vez incluidas las dependencias debemos configurar Tapestry para que nos proporcione el soporte de acceso a una base de datos empleando JPA, definimos en el módulo de la aplicación y contenedor de dependencias, el servicio DAO y al mismo tiempo configuraremos la transaccionalidad mediante anotaciones para el DAO.

public static void bind(ServiceBinder binder) {
binder.bind(ValidationDecoratorFactory.class, AppValidationDecoratorFactory.class).withId("AppValidationDecoratorFactory");
binder.bind(ProductoDAO.class, ProductoDAOImpl.class);
}
public static void contributeBeanValidatorSource(OrderedConfiguration<BeanValidatorConfigurer> configuration) {
configuration.add("AppConfigurer", new BeanValidatorConfigurer() {
public void configure(javax.validation.Configuration<?> configuration) {
configuration.ignoreXmlConfiguration();
}
});
}
/**
* Los servicios con una interfaz *DAO (es necesario que sea una interfaz)
* soportan las anotaciones de transaccionalidad.
*/
@Match("*DAO")
public static void adviseTransactionally(JpaTransactionAdvisor advisor, MethodAdviceReceiver receiver) {
advisor.addTransactionCommitAdvice(receiver);
}
view raw AppModule.java hosted with ❤ by GitHub
Las clases con capacidad de persistencia han de ubicarse en un subpaquete del paquete de Tapestry. El paquete de Tapestry es aquel que está indicado en el parámetro de contexto tapestry.app-package en el archivo web.xml de la aplicación web. Si tapestry.app-package fuese es.com.blogspot.elblogdepicodev.tapestry.jpa el paquete de las entidades debería ser es.com.blogspot.elblogdepicodev.tapestry.jpa.entities. Esta es la convención y la forma preferida de hacerlo, si se quiere cambiar es posible hacerlo mediante configuración. La entidad de persistencia que usaré para el ejemplo es la siguiente, en la que se incluyen las anotaciones de JPA:

package es.com.blogspot.elblogdepicodev.tapestry.jpa.entities;
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 javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Length;
@Entity
public class Producto implements Serializable {
private static final long serialVersionUID = 4301591927955774037L;
@Id
@GeneratedValue
private Long id;
@NotNull
@Length(min = 3, max = 100)
@Column(name = "nombre", length = 100)
private String nombre;
@NotNull
@Length(min = 0, max = 5000)
@Column(name = "descripcion", length = 5000)
private String descripcion;
@NotNull
@Min(value = 0) @Max(value = 1000)
@Column(name = "cantidad")
private Long cantidad;
@NotNull
@Column(name = "fecha")
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
El código de acceso a base de datos suele ponerse en una clase denominada servicio y DAO (Data Access Object) que contiene todo ese código. Ya que las operaciones de acceso a base de datos son candidatas a ser reutilizadas desde varias páginas o componentes es recomendable hacerlo así, además de hacer que las páginas de Tapestry sean más pequeñas (ya tienen suficiente responsabilidad con hacer de controlador en el modelo MVC) permite que si un día cambiásemos de framework web solo tendríamos que modificar la capa de presentación. Todo el código de los servicios nos serviría perfectamente sin hacer ninguna modificación.

El contenedor de dependencias se encargará en el momento que necesite construir una instancia del servicio DAO inyectarle en el constructor los parámetros necesarios. En este caso una de las clases principales de la API JPA que es EntityManager. Una vez con la referencia a entityManager trabajaríamos usando sus métodos para realizar las consultas y operaciones que necesite proporcionar el DAO.

package es.com.blogspot.elblogdepicodev.tapestry.jpa.dao;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import es.com.blogspot.elblogdepicodev.tapestry.jpa.entities.Producto;
import es.com.blogspot.elblogdepicodev.tapestry.jpa.misc.Pagination;
public class ProductoDAOImpl implements ProductoDAO {
private EntityManager entityManager;
public ProductoDAOImpl(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public Producto findById(Long id) {
return entityManager.find(Producto.class, id);
}
@Override
public List<Producto> findAll(Pagination paginacion) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Producto> cq = cb.createQuery(Producto.class);
Root<Producto> root = cq.from(Producto.class);
cq.select(root);
if (paginacion != null) {
cq.orderBy(paginacion.getOrders(root, cb));
}
Query q = entityManager.createQuery(cq);
if (paginacion != null) {
q.setFirstResult(paginacion.getStart());
q.setMaxResults(paginacion.getEnd() - paginacion.getStart() + 1);
}
return q.getResultList();
}
@Override
public void persist(Producto producto) {
entityManager.persist(producto);
}
@Override
public void remove(Producto producto) {
entityManager.remove(producto);
}
@Override
public void removeAll() {
Query query = entityManager.createQuery("delete from Producto");
query.executeUpdate();
}
}
Hay métodos del DAO que finalmente lanzan sentencias de inserción, modificación y borrado, este DAO es muy sencillo pero si realizase varias operaciones contra la base de datos estas deberían cumplir las reglas ACID. Para cumplir las reglas ACID hay que utilizar transacciones, se puede conseguir de forma declarativa utilizando la anotación @CommitAfter. Esta anotación inicia una transacción al iniciarse el método y lanza un commit al finalizar el método. En caso de que se produzca una excepción se seguirá haciendo commit para las excepciones checked (de obligado tratamiento o lanzado) y únicamente un rollback para las excepciones unckecked (que extienden de RuntimeException y no es necesario capturarlas o lanzarlas).

Si quisiésemos hacer rollback en una excepción checked deberíamos hacerlo manualmente capturando la excepción y lanzando una unchecked aunque si es la opción que queremos por defecto podríamos implementar una anotación para tratar la transaccionalidad de esta manera y de forma declarativa en todos los casos.

Una vez que tenemos el servicio DAO basta con inyectarlo en la clase de una página o componente y hacer uso de él. En la siguiente página se muestra un listado de productos donde con un botón se pueden eliminar y con otro se pueden crear nuevos. En la clase de la página se hace uso del DAO para buscar las entidades, crear una nueva con un nombre y descripción aleatoria y eliminar un producto.

package es.com.blogspot.elblogdepicodev.tapestry.jpa.pages.admin;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.annotations.Cached;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.beaneditor.BeanModel;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.BeanModelSource;
import es.com.blogspot.elblogdepicodev.tapestry.jpa.dao.ProductoDAO;
import es.com.blogspot.elblogdepicodev.tapestry.jpa.entities.Producto;
public class EjemploJPA {
@Inject
@Symbol(SymbolConstants.TAPESTRY_VERSION)
@Property
private String tapestryVersion;
@Inject
private ProductoDAO dao;
@Inject
private BeanModelSource beanModelSource;
@Inject
private ComponentResources resources;
@Property
private Producto producto;
public BeanModel<Producto> getModel() {
BeanModel<Producto> model = beanModelSource.createDisplayModel(Producto.class, resources.getMessages());
// Columnas sin ordenación
for (String name :model.getPropertyNames()) {
model.get(name).sortable(false);
}
// Ocultar columna id
model.exclude("id");
// Añadir columna de acción
model.add("action", null).label("").sortable(false);
return model;
}
void onNuevo() {
Producto producto = new Producto(UUID.randomUUID().toString(), UUID.randomUUID().toString(), 1l, new Date());
dao.persist(producto);
}
void onEliminar(Long id) {
Producto producto = dao.findById(id);
dao.remove(producto);
}
@Cached
public List<Producto> getProductos() {
return dao.findAll(null);
}
}
view raw EjemploJPA.java hosted with ❤ by GitHub
<!DOCTYPE html>
<html t:type="layout"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
xmlns:p="tapestry:parameter">
Versión: <b>${tapestryVersion}</b><br/>
<t:holaMundo/><br/>
<h1>Lista de productos</h1>
<t:grid source="productos" row="producto" model="model" lean="true" inPlace="true" class="table table-bordered table-condensed">
<p:nombreCell>
${producto.nombre}
</p:nombreCell>
<p:actionCell>
<t:eventlink event="eliminar" context="producto.id" class="btn btn-danger">Eliminar</t:eventlink>
</p:actionCell>
<p:empty>
<p>No hay productos.</p>
</p:empty>
</t:grid>
<t:eventlink event="nuevo" class="btn btn-primary">Nuevo producto</t:eventlink>
</html>
view raw EjemploJPA.tml hosted with ❤ by GitHub
Esta es una captura de pantalla del ejemplo en ejecución:
Para casos más complejos de transacciones como transacciones anidadas, requeridas, soportables, ninguna u obligatoria podemos basarnos en lo comentado en esta entrada donde queda perfectamente explicado.

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 previamente (salvo java y git). 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/TapestryJPA
$ ./gradlew tomcatRun
# Abrir en el navegador http://localhost:8080/TapestrySecurity/ejemplojpa
view raw git.sh hosted with ❤ by GitHub
Referencia:
DAO genérico para JPA (ORM)
Documentación sobre Apache Tapestry
http://es.wikipedia.org/wiki/Java_Database_Connectivity
https://en.wikipedia.org/wiki/Relational_database
http://en.wikipedia.org/wiki/Cardinality_(data_modeling) http://en.wikipedia.org/wiki/ACIDhttp://en.wikipedia.org/wiki/SQL http://en.wikipedia.org/wiki/NoSQLhttp://en.wikipedia.org/wiki/Hibernate_(Java)