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

Última actualización: 06/10/2018

logo spring

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 siguiendo una praxis común. Los datos que se explotan pueden provenir de orígenes tan dispares como bases de datos relacionales, a través de JPA o el novedoso Spring Data JDBC, bases de datos NoSQL como MongoDB o Neo4J, o directorios LDAP. El presente tutorial 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 sobre versiones

La primera versión de este tutorial data de 2014 y utilizaba Spring 3 y Java 6. Necesitaba una revisión a fondo, así que lo he actualizado a la última versión estable de Spring Data JPA, con Spring 5 y Java 8, y que tiene algunas incompatibilidades con el código del tutorial original, además de nuevas funcionalidades. El proyecto del tutorial original se encuentra en un tag del repositorio git por si el lector tuviera que recurrir a versiones muy antiguas de Spring Data JPA.

Como nota informativa, indico que:

  • Hasta Spring Data JPA 1.6.4.RELEASE se requiere Spring 3 \Java 6.
  • Spring Data JPA 1.7.0.RELEASE requiere Spring 4\ Java7
  • A partir de la versión 2 ya se utiliza Spring 5 y Java 8.

Proyecto para pruebas

Actualmente para la creación de un nuevo proyecto basado en el ecosistema Spring tenemos dos grandes alternativas:

  • Utilizar Spring Boot (opción recomendada).
  • Crear manualmente o con plantillas el proyecto, esto es, sin Spring Boot.

Aunque recomiendo la primera opción, en este tutorial voy a crear el proyecto desde cero para demostrar cómo se configuran todos los elementos necesarios para utilizar Spring Data JPA, algo totalmente automático en Spring Boot y que explico en Introducción a Spring Boot: Aplicación Web con Spring Data JPA.

Vamos a parir de un proyecto estándar Maven 3, por lo que en primer lugar procedemos a definir las dependencias que necesitamos en el pom.xml.

  1. Spring Data JPA. Utilizaremos la última versión estable en el momento de escribir estas líneas (2.1.0.RELEASE) y que ya incluye como dependencias los módulos del core de Spring requeridos en la versión 5.1.0.RELEASE. Si necesitamos usar una versión distinta de Spring, definimos estas dependencias explícitamente y las excluimos de Spring Data JPA por ejemplo así:
    <dependency>
                <groupId>org.springframework.data</groupId>
                <artifactId>spring-data-jpa</artifactId>
                <version>${spring.data.jpa.version}</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>
    
  2. Hibernate core y hibernate-jpa-2.1-api.
  3. En el tutorial veremos cómo utilizar la caché de segundo nivel de Hibernate, por lo que se incluye el módulo de Hibernate correspondiente (hibernate-ehcache).
  4. El driver jdbc para la base de datos a utilizar, en nuestro caso MySQL.
  5. Un pool de conexiones JDBC como c3p0.
  6. Suelo utilizar log4j, así que incluyo el “bridge” de slf4j correspondiente para redireccionar a log4j los logs de Spring.
  7. Las dependencias para hacer testing con Spring y JUnit 4 tal y como se describe en el tutorial Testing Spring con JUnit 4.
  8. Cursos aplicaciones móviles

    El pom inicial, y que más adelante se modificará según vayamos viendo nuevas funcionalidades, queda tal que así:

    <project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0  http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.danielme.demo</groupId>
        <artifactId>spring-data-jpa-demo</artifactId>
        <version>2.0</version>
        <name>spring-data-jpa-demo</name>
        <inceptionYear>2014</inceptionYear>
    
        <description>Spring/Spring Data JPA + JPA 2 + Hibernate 5 + c3p0 + EHCache + log4j Integration Demo</description>
    
        <properties>
    
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <java.version>1.8</java.version>
    
            <hibernate.version>5.2.17.Final</hibernate.version>
            <c3p0.version>0.9.1.2</c3p0.version>
            <slf4jlog4j.version>1.7.7</slf4jlog4j.version>
            <mysql.version>5.1.33</mysql.version>
            <spring.data.jpa.version>2.1.0.RELEASE</spring.data.jpa.version>
            <spring.version>5.1.0.RELEASE</spring.version>
            <junit.version>4.12</junit.version>
    
        </properties>
    
        <developers>
            <developer>
                <id>danielme.com</id>
                <name>Daniel Medina</name>
                <email>danielme_com@yahoo.com</email>
                <url>http://danielme.com</url>
                <roles>
                    <role>developer</role>
                </roles>
            </developer>
        </developers>
    
        <scm>
            <url>https://github.com/danielme-com/Spring-Data-JPA-Demo.git</url>
        </scm>
    
        <licenses>
            <license>
                <name>GPL 3</name>
                <url>http://www.gnu.org/licenses/gpl-3.0.html</url>
            </license>
        </licenses>
    
    
        <build>
            <pluginManagement>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <configuration>
                            <source>${java.version}</source>
                            <target>${java.version}</target>
                            <encoding>${project.build.sourceEncoding}</encoding>
                        </configuration>
                    </plugin>
    
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-enforcer-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>enforce-java</id>
                                <goals>
                                    <goal>enforce</goal>
                                </goals>
                                <configuration>
                                    <rules>
                                        <requireJavaVersion>
                                            <version>${java.version}</version>
                                        </requireJavaVersion>
                                    </rules>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
    
                </plugins>
            </pluginManagement>
        </build>
    
        <dependencies>
    
            <dependency>
                <groupId>org.springframework.data</groupId>
                <artifactId>spring-data-jpa</artifactId>
                <version>${spring.data.jpa.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-core</artifactId>
                <version>${hibernate.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.hibernate.javax.persistence</groupId>
                <artifactId>hibernate-jpa-2.1-api</artifactId>
                <version>1.0.2.Final</version>
            </dependency>
    
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-ehcache</artifactId>
                <version>${hibernate.version}</version>
            </dependency>
    
            <dependency>
                <groupId>c3p0</groupId>
                <artifactId>c3p0</artifactId>
                <version>${c3p0.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
                <version>${slf4jlog4j.version}</version>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
    
            <!-- testing -->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>${spring.version}</version>
                <scope>test</scope>
            </dependency>
    
        </dependencies>
    
    </project>
    
    

    Ahora vamos a configurar Spring de las formas posibles: los clásicos XML que ya están casi en desuso, y en clases Java. Esta configuración es exactamente la misma que la del tutorial Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache por lo que no voy a explayarme al hablar de ella.

    Se configura el EntityManager de JPA para Hibernate a partir de la fuente de datos (un pool de conexiones a un MySQL), y su correspondiente gestor de transacciones. Los parámetros de configuración se externalizan en el fichero db.properties. Para Spring Data JPA la única configuración a realizar es indicar la ubicación de los repositorios.

    <?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-4.0.xsd
        http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/tx  http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
        http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.8.xsd">
    
        <jpa:repositories
            base-package="com.danielme.demo.springdatajpa.repository" />
    
        <!-- Autowired -->
        <!-- used to activate annotations in beans already registered in the 
            application context (no matter if they were defined with XML or by package 
            scanning) -->
        <context:annotation-config />
        <!-- scans packages to find and register beans within the application 
            context. -->
        <context:component-scan
            base-package="com.danielme.demo.springdatajpa" />
    
        <!-- enable @Transactional Annotation -->
        <tx:annotation-driven
            transaction-manager="transactionManager" />
    
        <!-- @PersistenceUnit annotation -->
        <bean
            class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
    
        <!-- classpath*:*.properties -->
        <context:property-placeholder
            location="classpath:db.properties" />
    
        <!-- data source with c3p0 -->
        <bean id="dataSource"
            class="com.mchange.v2.c3p0.ComboPooledDataSource"
            destroy-method="close" p:driverClass="${jdbc.driverClassName}"
            p:jdbcUrl="${jdbc.url}" p:user="${jdbc.username}"
            p:password="${jdbc.password}"
            p:acquireIncrement="${c3p0.acquire_increment}"
            p:minPoolSize="${c3p0.min_size}"
            p:maxPoolSize="${c3p0.max_size}"
            p:maxIdleTime="${c3p0.max_idle_time}"
            p:unreturnedConnectionTimeout="${c3p0.unreturned_connection_timeout}" />
    
    
        <!-- Hibernate as JPA vendor -->
        <bean id="jpaAdapter"
            class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
    
        <bean id="entityManagerFactory"
            class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
            p:dataSource-ref="dataSource"
            p:packagesToScan="com.danielme.demo.springdatajpa.model"
            p:jpaVendorAdapter-ref="jpaAdapter">
    
            <property name="jpaProperties">
                <props>
                    <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
                    <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                    <prop key="hibernate.connection.autocommit">${hibernate.connection.autocommit}</prop>
                    <prop key="hibernate.cache.use_second_level_cache">${hibernate.cache}</prop>
                    <prop key="hibernate.cache.use_query_cache">${hibernate.query.cache}</prop>
                    <prop key="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider
                    </prop>
                    <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory
                    </prop>
                    <!--useful for debugging -->
                    <prop key="hibernate.generate_statistics">${hibernate.statistics}</prop>
                </props>
            </property>
    
            <property name="loadTimeWeaver">
                <bean
                    class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
            </property>
    
        </bean>
    
        <bean id="transactionManager"
            class="org.springframework.orm.jpa.JpaTransactionManager"
            p:entityManagerFactory-ref="entityManagerFactory" />
    
    </beans>
    

    La misma configuración en forma programática sería así:

    package com.danielme.demo.springdatajpa;
    
    import java.beans.PropertyVetoException;
    import java.util.Properties;
    
    import javax.persistence.EntityManagerFactory;
    import javax.sql.DataSource;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.core.env.Environment;
    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    import org.springframework.orm.jpa.JpaTransactionManager;
    import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
    import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import com.mchange.v2.c3p0.ComboPooledDataSource;
    
    @Configuration
    @PropertySource("classpath:db.properties")
    @ComponentScan("com.danielme")
    @EnableTransactionManagement
    @EnableJpaRepositories(basePackages="com.danielme.demo.springdatajpa.repository")
    public class ApplicationContext {
    
        @Bean(destroyMethod = "close")
        DataSource dataSource(Environment env) {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            try {
                ds.setDriverClass(env.getRequiredProperty("jdbc.driverClassName"));
            } catch (IllegalStateException | PropertyVetoException ex) {
                throw new RuntimeException(
                        "error while setting the driver class name in the datasource", ex);
            }
            ds.setJdbcUrl(env.getRequiredProperty("jdbc.url"));
            ds.setUser(env.getRequiredProperty("jdbc.username"));
            ds.setPassword(env.getRequiredProperty("jdbc.password"));
            ds.setAcquireIncrement(env.getRequiredProperty("c3p0.acquire_increment", Integer.class));
            ds.setMinPoolSize(env.getRequiredProperty("c3p0.min_size", Integer.class));
            ds.setMaxPoolSize(env.getRequiredProperty("c3p0.max_size", Integer.class));
            ds.setMaxIdleTime(env.getRequiredProperty("c3p0.max_idle_time", Integer.class));
            ds.setUnreturnedConnectionTimeout(
                    env.getRequiredProperty("c3p0.unreturned_connection_timeout", Integer.class));
    
            return ds;
        }
    
        @Bean
        public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
                Environment env) {
            LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
            // entityManager.setPersistenceXmlLocation("classpath*:META-INF/persistence.xml");
            // entityManager.setPersistenceUnitName("hibernatePersistenceUnit");
            entityManager.setPackagesToScan("com.danielme.demo.springdatajpa.model");
            entityManager.setDataSource(dataSource);
            entityManager.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    
            Properties jpaProperties = new Properties();
            jpaProperties.put("hibernate.hbm2ddl.auto",
                    env.getRequiredProperty("hibernate.hbm2ddl.auto"));
            jpaProperties.put("hibernate.dialect", env.getRequiredProperty("hibernate.dialect"));
            jpaProperties.put("hibernate.connection.autocommit",
                    env.getRequiredProperty("hibernate.connection.autocommit"));
            jpaProperties.put("hibernate.cache.use_second_level_cache",
                    env.getRequiredProperty("hibernate.cache"));
            jpaProperties.put("hibernate.cache.use_query_cache",
                    env.getRequiredProperty("hibernate.query.cache"));
            jpaProperties.put("hibernate.cache.provider_class", "org.hibernate.cache.EhCacheProvider");
            jpaProperties.put("hibernate.cache.region.factory_class",
                    "org.hibernate.cache.ehcache.EhCacheRegionFactory");
            jpaProperties.put("hibernate.generate_statistics",
                    env.getRequiredProperty("hibernate.statistics"));
            jpaProperties.put("hibernate.show_sql", env.getRequiredProperty("hibernate.show_sql"));
            jpaProperties.put("hibernate.format_sql", env.getRequiredProperty("hibernate.format_sql"));
    
            entityManager.setJpaProperties(jpaProperties);
    
            return entityManager;
        }
    
        @Bean
        JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
            JpaTransactionManager transactionManager = new JpaTransactionManager();
            transactionManager.setEntityManagerFactory(entityManagerFactory);
            return transactionManager;
        }
    }
    
    

    Esta es la entidad con la que vamos a trabajar.

    package com.danielme.demo.springdatajpa.model;
    
    import java.util.Calendar;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    
    import javax.persistence.PrePersist;
    import javax.persistence.Table;
    import javax.persistence.Temporal;
    import javax.persistence.TemporalType;
    
    @org.hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE)
    @Entity
    @Table(name = "countries")
    public class Country {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
    
        @Column(nullable = false, unique = true)
        private String name;
    
        @Column(nullable = false)
        private Integer population;
    
        @Column(updatable = false, nullable = false)
        @Temporal(TemporalType.TIMESTAMP)
        private Calendar creation;
    
        public Country() {
            super();
        }
    
        public Country(String name, Integer population) {
            super();
            this.name = name;
            this.population = population;
        }
    
        @PrePersist
        public void onPersist() {
            creation = Calendar.getInstance();
        }
    
    ...getters y setters
      
    
    }
    

    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 la jerarquía.

    spring_data_jpa_repository_hierachy

    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.

    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. Una de las características más potentes de Spring Data JPA es la posibilidad de definir en la interfaz del repositorio métodos que ejecuten consultas directamente 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>{
             
             Optional<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>
      {
             Optional<Country> findByName(String name);
             
             List<Country> findByPopulationGreaterThan(Integer population);
             
             int countByPopulationGreaterThan(Integer population);        
      }
      
      

      Nota: en las versiones de Spring Data JPA que requieren Java 8 los métodos que devuelven un único objeto pueden retornar Optional (buena práctica muy recomendable), en todas las versiones de Spring Data JPA se puede devolver directamente la entidad, por ejemplo:

       Country findByName(String name);
      
    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(:name)")
      List<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.byPopulationNamedQuery", query = "FROM Country WHERE population = ?1")
      public class Country
      {
      

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

      Optional<Country> byPopulationNamedQuery(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 primero opción (no me gusta utilizar named query).

    Los métodos que realizan consultas pueden devolver o un único objeto o una lista (en este caso se puede utilizar también Collection o Stream de Java 8). 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 corresponderse 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);
    

    Si el resultado de la consulta queremos mapearlo a una clase que no sea una entidad podemos utilizar el mecanismo de proyección estándar de JPQL en nuestros repositorios. Por ejemplo, creamos la siguiente clase.

     
    package com.danielme.demo.springdatajpa.model;
    
    public class Pair {
    
        private Long id;
        private String value;
    
        public Pair(Long id, String value) {
            super();
            this.id = id;
            this.value = value;
        }
    
        public Long getId() {
            return id;
        }
    
        public String getValue() {
            return value;
        }
    
    }
    

    Ahora queremos hacer una consulta a base de datos para obtener una instancia de Pair correspondiente a los datos de un Country dado por su id de tal modo que el Pair contenga ese id y en value el nombre del país. El mapeo entre los resultados de la consulta y los atributos de la clase Pair lo hacemos mediante el constructor.

    @Query("select new com.danielme.demo.springdatajpa.model.Pair(c.id, c.name) from Country c where c.id = ?1")
    Pair getPairById(Long id);
    

    También podemos utilizar el mecanismo de tuplas de JPA 2.0 con Spring Data JPA

    @Query("select c.id as ID, c.name As value from Country c where c.id = ?1")
    Tuple getTupleById(Long id);
    
    tuple.get("ID");
    tuple.get("value");
    

    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:

    @Query("from Country c where lower(c.name) like lower(:name)")
    List<Country> findByNameWithQuery(@Param("name") String name, Sort sort);
    

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

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

    …pero los constructores de Sort han sido marcados como deprecated y en su lugar se deben utilizar los métodos estáticos by:

    countryRepository.findByNameWithQuery("%i%", Sort.by(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 habitualmente será 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). Otra opción es devolver un Slice (disponible a partir de la versión 1.8), y que a diferencia de Page sólo indica si hay más páginas por obtener.

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

    Para invocar este método hay que crear un objeto Pageable con PageRequest indicando el número de página solicitada (la primera es 0) y el tamaño de las páginas. Opcionalmente también se pueden indicar los criterios de ordenación:

    Page<Country> page0 = countryRepository.findByNameWithQuery("%i%",
                    new PageRequest(0, 4, Sort.by(Sort.Direction.ASC,"name")));
    

    Los constructores de PageRequest están marcados como deprecated y en su lugar debemos utilizar los métodos estáticos of.

    Page<Country> page0 = countryRepository.findByNameWithQuery("%i%",
                    PageRequest.of(0, 4, Sort.by(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 registros afectados.
    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 actualizaciones de tipo DELETE se pueden definir directamente 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 esta funcionalidad en un repositorio simplemente 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);
    

    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,

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

    QueryHints también se puede aplicar a los métodos heredados de JpaRepository simplemente sobrescribiéndolos.

    Probando el repositorio

    En la siguiente clase se han implementado varios tests que permiten comprobar el funcionamiento del repositorio de ejemplo. Para más información sobre testing, consultar los tutoriales Testing con JUnit 4 y Spring: testing con JUnit 4.

    Para poderlos ejecutar es necesario configurar la conexión a la base de datos MySQL en el fichero db.properties. Los tests se basan en el juego de datos de prueba definidos en el script test.sql el cual se ejecuta antes de cada test.

    package com.danielme.demo.springdatajpa.test;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertFalse;
    
    import static org.junit.Assert.assertTrue;
    
    import java.util.Calendar;
    
    import java.util.Optional;
    
    import javax.persistence.Tuple;
    
    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.jdbc.Sql;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import com.danielme.demo.springdatajpa.ApplicationContext;
    import com.danielme.demo.springdatajpa.model.Country;
    import com.danielme.demo.springdatajpa.model.Pair;
    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")
    @ContextConfiguration(classes = { ApplicationContext.class })
    @Sql(scripts = { "/test.sql" })
    public class CountryRepositoryTest {
    
        private static final Long SPAIN_ID = 2L;
    
        @Autowired
        private CountryRepository countryRepository;
    
        @Test
        public void testExists() {
            assertTrue(countryRepository.exists("Spain"));
        }
    
        @Test
        public void testNotExists() {
            assertFalse(countryRepository.exists("Italy"));
        }
    
        @Test
        public void testPopulation() {
            assertEquals(3, countryRepository.countByPopulationGreaterThan(45000000));
            assertEquals(3, countryRepository.findByPopulationGreaterThan(45000000).size());
        }
    
        @Test
        public void testName() {
            Optional<Country> opt = countryRepository.findByName("Norway");
            assertTrue(opt.isPresent());
            assertEquals("Norway", opt.get().getName());
        }
    
        @Test
        public void testNoName() {
            assertFalse(countryRepository.findByName("France").isPresent());
        }
    
        @Test
        public void testNamedQuery() {
            assertTrue(countryRepository.byPopulationNamedQuery(115296767).isPresent());
        }
    
        @Test
        public void testQuerysSortingAndPaging() {
            Page<Country> page0 = countryRepository.findByNameWithQuery("%i%",
                    PageRequest.of(0, 2, Sort.by(Sort.Direction.ASC, "name")));
            assertTrue(page0.getTotalElements() == 4);
            assertTrue(page0.getTotalPages() == 2);
            assertTrue(page0.getContent().get(0).getName().equals("Colombia"));
        }
    
        @Test
        public void testUpdate() throws Exception {
            Calendar creation = countryRepository.findByName("Norway").get().getCreation();
            assertTrue(countryRepository.updateCreation(Calendar.getInstance()) == 5);
            assertTrue(countryRepository.findByName("Norway").get().getCreation().after(creation));
        }
    
        @Test
        public void testDelete() throws Exception {
            assertTrue(countryRepository.deleteByName("Norway") == 1);
        }
    
        @Test
        public void testJpaCriteria() {
            assertTrue(countryRepository.findOne(CountrySpecifications.searchByName("Mexico")).get()
                    .getName().equals("Mexico"));
        }
    
        @Test
        public void testProjectionConstructor() {
            Pair pair = countryRepository.getPairById(SPAIN_ID);
            assertEquals(SPAIN_ID, pair.getId());
            assertEquals("Spain", pair.getValue());
        }
    
        @Test
        public void testProjectionTuple() {
            Tuple tuple = countryRepository.getTupleById(SPAIN_ID);
            assertEquals(SPAIN_ID, tuple.get("ID"));
            assertEquals("Spain", tuple.get("value"));
        }
    
    }
    
    
    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.

    Master Pyhton, Java, Scala or Ruby

4 comentarios sobre “Persistencia en BD con Spring Data JPA (I): Primeros pasos

  1. 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

  2. @Query(“from Country c where lower(c.name) like lower(?1)”)
    Country findByNameWithQuery(@Param(“name”) String name);

    donde se esta usando el param name ?
    que no tendría que ser así

    @Query(“from Country c where lower(c.name) like lower(:name)”)

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 )

Google+ photo

Estás comentando usando tu cuenta de Google+. 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 )

Conectando a %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.