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

Última actualización: 19/09/2020

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 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 ido actualizado periódicamente y ahora mismo se utiliza Spring Data JPA 2.3.3 y Java 8. 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\ Java 7
  • A partir de la versión 2 ya se utiliza Spring 5 y Java 8.

Los cambios se pueden revisar en este fichero.

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 mostrar cómo se configuran todos los elementos necesarios para utilizar Spring Data JPA, algo totalmente automático en Spring Boot y explicado en Introducción a Spring Boot: Aplicación Web con Spring Data JPA.

Vamos a partir 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 la última actualización del presente tutorial. En estos momentos se trata de (2.3.3.RELEASE) y que ya incluye como dependencias los módulos del core de Spring requeridos en la versión 5.2.8.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-jcache) para utilizar como proveedor EHCache 3. El fichero de configuración se encuentra en /src/main/resources/cache.xml
  4. El driver jdbc para la base de datos a utilizar, en nuestro caso MySQL. En concreto, estoy utilizando MySQL 5.7.31 y un driver de la serie 8 (8.0.21).
  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 de programación

    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.4.21.Final</hibernate.version>
            <c3p0.version>0.9.5.5</c3p0.version>
            <slf4jlog4j.version>1.7.7</slf4jlog4j.version>
            <mysql.version>8.0.21</mysql.version>
            <spring.data.jpa.version>2.3.3.RELEASE</spring.data.jpa.version>
            <spring.version>5.2.8.RELEASE</spring.version>
            <junit.version>4.12</junit.version>
            <ehcache.version>3.9.0</ehcache.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-jcache</artifactId>
                <version>${hibernate.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.ehcache</groupId>
                <artifactId>ehcache</artifactId>
                <version>${ehcache.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 en desuso, y en clases Java. Esta configuración es muy similar a 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 que en breves momentos vamos a implementar.

    <?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.ehcache.jsr107.EhcacheCachingProvider</prop>
                    <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.jcache.internal.JCacheRegionFactory</prop>
                    <prop key="hibernate.javax.cache.uri">#{ new org.springframework.core.io.ClassPathResource("/cache.xml").getURL().toString()}</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 realizada programáticamente 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.core.io.ClassPathResource;
    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.ehcache.jsr107.EhcacheCachingProvider.class.getCanonicalName());
            jpaProperties.put("hibernate.cache.region.factory_class",
                    org.hibernate.cache.jcache.internal.JCacheRegionFactory.class.getCanonicalName());
            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"));
            try {
                jpaProperties.put("hibernate.javax.cache.uri", new ClassPathResource("/cache.xml").getURL().toString());
            } catch (IOException ex) {
                throw new IllegalArgumentException("invalid cache.xml location", ex);
            }
    
            entityManager.setJpaProperties(jpaProperties);
    
            return entityManager;
        }
    
        @Bean
        JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
            JpaTransactionManager transactionManager = new JpaTransactionManager();
            transactionManager.setEntityManagerFactory(entityManagerFactory);
            return transactionManager;
        }
    }
    
    

    Vamos a trabajar con dos entidades vinculadas por una relación unidireccional.

    package com.danielme.demo.springdatajpa.model;
    
    import javax.persistence.*
    
    @org.hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE)
    @Entity
    @Table(name = "countries")
    public class Country {
       @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        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;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "confederation_id")
        private Confederation confederation;
    
        public Country() {
            super();
        }
    
        public Country(String name, Integer population, Confederation confederation) {
            super();
            this.name = name;
            this.population = population;
            this.confederation = confederation;
        }
    
        @PrePersist
        public void onPersist() {
            creation = Calendar.getInstance();
        }
    
    ...getters y setters  
    
    }
    
    package com.danielme.demo.springdatajpa.model;
    
    import javax.persistence.*;
    
    @Entity
    @Table(name = "confederations")
    public class Confederation {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(nullable = false, unique = true)
        private String name;
    
        public Confederation(Long id, String name) {
            this.id = id;
            this.name = name;
        }
    
        public Confederation(Long id, String name) {
            this.id = id;
            this.name = name;
        }
    
        ...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 aunque 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 una única entidad pueden retornar Optional (buena práctica muy recomendable). En todas las versiones de Spring Data JPA también se puede devolver directamente la entidad, o null si no se encuentra, por ejemplo:

       Country findByName(String name);
      

      En los nombre de los métodos no sólo se puede hacer referencia a los atributos de la entidad sino también a sus relaciones. Por ejemplo, la siguienta consulta filtra países según el nombre de su confederación.

      List<Country> findByConfederationName(String confederationName);
      

      Son especialmente interesantes las posibilidades de filtrado para cadenas (Contains, IgnoreCase, Like, StartsWith, EndsWith…). Contains permite realizar la búsqueda aplicando los comodines ‘%’ a la cadena, algo que no hace Like.

      List<Country> findByNameContainingIgnoreCase(String name);
      

      El método anterior genera la siguiente consulta SQL.

      select
              country0_.id as id1_1_,
              country0_.createBy as createby2_1_,
              country0_.createdDate as createdd3_1_,
              country0_.lastModifiedBy as lastmodi4_1_,
              country0_.lastModifiedDate as lastmodi5_1_,
              country0_.confederation_id as confeder9_1_,
              country0_.creation as creation6_1_,
              country0_.name as name7_1_,
              country0_.population as populati8_1_ 
          from
              countries country0_ 
          where
              upper(country0_.name) like upper(?) escape ?
      09-19-2020 01:15:56,001 PM TRACE org.hibernate.type.descriptor.sql.BasicBinder:64 - binding parameter [1] as [VARCHAR] - [%p%]
      09-19-2020 01:15:56,002 PM TRACE org.hibernate.type.descriptor.sql.BasicBinder:64 - binding parameter [2] as [CHAR] - [\]
      
    2. Definiendo la query en JPQL. Se anota el método con @Query para proporcionar la consulta. 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 el orden en el que aparecen en la signatura del método, 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 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 tiene que coincidir con el de la NamedQuery que a su vez debe tener como prefijo el nombre de la entidad seguido de un punto:

      Optional<Country> byPopulationNamedQuery(Integer population);
      

      También podemos obviar esta convención y simplemente indicar el nombre de la consulta

      @Query(name = "Country.byPopulationNamedQuery")
      Optional<Country> cualquierNombre(Integer population);
      

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

    Tipos devueltos

    Ya hemos visto en los ejemplos anteriores que 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). Pero 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 una entidad JPA. 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 es un agregado de varias columnas lo modelaremos con una clase y utilizaremos el mecanismo de proyección estándar de JPA. 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 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 directamente en la propia consulta.

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

    @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");
    

    Pero si queremos hacerlo al “estilo Spring Data”, podemos recurrir a su sistema de proyecciones creando una interfaz con métodos de tipo get para unos atributos “virtuales” cuyos nombres deben coincidir con los de los campos devueltos por la consulta; en caso contrario se lanzará la excepción NotReadablePropertyException. El ejemplo anterior puede ser implementado del siguiente modo.

    @Query("select c.id as id, c.name as value from Country c where c.id = ?1")
    PairProjection getPairByIdInterface(Long id);
    
    package com.danielme.demo.springdatajpa.model;
    
    public interface PairProjection {
    
        Long getId();
    
        String getValue();
    }
    

    Estas proyecciones son más simples y requieren menos código pero las consultas son ligeramente más lentas que las realizadas vía constructor en una clase, por lo que si necesitamos afinar mucho el rendimiento debemos tenerlo en cuenta.

    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 fueron marcados como deprecated e incluso eliminados, 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 los objetos con los resultados, 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 mismas. 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 fueron ocultados y actualmente 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

    La anotación @Query también nos permite 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 muy descriptivas):

    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);
    

    El método devuelve el número de entidades eliminadas. De forma alternativa se puede utilizar el prefijo removeBy que tiene el mismo funcionamiento.

    @Transactional
    int removeById(Long id);
    

    Estos borrados ejecutan dos sentencias SQL: un SELECT para encontrar las entidades a eliminar y, si la consulta devuelve al menos una, el DELETE propiamente dicho.

    11:54:07,115 AM DEBUG org.hibernate.SQL:92 - select country0_.id as id1_0_, country0_.createBy as createBy2_0_, country0_.createdDate as createdD3_0_, country0_.lastModifiedBy as lastModi4_0_, country0_.lastModifiedDate as lastModi5_0_, country0_.creation as creation6_0_, country0_.name as name7_0_, country0_.population as populati8_0_ from countries country0_ where country0_.id=?
    11:54:07,176 AM DEBUG org.hibernate.SQL:92 - delete from countries where id=?
    

    Se puede hacer el DELETE con una única sentencia escribiéndola directamente con JPQL.

    @Transactional
    @Modifying
    @Query("delete from Country where id=:id")
    int deleteCountryById(@Param("id") Long id);
    

    Aunque esta segunda opción es más directa y rápida, hay que tener en cuenta que no se ejecutarán los métodos anotados con @PreRemove de las entidades que se eliminen.

    Consultas con SQL nativo

    Con la anotación @Query también es posible crear consultas en el lenguaje SQL nativo de la base de datos indicándolo con el flag “nativeQuery” que por defecto tiene un valor de falso. Por ejemplo, la siguiente consulta obtiene una lista de todos los países, obsérvese que en el FROM se utiliza el nombre de la tabla y no de la entidad.

    @Query(value = "select * from countries", nativeQuery = true)
    List<Country> findAllNative();
    

    Para las consultas nativas se aplica casi todo lo que hemos visto anteriormente:

    1. Paginación y ordenación
      @Query(value = "select * from countries", nativeQuery = true)
      Page<Country> findAllNative(Pageable pageable);
      
  9. Retorno de entidades si los nombres de las columnas devueltas coinciden con sus atributos
  10. Proyecciones con interfaces
  11. Obtención de datos en tuplas
  12. Ejecución de consultas definidas en la entidad, en este caso de tipo @NamedNativeQuery
  13. Actualizaciones con @Modifying

No funcionarán las proyecciones con constructor, a menos que definamos la consulta como una @NamedNativeQuery tal que así:

@SqlResultSetMapping(
        name = "pairConstructor",
        classes = @ConstructorResult(targetClass = Pair.class,
                columns = {
                @ColumnResult(name = "id", type = Long.class),
                @ColumnResult(name = "name", type = String.class)}))
@NamedNativeQuery(
        name = "Country.byPopulationNamedNativeQuery",
        query = "select id, name FROM countries WHERE population = ?1",
        resultSetMapping = "pairConstructor")
@Query(nativeQuery = true)
Pair byPopulationNamedNativeQuery(Integer population);

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 para poder ejecutar las consultas definidas en el repositorio. 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 gracias a la anotación @Sql de Spring.

He incluido en la raiz del proyecto un fichero Dockerfile y un script para configurar un servidor MySQL ya preparado para ejecutar directamente los tests. La imagen se puede crear y ejecutar con los siguientes comandos desde el directorio raiz del proyecto.

docker build -t spring-data-jpa-mysql .
docker run -d -p 3306:3306 spring-data-jpa-mysql
package com.danielme.demo.springdatajpa.test;

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.model.PairProjection;
import com.danielme.demo.springdatajpa.repository.CountryRepository;
import com.danielme.demo.springdatajpa.repository.specifications.CountrySpecifications;
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 javax.persistence.Tuple;
import java.util.Calendar;
import java.util.List;
import java.util.Optional;

import static org.junit.Assert.*;

@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> optCountry = countryRepository.findByName("Norway");
        assertTrue(optCountry.isPresent());
        assertEquals("Norway", optCountry.get().getName());
    }

    @Test
    public void testFindByConfederation() {
        List<Country> countries = countryRepository.findByConfederationName("CONMEBOL");

        assertEquals(1, countries.size());
        assertEquals("Colombia", countries.get(0).getName());
    }

    @Test
    public void testNoName() {
        assertFalse(countryRepository.findByName("France").isPresent());
    }

    @Test
    public void testNamedQuery() {
        assertTrue(countryRepository.byPopulationNamedQuery(115296767).isPresent());
    }

    @Test
    public void testNamedNativeQuery() {
        assertEquals(countryRepository.byPopulationNamedNativeQuery(115296767).getValue(), "Mexico");
    }

    @Test
    public void testQuerysSortingAndPaging() {
        Page<Country> page0 = countryRepository.findByNameWithQuery("%i%",
                PageRequest.of(0, 2, Sort.by(Sort.Direction.ASC, "name")));
        assertEquals(4, page0.getTotalElements());
        assertEquals(2, page0.getTotalPages());
        assertEquals("Colombia", page0.getContent().get(0).getName());
    }

    @Test
    public void testUpdate() {
        Calendar creation = countryRepository.findByName("Norway").get().getCreation();
        assertEquals(5, countryRepository.updateCreation(Calendar.getInstance()));
        assertTrue(countryRepository.findByName("Norway").get().getCreation().after(creation));
    }

    @Test
    public void testDeleteByName() {
        assertEquals(1, countryRepository.deleteByName("Norway"));
    }

    @Test
    public void testRemoveById() {
        assertEquals(1, countryRepository.removeById(SPAIN_ID));
    }

    @Test
    public void testRemoveByIdWithQuery() {
        assertEquals(1, countryRepository.deleteCountryById(SPAIN_ID));
    }

    @Test
    public void testJpaCriteria() {
        assertEquals("Mexico", countryRepository
                .findOne(CountrySpecifications.searchByName("Mexico")).get().getName());
    }

    @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"));
    }

    @Test
    public void testProjectionInterface() {
        PairProjection pair = countryRepository.getPairByIdInterface(SPAIN_ID);
        assertEquals(SPAIN_ID, pair.getId());
        assertEquals("Spain", pair.getValue());
    }

    @Test
    public void testFindAllListNative() {
        List<Country> countries = countryRepository.findAllNative();
        assertEquals(5, countries.size());
    }

    @Test
    public void testFindAllPageNative() {
        Page<Country> page = countryRepository.findAllNative(PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "name")));
        assertEquals(5, page.getTotalElements());
        assertEquals(2, page.getTotalPages());
    }



}

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.

Otros tutoriales sobre Spring y\o Hibernate

JPA e Hibernate: relaciones y atributos Lazy

Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA

Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache

SQL nativo con JPA e Hibernate

JPA + Hibernate: Claves primarias

Testing Spring con JUnit 4

Master Pyhton, Java, Scala or Ruby

6 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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .