Mapeo de Procedimientos Almacenados con Hibernate

1
28660

Mapeo de Procedimientos Almacenados con Hibernate

 

Índice de contenidos.

1. Introducción

En este tutorial vamos a ver la forma de trabajar con Hibernate para que podamos llamar a los procedimientos almacenados de nuestra base de datos. En muchos desarrollos es común ver como tienen toda o parte de la lógica de negocio en la base de datos. En estos casos, debido a que no vamos a replicar esta lógica de negocio en el código fuente de la aplicación para utilizarlo desde la capa de negocio, podemos invocarlo mediante queries nativas y el resultado de las mismas a entidades para que sea más sencillo trabajar con los datos.

La forma de mapear las queries nativas está disponible tanto vía anotaciones como mediante el fichero de mapeo hbm.xml. En este tutorial usaremos anotaciones. Más info aquí.

Para la base de datos utilizaré PostgreSQL.

Para hacer las pruebas me basaré en el código fuente del siguiente tutorial.

El código fuente del ejemplo lo puedes descargar de aquí.

2. Entorno

  • MacBook Pro 15′ (2.4 GHz Intel Core i5, 4GB DDR3 SDRAM).
  • Sistema Operativo: Mac OS X Snow Leopard 10.6.4
  • JDK 1.6.0_20
  • Hibernate Core 3.3.2.GA
  • PostgreSQL 9.0.2
  • JUnit 4.7

3. Procedimiento Almacenado

Lo primero que haremos será crearnos un procedimiento almacenado en PostgreSQL para posteriormente mapearlo con Hibernate. La lógica del procedimiento es lo de menos, en este caso el procedimiento findByNumber se encarga de devolver un registro con los datos del futbolista al cual corresponde el número recibido por parámetro.

CREATE OR REPLACE FUNCTION findByNumber(integer) RETURNS record AS $body$
DECLARE
 numberToFind ALIAS FOR $1;
 footballer record;

BEGIN
		select id, name, lastname, number INTO footballer FROM footballer WHERE number = numberToFind;

 RETURN footballer;
END;
$body$ LANGUAGE plpgsql;

4. Mapeo de Hibernate

Una vez que tenemos el procedimiento almacenado en la base de datos vamos a mapearlo a través de una query nativa. Las queries nativas se utilizan para escribir en sql estándar (en lugar de HQL) una consulta que pedimos a Hibernate que lance. Mediante esta query nativa llamamos al procedimiento almacenado y su resultado se guardará en la entidad Footballer.

@Entity
@NamedNativeQuery(name = "Footballer.findByNumber", query = "select * from findByNumber(?) as (id int, name varchar, lastname varchar, number int);", resultClass = Footballer.class)
public class Footballer {

	@Id
	@GeneratedValue
	private Integer id;

	private String name;

	private String lastname;

	private int number;
	
	...
	
	// Se omiten los getters y setters

A través de la anotación @NamedNativeQuery indicamos la query nativa llamada Footballer.findByNumber. La query se especifica mediante el atributo query donde hacemos la llamada al procedimiento almacenado que definimos anteriormente. Debido a que lo que nos devuelve la base de datos es un registro debemos sacar los datos para posteriormente ser mapeados a través del ‘select … as’ indicando su nombre y tipo como se muestra en la consulta anterior. Por último, a través del parámetro resultClass indicamos la clase donde mapear los datos, en este caso la clase Footballer. No es necesario indicar los atributos de la clase si se llaman igual que los que devuelve la consulta que llama al procedimiento. Si fueran diferentes se lo debemos indicar a través de un resultSetMapping mapendo tanto la entidad destino de los datos como sus atributos. Este sería el ejemplo de ese caso:

@Entity
@NamedNativeQuery(name = "Footballer.findByNumber", query = "select * from findByNumber(?) as (identificador int, nombre varchar, apellidos varchar, numero int);", resultSetMapping = "mapping")
@SqlResultSetMapping(name = "mapping", entities = { 
	@EntityResult(entityClass = Footballer.class, fields = { 
		@FieldResult(name = "id", column = "identificador"), 
		@FieldResult(name = "name", column = "nombre"), 
		@FieldResult(name = "lastname", column = "apellidos"), 
		@FieldResult(name = "number", column = "numero") }) })

	@Id
	@GeneratedValue
	private Integer id;

	private String name;

	private String lastname;

	private int number;
	
	...
	
	// Se omiten los getters y setters

De esta manera le decimos a Hibernate cómo debe mapear los datos en los atributos de la entidad. Esta forma es menos recomendable que la primera ya que implica mayor configuración, recordar el patrón Convention over Configuration. Si decidimos utilizarla debemos indicar el resultSetMapping que indicará la forma de mapear los datos devueltos por la consulta. Para ello definimos un «mapping» mediante la anotación SqlResultSetMapping donde configuramos la entidad destino de los datos «Footballer.class» y los campos dentro de la entidad donde recoger los datos de la query a través de los FieldResult.

5. Llamada a la NamedNativeQuery

Por último ya únicamente falta la llamada al procedimiento almacenado, para ello nos creamos un método en nuestro Dao llamado «findByNamedQuery». Este método recibe el nombre de la namedQuery, parámetros para indicar el primer registro a sacar en la consulta y el máximo de registros y por último los filtros de la consulta.

	public <T> List<T> findByNamedQuery(String namedQuery, int firstResult, int maxResults, Object... values) {

		final Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
		final Query query = session.getNamedQuery(namedQuery);

		for (int i = 0; i < values.length; i++) {
			query.setParameter(i, values[i]);
		}

		if (firstResult > 0) {
			query.setFirstResult(firstResult);
		}

		if (maxResults > 0) {
			query.setMaxResults(maxResults);
		}

		return query.list();
	}

Para comprobar que todo está bien lo probamos mediante un test de JUnit. Antes de ejecutarse el test Spring realizará la llamada al método init del bean AddSampleData que proveerá de datos a la base de datos.

package com.autentia.tutoriales.dao.service.impl;

// imports

@Service
public class AddSampleData extends HibernateDaoSupport {

	private Dao dao;

	@Autowired
	public AddSampleData(Dao dao, SessionFactory factory) {
		super.setSessionFactory(factory);
		this.dao = dao;
	}

	@SuppressWarnings("unused")
	@PostConstruct
	private void init() {
		final List<Footballer> list = new ArrayList<Footballer>();

		final Footballer iker = new Footballer();
		iker.setName("Iker");
		iker.setLastname("Casillas");
		iker.setNumber(1);
		list.add(iker);

		final Footballer pepe = new Footballer();
		pepe.setName("Pepe");
		pepe.setLastname("Lima");
		pepe.setNumber(3);
		list.add(pepe);

		final Footballer sergio = new Footballer();
		sergio.setName("Sergio");
		sergio.setLastname("Ramos");
		sergio.setNumber(4);
		list.add(sergio);
		
		// ...
		
		dao.saveOrUpdate(list);
	}

El test de JUnit busca un futbolista por su número de camiseta llamando al procedimiento almacenado «Footballer.findByNumber» y comprueba que se recibe en el listado un futbolista que cumple los criterios de la búsqueda. Se le pasa al método findByNamedQuery los valores firstResult = -1 y maxResult= -1 ya que no son relevantes en esta consulta.

	package com.autentia.tutoriales;

	// imports
	
	@RunWith(SpringJUnit4ClassRunner.class)
	@ContextConfiguration(locations = { "classpath:applicationContext.xml" })
	public class NativeQueryTest {
	
		@Resource
		private Dao dao;
	
		private static final int IKER_CASILLAS_NUMBER = 1;
	
		@Test
		public void testFindByNumberByStoreProcedure() {
			final List<Footballer> footballerList = dao.findByNamedQuery("Footballer.findByNumber", -1, -1, IKER_CASILLAS_NUMBER);
	
			Assert.assertEquals("Debería haber un futbolista ", 1, footballerList.size());
	
			final Footballer footballer = footballerList.get(0);
	
			Assert.assertEquals("Iker", footballer.getName());
			Assert.assertEquals("Casillas", footballer.getLastname());
			Assert.assertEquals(IKER_CASILLAS_NUMBER, footballer.getNumber());
		}
	}

Para completar la prueba de concepto vamos a crear otro procedimiento almacenado que se encargará de sumar dos números y devolver el resultado de la suma. Lógicamente la funcionalidad del procedimiento es lo de menos, lo que nos interesa en este caso es el resultado de una operación que no formará parte de una entidad sino que será un escalar por lo que los mapeos son diferentes.

Lo primero será crear el procedimiento almacenado encargado de realizar la suma:

CREATE OR REPLACE FUNCTION suma(operando1 integer, operando2 integer) RETURNS int AS $body$
	DECLARE
	 operando1 ALIAS FOR $1;
	 operando2 ALIAS FOR $2;
	 resultado int;
	
	BEGIN
		resultado = operando1 + operando2;
	
	 RETURN resultado;
	END;
$body$ LANGUAGE plpgsql;

Creamos una nueva NamedNativeQuery en la entidad Footballer. Se mete en esta entidad por simplificar el ejemplo aunque no tenga nada que ver 😉

	@Entity
@NamedNativeQueries({
		@NamedNativeQuery(name = "Footballer.findByNumber", query = "select * from findByNumber(?) as (id int, name varchar, lastname varchar, number int);", resultClass = Footballer.class),
		@NamedNativeQuery(name = "Footballer.SumaDeEnteros", query = "select suma(?,?) as resultado", resultSetMapping = "scalar") })
@SqlResultSetMapping(name = "scalar", columns = @ColumnResult(name = "resultado"))
	public class Footballer {
		// ....
	}

Mediante la query nativa «Footballer.SumaDeEnteros» llamamos al procedimiento almacenado «suma» que recibe dos enteros y devuelve el resultado. Este resultado será un valor escalar y la forma de indicarle a Hibernate que trate este valor es mediante el SqlResultSetMapping donde se define el nombre que llevará el dato en el ResultSet de la consulta.

Para probar esta nueva funcionalidad añadimos un nuevo test de JUnit donde realizamos la llamada a la NativeNamedQuery pasándole los valores 2 y 3 y comprobamos el valor de la suma.

	@Test
	public void testSumaByStoreProcedure() {
		final List<Integer> list = dao.findByNamedQuery("Footballer.SumaDeEnteros", -1, -1, 2, 3);
		Assert.assertEquals(Integer.valueOf(5), list.get(0));
	}

6.Conclusiones

Muchas veces hemos escuchado que una aplicación o módulo funcional no se puede migrar a Hibernate (o Java) debido a que hay mucha lógica de negocio en procedimientos almacenados de la base de datos. Como hemos podido ver no es excusa tener estos procedimientos ya que Hibernate es capaz de llamar a esta lógica y mapear el resultado por lo que nos podemos beneficiar de las grandes ventajas que nos proporciona Hibernate para trabajar con el modelo de datos.

Espero que te haya servido de ayuda.

Un saludo. Juan.

1 COMENTARIO

  1. Hola, antes que nada quiero darte las gracias por compartir tus conocimientos con todos. Pero tengo una duda he mapeado las tablas con Anotaciones y Netbeans ha creado automáticamente algunas querys para cada clase persistida, mi pregunta es ¿tengo que declarar en algún lado esos nombres de querys? estoy combinando Hibernate+Spring. Había leido que tenía que declarar el nombre de las querys en el archivo configuración de Spring, pero no encuentro info de como hacerlo y tampoco de como obtendría esos nombres desde Spring (estoy usando getHibernateTemplate para acceder a las clases desde Spring).

    Si puedes orientarme te estaría muy agradecido nuevamente. Saludos¡

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad