Persistencia en BD con Spring Data JPA (I): Primeros pasos


VERSIONES

  1. 08/02/2014 (Primera publicación)
  2. 02/03/2014:
    • Añadido ejemplo de UPDATE
    • Actualización de librerias de Spring
  3. 01/11/2014:
    • Ampliación de la sección de consultas
    • Configuración de caché
    • División del artículo original en dos partes y creación de una tercera
    • Diagrama de clases

logo spring

Nota: Este tutorial es una continuación de Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache. El objetivo es reutilizar el mismo proyecto de ejemplo e incluir Spring Data JPA.

Spring Data es el nombre de un módulo de Spring que a su vez engloba un gran número de sub-módulos cuyo objetivo es facilitar el acceso y explotación de datos en aplicaciones basadas en Spring, obteniéndose estos datos de fuentes tan dispares como servicios RestFUL, bases de datos relacionales a través de JPA, o bases de datos NoSQL como MongoDB o Neo4J, entre otras. Este artículo expondrá el uso básico del módulo para JPA utilizándose como implementación Hibernate y se divide en tres partes:

  1. Primeros pasos
  2. Repositorios personalizados
  3. Auditoría

Nota:A partir de la versión 1.7.0.RELEASE Spring Data JPA requiere Spring 4. En el artículo se utilizará Spring 3 y Spring Data JPA 1.6.4.RELEASE ya que Spring 3 se sigue utilizando ampliamente incluso en proyectos de nueva creación.

Entorno de pruebas:

Requisitos: Conocimientos básicos de Maven, JPA y Spring IoC.

Proyecto para pruebas

Tal y como se ha comentado, se partirá del proyecto de ejemplo de un artículo anterior disponible aquí. Se trata de un proyecto Maven a partir del cual se puede generar un proyecto para Eclipse con Maven o bien desde el propio IDE con el plugin M2Eclipse.

$mvn clean eclipse:eclipse

Configurando Spring Data JPA

Para utilizar Spring Data JPA en el proyecto, en primer lugar hay que incluir las dependencias necesarias en el pom.xml que sólo será una. Puesto que spring-data-jpa tiene dependencias que ya están incluídas en el proyecto con otra versión, estas dependencias se van a excluir para evitar importaciones innecesarias y que puedan causar conflictos. Así pues, en el pom se añadirá lo siguiente:

<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-jpa</artifactId>
			<version>1.6.4.RELEASE</version>
			<exclusions>
				<exclusion>
					<artifactId>spring-orm</artifactId>
					<groupId>org.springframework</groupId>
				</exclusion>
				<exclusion>
					<artifactId>spring-tx</artifactId>
					<groupId>org.springframework</groupId>
				</exclusion>
				<exclusion>
					<artifactId>spring-beans</artifactId>
					<groupId>org.springframework</groupId>
				</exclusion>
				<exclusion>
					<artifactId>spring-aop</artifactId>
					<groupId>org.springframework</groupId>
				</exclusion>
				<exclusion>
					<artifactId>spring-core</artifactId>
					<groupId>org.springframework</groupId>
				</exclusion>
				<exclusion>
					<artifactId>spring-context</artifactId>
					<groupId>org.springframework</groupId>
				</exclusion>
			</exclusions>
		</dependency>

Opcionalmente, se incluye la dependencia slf4j-log4j para poder redireccionar los logs de Spring Data al sistema de logs de log4j. Puesto que esta dependencia ya incluye log4j no es necesario esta última.

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.7</version>
</dependency>

La configuración para el acceso a MySQL (o cualquier base de datos soportada por Hibernate) con JPA e Hibernate no cambia, pero será necesario definir en el applicationContext.xml la ruta de paquetes en la que se encuentran nuestros repositorios de Spring Data que reemplazarán a los clásicos DAOs. Vamos a crear un paquete com.danielme.demo.springdatajpa.repository . El applicationContext.xml quedará tal que así:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:jpa="http://www.springframework.org/schema/data/jpa"
	xsi:schemaLocation="http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans-3.2.xsd  
	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context-3.2.xsd  
	http://www.springframework.org/schema/tx  http://www.springframework.org/schema/tx/spring-tx-3.2.xsd  
	http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd">

	<jpa:repositories base-package="com.danielme.demo.springdatajpa.repository"/>
...

El primer repositorio

Ya estamos listos para utilizar Spring Data JPA. Para cada entidad JPA con la que queramos trabajar, en lugar del tradicional DAO, se creará un “repositorio” de Spring Data que no es más que una interfaz que especializa JpaRepository. Esta interfaz proporciona “de serie” las operaciones CRUD más habituales y que por lo tanto no tendremos que implementar. El siguiente diagrama de clases muestra los métodos que disponemos.

JPARepository Hierarchy

La convención a seguir para nombrar el repositorio/interfaz es utilizar el nombre de la entidad con el sufijo “Repository”. Asimismo, hay que indicar la entidad para la que se crea el repositorio y el tipo de su clave primaria.

package com.danielme.demo.springdatajpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.danielme.demo.springdatajpa.Country;

public interface CountryRepository extends JpaRepository<Country, Long>
{

}

El repositorio ya está listo para ser utilizado como cualquier bean de Spring. Más adelante tras añadir algunos métods al repositorio se utilizará en un test de JUnit.

Querys (SELECT) desde Spring Data JPA

Los métodos que el repositorio tiene “out-of-the-box” disminuyen el código de nuestros antiguos dao y permiten estandarizar el uso de repositorios en todos los proyectos en los que usemos Spring Data JPA, pero normalmente no serán suficientes. Podemos definir métodos que ejecuten consultas directamente en la intefaz del repositorio sin tener que codificar nada.
Esto puede hacerse de tres formas distintas:

  1. Usando una convención para el nombre del método. El método tendrá el prefijo findBy y contendrá el nombre de los atributos por los que se quiera filtrar unidos por los operadores And, Or, Between, LessThan, GreaterThan… (lista completa) Es la forma más simple de hacer una query pero no nos servirá si tenemos muchos parámetros (el nombre del método será muy largo) o si la consulta es más compleja.

    package com.danielme.demo.springdatajpa.repository;
    
    import java.util.List;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import com.danielme.demo.springdatajpa.model.Country;
    
    public interface CountryRepository extends JpaRepository<Country, Long
           
           Country findByName(String name);
           
           List<Country> findByPopulationGreaterThan(Integer population);			
    }
    
    

    Además de findBy se pueden usar el prefijo countBy para devolver el número de objetos que verifican los filtros definidos:

    package com.danielme.demo.springdatajpa.repository;
    
    import java.util.List;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import com.danielme.demo.springdatajpa.model.Country;
    
    public interface CountryRepository extends JpaRepository<Country, Long>
    {
           Country findByName(String name);
           
           List<Country> findByPopulationGreaterThan(Integer population);
           
           int countByPopulationGreaterThan(Integer population);		
    }
    
    
  2. Definiendo la query en JPQL. Se anota el método con @Query para definir consulta en JPQL. Los parámetros de esta consulta serán los parámetros que reciba el método.
    @Query("from Country c where lower(c.name) like lower(?1)")
     List<Country> getByNameWithQuery(String name);				
    

    En este ejemplo los parámetros que recibe el método son referenciados en la consulta según su número de orden pero también se puede utilizar un alias para los mismos gracias a la anotación @Param“:

    @Query("from Country c where lower(c.name) like lower(?1)")
    Country findByNameWithQuery(@Param("name") String name);
    
  3. Utilizar una NamedQuery ya definida. Vamos a añadir una NamedQuery a la entidad Country.
    @Entity
    @Table(name = "countries")
    @NamedQuery(name = "Country.getByPopulationNamedQuery", query = "FROM Country WHERE population > ?1")
    public class Country
    {
    

    El nombre del método que la ejecuta coincide con el de la NamedQuery:

    Country getByPopulationNamedQuery(Integer population);
    

    En cierto modo lo que hemos hecho es llevarnos la query del repositorio a la definición de la entidad, personalmente prefiero la segunda opción.

Los métodos que realizan consultas pueden devolver o un único objeto o una lista y en el primer caso si el resultado consta de más de un objeto se lanzará la excepción org.springframework.dao.IncorrectResultSizeDataAccessException.

La clase de los objetos obtenidos debe correspondese con la respuesta esperada de la consulta JPQL y no tiene que ser necesariamente la clase de la entidad a la que corresponda el repositorio. Por ejemplo, añadamos al repositorio un método que comprueba si un país existe dado su nombre exacto utilizando una consulta JPQL que devuelve un valor lógico:

@Query("select case when (count(c) > 0)  then true else false end from Country c where c.name = ?1)")
boolean exists(String name);	

Paginación y ordenación

En numerosas ocasiones será necesario paginar y/o ordenar los datos que obtengamos. Para ello hay que añadir a los métodos de consulta un último parámetro de tipo Pageable o Sort según proceda (los objetos Pageable incluyen un Sort). Por ejemplo, permitamos criterios de ordenación para el método findByNameWithQuery:

Country findByNameWithQuery(Integer population, Sort sort); 

Este método lo usaríamos de la siguiente manera:

countryRepository.findByNameWithQuery("%i%", new Sort( new Sort.Order(Sort.Direction.ASC,"name")))

En las consultas definidas mediante el nombre del método los criterios de ordenación también pueden ser definidos, de forma fija, en el nombre del método:

List<Country> findByPopulationGreaterThanOrderByPopulationAsc(Integer population);

En el caso de la paginación procedemos de forma análoga pasando al método un objeto de tipo Pageable. El método puede seguir devolviendo una lista, pero resulta más conveniente retornar un Page ya que este objeto, además de las entidades encontradas, incluye información detallada sobre la página obtenida (número de página, elementos totales existentes, etc).

@Query("from Country c where lower(c.name) like lower(?1)")
Page<Country> findByNameWithQuery(String name, Pageable page);

Cuando queramos paginar los resultados de la consulta contruiremos un objeto Pageable con PageRequest indicando el número de página solicitada (la primera es 0) y el tamaño de las página. Opcionalmente también se pueden indicar los criterios de ordenación:

 Page<Country> page = countryRepository.findByNameWithQuery("%i%", new PageRequest(0, 3, new Sort( new Sort.Order(Sort.Direction.ASC,"name"))));

Querys de actualización

Con la anotación @Query también se pueden definir sentencias JPQL de modificación de datos pero hay que tener en cuenta las siguientes consideraciones (su incumplimiento es fácil de detectar ya que se lanzarán excepciones):

  1. El método debe estar anotado con @Modifying si no Spring Data JPA interpretará que se trata de una select y la ejecutará como tal.
  2. Se devolverá o void o un entero (int\Integer) que contendrá el número de objetos modificados o eliminados.
  3. El método deberá ser transaccional o bien ser invocado desde otro que sí lo sea.

Aplicando los puntos anteriores, vamos a añadir un método que actualice las fechas de todos los países según una dada.

@Transactional
@Modifying
@Query("UPDATE Country set creation = (?1)"
 int updateCreation(Calendar creation);			

Las actualizciones de tipo DELETE se pueden definir mediante el nombre del método, por ejemplo

@Transactional
int deleteByName(String name);

Consultas con JPA Criteria

Veamos cómo funciona el soporte de Spring Data JPA para la ejecución de consultas definidas de forma programática con la API Criteria introducida en JPA 2.0 (Spring Data también se integra con el framework Querydsl). Para tener est funcionalidad en un repositorio siplemente hay que heredar de la clase JpaSpecificationExecutor.

public interface CountryRepository extends JpaRepository<Country, Long>, JpaSpecificationExecutor<Country>

Esta interfaz añade al repositorio algunos métodos que permiten ejecutar las consultas encapsuladas en un objeto Specification. Siguiendo lo indicado en el blog oficial de Spring, vamos a crear una clase abstracta con métodos estáticos cada uno de los cuales generará una Specification por ejemplo para buscar un país por su nombre:

package com.danielme.demo.springdatajpa.repository.specifications;


import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.data.jpa.domain.Specification;

import com.danielme.demo.springdatajpa.Country;

public abstract class CountrySpecifications
{
	public static Specification<Country> searchByName(final String name) {
		 return new Specification<Country>() {

			@Override
			public Predicate toPredicate(Root<Country> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder)
			{
				return criteriaBuilder.equal(root.get("name"), name);
			}
		};
	  }
}

Asumiendo que la búsqueda sólo va a devolver un resultado, se realizará de la siguiente forma.

 Country country = countryRepository.findOne(CountrySpecifications.searchByName("Mexico"));

Integración con la caché de Hibernate

En el artículo Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache vimos cómo utilizar la caché de Hibernate de segundo nivel para almacenar los resultados de las consultas y así poder mejorar el rendimiento de la capa de persistencia. Esta funcionalidad también puede ser utilizada en los repositorios de Spring Data JPA, si en el DAO de ejemplo usábamos la siguiente sentencia:

query.setHint(QueryHints.HINT_CACHEABLE, true);

Ahora en Spring Data JPA podemos conseguir lo mismo con la anotación @QueryHints que permite aplicar a un método del repositorio los QueryHint que sean necesarios. Por ejemplo,

package com.danielme.demo.springdatajpa.repository;

import java.util.Calendar;
import java.util.List;

import javax.persistence.QueryHint;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.transaction.annotation.Transactional;

import com.danielme.demo.springdatajpa.model.Country;


public interface CountryRepository extends JpaRepository<Country, Long>, JpaSpecificationExecutor<Country>
{
        Country findByName(String name);

        @QueryHints(value = { @QueryHint (name = "org.hibernate.cacheable", value = "true")})
        List<Country> findByPopulationGreaterThan(Integer population);

        int countByPopulationGreaterThan(Integer population);

        @Query("from Country c where lower(c.name) like lower(?1)")
        Page<Country> getByNameWithQuery(String name, Pageable page);

        Country getByPopulationNamedQuery(Integer population);

        List<Country> findByPopulationGreaterThanOrderByPopulationAsc(Integer population);

        @Query("select case when (count(c) > 0)  then true else false end from Country c where c.name = ?1)")
        boolean exists(String name);

        @Transactional
        @Modifying
        @Query("UPDATE Country set creation = (?1)")
        int updateCreation(Calendar creation);		
}

Opcionalmente se puede definir para @QueryHints el parámetro booleano forCounting para deshabilitar el hint en el caso de que se aplique paginación.

QueryHints también se puede aplicar a los métodos definidos por JpaRepository simplemente sobreescribiéndolos.

Probando el repositorio

En la siguiente clase se han implementado varios tests que permiten comprobar el funcionamiento del repositorio de ejemplo.

package com.danielme.demo.springdatajpa.test;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.Calendar;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import com.danielme.demo.springdatajpa.model.Country;
import com.danielme.demo.springdatajpa.repository.CountryRepository;
import com.danielme.demo.springdatajpa.repository.specifications.CountrySpecifications;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/resources/applicationContext.xml")
public class CountryRepositoryTest
{

	@Autowired
	private CountryRepository countryRepository;

	private static boolean springInit = false;

	@Before
	@Transactional
	public void setUp() throws Exception
	{
		if (!springInit)
		{
			// empty repository
			countryRepository.deleteAllInBatch();

			// insert
			countryRepository.save(new Country("Spain", 47265321));
			countryRepository.save(new Country("Mexico", 115296767));
			countryRepository.save(new Country("Germany", 81799600));
			countryRepository.save(new Country("Finland", 5470820));
			countryRepository.save(new Country("Colombia", 47846160));
			countryRepository.save(new Country("Costa Rica", 4586353));
			countryRepository.save(new Country("Norway", 5136700));
			springInit = true;
		}
	}

	@Test
	public void testSimpleQuerys()
	{
		assertTrue(countryRepository.findByName("Germany").getName().equals("Germany"));
		assertNull(countryRepository.findByName("France"));
		assertTrue(countryRepository.countByPopulationGreaterThan(45000000) == 4);
		assertTrue(countryRepository.findByPopulationGreaterThan(100000000).get(0).getName().equals("Mexico"));
		assertTrue(countryRepository.exists("Spain"));
		assertTrue(countryRepository.getByPopulationNamedQuery(5470820).getName().equals("Finland"));
		assertFalse(countryRepository.exists("Italy"));

	}

	@Test
	public void testQuerysSortingAndPaging()
	{
		List<Country> countries = countryRepository.findByPopulationGreaterThanOrderByPopulationAsc(45000000);
		assertTrue(countries.size() == 4);
		assertTrue(countries.get(0).getName().equals("Spain"));
		assertTrue(countries.get(3).getName().equals("Mexico"));

		Page<Country> page0 = countryRepository.getByNameWithQuery("%i%", new PageRequest(0, 4, new Sort(
				new Sort.Order(Sort.Direction.ASC, "name"))));
		assertTrue(page0.getTotalElements() == 5);
		assertTrue(page0.getTotalPages() == 2);
		assertTrue(page0.isFirst());
		assertTrue(page0.getContent().get(0).getName().equals("Colombia"));

	}

	@Test
	public void testModifyingQuerys() throws Exception
	{
		Calendar creation = countryRepository.findByName("Norway").getCreation();
		Thread.sleep(2000);
		assertTrue(countryRepository.updateCreation(Calendar.getInstance()) == 7);
		assertTrue(countryRepository.findByName("Norway").getCreation().after(creation));
                assertTrue(countryRepository.deleteByName("%") == 0);
		assertTrue(countryRepository.deleteByName("Norway") == 1);
	}

	@Test
	public void testJpaCriteria()
	{
		assertTrue(countryRepository.findOne(CountrySpecifications.searchByName("Mexico")).getName().equals("Mexico"));
	}
}


Los tests se pueden ejecutar desde Eclipse con la opción del menú contextual para la clase “Run As -> JUnit Test” o bien con el comando mvn test .

junit spring data jpa

Segunda parte

Código de ejemplo

El proyecto completo se encuentra disponible en GitHub (para más información sobre cómo utilizar GitHub, consultar este artículo). Este proyecto abarca las tres partes del artículo por lo que hay pequeñas diferencias entre el código expuesto en esta primera parte y el proyecto final.

One Response to Persistencia en BD con Spring Data JPA (I): Primeros pasos

  1. Alexander Iglesa dice:

    gracias por tomarte el tiempo para explicar este tipo de cosas, es de bastante ayuda encontrar este tipo de documentación cuando se está iniciando en Spring

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: