Spring Data se fundamenta en el concepto de repositorio. Ya vimos una pequeña y sorprendente muestra al final del primer capítulo. En el presente exploraremos este concepto poniendo el foco en el mundo JPA.
Índice
- El primer repositorio
- Repositorios genéricos predefinidos
- Personalizar los repositorios genéricos
- Crea tus propios repositorios genéricos
- Resumen
- Código de ejemplo
El primer repositorio
En Spring Data, un repositorio es una abstracción que proporciona las operaciones relativas a una clase de dominio para interactuar con un almacén de datos. Pero no de cualquier manera; a fin de cuentas, esta definición es trasladable a las clases DAO de toda la vida. El gran objetivo es reducir al máximo el código a escribir.
He aquí el repositorio más simple posible para la clase de tipo entidad Country
del proyecto de ejemplo:
package com.danielme.springdatajpa.repository.basic;
import com.danielme.springdatajpa.model.entity.Country;
import org.springframework.data.repository.Repository;
public interface CountryRepository extends Repository<Country, Long> {
}
Lo primero a destacar es la manera en la que declaramos el repositorio: con una interfaz que herede de Repository
. La tipamos con la clase del objeto del dominio que gestiona el repositorio, en nuestro caso Country
, y la de su identificador. Se ve más claro con el código de Repository
:
package org.springframework.data.repository;
import org.springframework.stereotype.Indexed;
@Indexed
public interface Repository<T, ID> {
}
El propósito de la interfaz es informar a Spring que las interfaces que la especialicen representan repositorios. No se requiere nada más, como aplicar alguna anotación o seguir una convención a la hora de nombrar los repositorios. Aun así, voy a respetar la praxis habitual consistente en usar el nombre de la clase de la entidad con el sufijo Repository
.
Como alternativa, podemos definir un repositorio anotando una interfaz con @RepositoryDefinition:
@RepositoryDefinition(domainClass = Country.class, idClass = Long.class)
public interface CountryRepository {
}
Ten cuidado con la anotación @Repository
. No tiene nada que ver con Spring Data. Es un subtipo de @Component
que sirve para marcar como beans de Spring clases que contienen operaciones de accesos a fuente de datos.
Spring Data es capaz de encontrar y configurar todos los repositorios, siempre y cuando esté activada esa opción. Es el caso de Spring Boot y, por ende, del proyecto de ejemplo. Cuando no sea así, activaremos la detección y configuración de los repositorios marcando con @EnableJpaRepositories
una clase de configuración (clase marcada con @Configuration
o @SpringBootApplication
). Por omisión, Spring buscará los repositorios a partir del paquete de la clase con esa anotación. La propiedad basePackages
permite indicar los paquetes raíz.
@Configuration
@EnableJpaRepositories(basePackages="com.danielme.springdatajpa.repository")
class JpaConfiguration {}
Si el repositorio es una interfaz, ¿dónde la implementamos? En ningún sitio. Spring genera en tiempo de ejecución una implementación como un objeto proxy. Comprobémoslo inyectando CountryRepository
en una prueba:
package com.danielme.springdatajpa.repository.basic;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
class CountryRepositoryTest {
@Autowired
private CountryRepository countryRepository;
@Test
void testRepository() {
log.info(countryRepository.toString());
}
}
Ejecutando el test desde nuestro IDE en modo depuración con un punto de ruptura o breakpoint, veremos la realidad del objeto CountryRepository
.

Sea como fuere, CountryRepository
no sirve de nada, pues carece de métodos. Aprenderemos a escribir consultas en futuros capítulos. Antes, dedicaremos el resto del actual a revisar los repositorios genéricos que Spring Data y Spring Data JPA ofrecen de serie.
Repositorios genéricos predefinidos
CrudRepository
Será habitual que muchas entidades precisen de un conjunto de operaciones denominadas CRUD: creación, lectura, actualización y borrado. El acrónimo viene del inglés (Create, Read, Update, Delete)
Spring Data proporciona un subtipo de Repository
con el elocuente nombre de CrudRepository
. Define doce métodos que, para mayor seguridad, respetan el tipado de la entidad y de su identificador.

Esta es la declaración de la interfaz:
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
La anotación @NoRepositoryBean
evita que Spring cree un objeto proxy para este repositorio. Asimismo, para que CrudRepository
sea un repositorio genérico, no está tipado; en su lugar respeta el tipado genérico (T
, ID
) de Repository
. Todo lo anterior conlleva que CrudRepository
siempre forme parte de una jerarquía de interfaces-repositorio que tenga como elemento final un repositorio concreto para un objeto de dominio.
CrudRepository
ofrece los siguientes métodos de lectura:
Optional<T> findById(ID id) | Obtiene una entidad dado su identificador. Cuando no exista devuelve un opcional vacío. |
boolean existsById(ID id) | Indica si una entidad existe. Si solo queremos comprobarlo y no necesitamos la entidad, existsById es más legible y eficiente que findById . |
Iterable<T> findAll() | Recupera todas las entidades. Usar este método es peligroso porque si hay muchas nos traeremos una colección inmensa de objetos. Debemos obtener las entidades en pequeños lotes con paginación, tal y como analizaremos en el capítulo nueve. |
Iterable<t> findAllById(Iterable<T> ids) | Obtiene un lote de entidades a partir de sus identificadores únicos (no se admiten nulos, se lanzará NullPointerException ). Las que no existan no aparecerán en los resultados, ni siquiera como nulas. |
long count() | Cuenta el número total de entidades. Espero que nadie llame a findAll y cuente 🤦 |
Estos son los métodos de escritura:
<S extends T> S save (S entity) | Guarda la entidad, tanto si es de nueva creación como si ya existe. |
<S extends T> Iterable<S> saveAll(Iterable<S> entities) | Guarda las entidades solicitadas. En el caso de JPA, se llama a save para cada una de ellas. |
void deleteById(ID id) | Elimina la entidad según su identificador. Si no existe, se lanza EmptyResultDataAccessException . Deberíamos ejecutar antes existsById para chequear la existencia. O mejor todavía, recurrir a otras alternativas que se propondrán en el curso. |
void delete(T entity) | Elimina la entidad. Si no existe, no hace nada. |
void deleteAllById(Iterable<? extends ID> ids) | Elimina las entidades indicadas con sus identificadores únicos (no se admiten nulos, se lanzará NullPointerException ). La implementación de JPA invoca deleteById para cada identificador. |
void deleteAll(Iterable<? extends T> entities) | Elimina las entidades recibidas como argumentos (no se admiten nulos). En JPA se ejecuta delete para cada identificador. |
void deleteAll() | ¡Lo borra todo! Además de peligrosa, es una operación ineficiente si hay muchas entidades porque las obtiene con findAll y luego llama a delete para cada una. |
¿Ves algo llamativo en los parámetros y retornos de estos métodos? En lugar de listas o colecciones, los agregados de datos son objetos de tipo Iterable
, una abstracción de nivel superior (la interfaz Collection
hereda de ella). El motivo es ser lo menos restrictivos posible con el fin de facilitar la implementación de CrudRepository
por parte de los módulos de Spring Data.
Este tipo de decisiones son habituales cuando se diseñan librerías y marcos de trabajo genéricos; sin embargo, en proyectos finales lo común será que utilicemos listas. Por ello, Spring Data 3.0 introdujo la siguiente interfaz.
public interface ListCrudRepository<T, ID> extends CrudRepository<T, ID> {
<S extends T> List<S> saveAll(Iterable<S> entities);
List<T> findAll();
List<T> findAllById(Iterable<ID> ids);
}
ListCrudRepository
especializa a CrudRepository
para sobrescribir los métodos que devuelven Iterable
de tal modo que retornen listas.

En cualquier caso, ¿cómo disfrutamos de los métodos de CrudRepository
? Tan fácil como heredar de ella:
package com.danielme.springdatajpa.repository;
import com.danielme.springdatajpa.model.Country;
import org.springframework.data.repository.CrudRepository;
public interface CountryCrudRepository extends CrudRepository<Country, Long> {
}
package com.danielme.springdatajpa.repository;
import com.danielme.springdatajpa.model.Confederation;
import org.springframework.data.repository.CrudRepository;
public interface ConfederationCrudRepository extends CrudRepository<Confederation, Long> {
}
¡Voilà! Ahora contamos con las operaciones de CrudRepository
para las entidades Country
y Confederation
.

Probando CrudRepository
Usemos los repositorios en una nueva clase de prueba:
@SpringBootTest
class CountryCrudRepositoryTest {
@Autowired
private CountryCrudRepository countryRepository;
@Autowired
private ConfederationCrudRepository confederationRepository;
@Test
void testCreate() {
Country country = new Country();
country.setName("Cuba");
country.setPopulation(11113215);
country.setOcde(false);
country.setCapital("Havana City");
country.setUnitedNationsAdmission(LocalDate.of(1945, 10, 24));
Confederation concacaf = confederationRepository.findById(CONCACAF_ID).get();
country.setConfederation(concacaf);
countryRepository.save(country);
assertThat(country.getId()).isNotNull();
}
}
testCreate
añade un país a la base de datos. Country
requiere de la confederación futbolística correspondiente al país y que obtenemos con ConfederationCrudRepository
. Solicitamos el guardado llamando a save
. Tras la operación, el objeto Country
contiene el identificador que la base de datos le asignó, hecho que corrobora la última línea.
El método save
guarda una entidad sin importar si es nueva o una ya existente. Spring Data JPA decidirá si llamar al método merge
o al persist
del gestor de entidades (*). Debes saber que persist
siempre es reemplazable por merge
, pero aquí te explico un caso en el que se desaconseja hacerlo. De todas formas, con Spring Data JPA es un detalle irrelevante.
(*) Si no estás familiarizado con el funcionamiento del gestor de entidades (la interfaz que ofrece las operaciones básicas de JPA), te recomiendo este artículo:
Aunque Spring Data JPA entierra el gestor bajo sus abstracciones, es fundamental que comprendas su funcionamiento si quieres hacer un uso responsable de JPA.
Volvamos a nuestras pruebas. Actualicemos la población de España:
@Test
void testUpdatePopulation() {
Country country = countryRepository.findById(DatasetConstants.SPAIN_ID).get();
int newPopulation = 47432805;
country.setPopulation(newPopulation);
countryRepository.save(country);
Country countryAfterSave = countryRepository.findById(DatasetConstants.SPAIN_ID).get();
assertThat(countryAfterSave.getPopulation()).isEqualTo(newPopulation);
}
Obtenemos la entidad, la editamos y solicitamos su guardado con save
. Todo muy lógico, de hecho el test es válido; pero a veces la llamada a save
es innecesaria. Lo veremos en el próximo capítulo.
La ejecución de las pruebas anteriores tiene un efecto colateral que afectará a las demás: modifican los registros almacenados en la base de datos. Todos los tests deben ser independientes y ejecutables en cualquier orden.
Lo que haré será vaciar las tablas y volver a cargar los registros definidos en el fichero data.sql
tras la ejecución de pruebas que alteren los datos. Aunque parece una tarea complicada, gracias a la anotación @Sql
de Spring es bien sencillo:
@Sql(value = {"/reset.sql", "/data.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
@Sql
ejecuta en orden los scripts /src/test/resources/reset.sql
y /src/test/resources/data.sql
después de la prueba marcada con ella (Sql.ExecutionPhase.AFTER_TEST_METHOD
). El valor predeterminado de executionPhase
es ExecutionPhase.BEFORE_TEST_METHOD
(antes de la prueba).
@Sql
también puede marcar una clase de prueba para que se aplique a todos los tests que contenga.
@SpringBootTest
@Sql(value = {"/reset.sql", "/data.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
class CountryCrudRepositoryTest {
Sin duda, lo más cómodo sería asegurar la ejecución de ambos scripts anotando con @Sql
todas las clases de prueba. Dicho esto, solo usaré @Sql
cuando sea imprescindible, ya que no quiero demorar la ejecución de las pruebas.
JpaRepository
Los módulos de Spring Data suelen incluir repositorios genéricos con funcionalidades específicas de la tecnología que tratan. En Spring Data JPA encontramos JpaRepository
.

¡El diagrama se complica! Calma. JpaRepository
es un tipo de ListCrudRepository
con métodos adicionales. Varios de ellos deben obviarse porque están marcados obsoletos. También hereda de ListPagingAndSortingRepository
y QueryByExampleExecutor
. Dado que trataremos ambas interfaces en futuros capítulos, vamos a ignorarlas.
Antes de Spring Data 3, JpaRepository
heredaba de CrudRepository
y PagingAndSortingRepository
en vez de ListCrudRepository
y ListPagingAndSortingRepository
. Por ello contaba con estos métodos que sobrescribian a los declarados en las interfaces «padre» para que devolvieran List
en lugar de Iterable
:
List<T> findAll();
List<T> findAllById(Iterable<ID> ids);
<S extends T> List<S> saveAll(Iterable<S> entities);
Existe un método para recuperar una entidad que se limita a invocar al getReference
del gestor de entidades:
T getReferenceById(ID id)
La «referencia» retornada consiste en un objeto proxy que representa a la entidad y que solo contiene su identificador. Por tanto, la creación de la referencia no implica una consulta a la base de datos que obtenga todos los datos de la entidad. Eso sí, cuando pidamos al proxy cualquier atributo distinto del identificador, se obtendrá el registro desde la tabla.
Estamos ante una operación útil para afinar el rendimiento en ciertos escenarios. El más evidente que se me ocurre es el establecimiento de una relación simple, tarea que hicimos en el método CountryCrudRepositoryTest#testCreate
con estas líneas:
Confederation concacaf = confederationRepository.findById(CONCACAF_ID).get();
country.setConfederation(concacaf);
Date cuenta de que solo queremos la entidad representada por el objeto concacaf
para relacionarla con country
. A su vez, lo único que JPA precisa de concacaf
es su identificador porque será la clave ajena a la tabla confederations
del nuevo registro en countries
correspondiente a country
. Por este motivo es un desperdicio recuperar la entidad concacaf
completa desde la base de datos con findById
, acción que implica ejecutar una SELECT
. Es suficiente con el objeto «referencia» que contiene el identificador:
Confederation concacaf = confederationRepository.getReferenceById(CONCACAF_ID);
country.setConfederation(concacaf);
La operación flush
del gestor de entidades fuerza la sincronización inmediata de las entidades del contexto de persistencia con la base de datos. Será raro que tengamos que recurrir a este método, pues Hibernate efectúa la sincronización automáticamente. Lo comprobaremos en el próximo capítulo.
void flush() | Es lo que parece: invoca al método flush . |
<S extends T> List<S> saveAll(Iterable<S> entities) | Llama a save para cada entidad. |
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities) | Invoca saveAll para aplicar save a cada entidad. Tras ello, llama a flush . |
Otro grupo de métodos de JpaRepository
son las operaciones de borrado en lote (batch):
void deleteAllInBatch();
void deleteAllInBatch(Iterable<T> entities);
void deleteAllInBatch();
Parecen más sofisticadas de lo que son. Borran las entidades en una sola sentencia DELETE
de SQL. Si bien esto es más rápido que hacerlo de una en una llamando al remove
del gestor de entidades, hay un inconveniente que puede ser importante: las entidades que ya estuvieran en el contexto de persistencia no se ven afectadas por el borrado. Esto significa que los métodos de tipo @PreRemove
y @PostRemove
asociados a esas entidades se ignoran. Tampoco se propaga la operación de borrado hacia las entidades relacionadas aunque así lo hayamos establecido en la configuración de la clase de las entidades que estamos eliminando.
Personalizar los repositorios genéricos
A priori, las operaciones de los repositorios genéricos como CrudRepository
nos vienen muy bien. El problema es que cuando heredamos de CrudRepository
, o de cualquier interfaz, es imposible excluir métodos; es todo o nada. En consecuencia, es posible que agreguemos a nuestros repositorios operaciones innecesarias o, peor aún, que no queremos. Por ejemplo, alerté párrafos atrás acerca del empleo de findAll
y deleteAll
.
Por fortuna, podemos incorporar en un repositorio métodos de los repositorios genéricos con tan solo copiar su signatura. En realidad estamos declarando «consultas derivadas», tema del capítulo cinco.
Imagina que deseamos que el repositorio de las confederaciones ofrezca algunos métodos de consulta de CrudRepository
. No queremos el resto, luego heredar de CrudRepository
no es una opción. Así que heredamos de Repository
y copiamos los métodos de CrudRepository
que necesitemos:
public interface ConfederationCustomCrudRepository extends Repository<Confederation, Long> {
Optional<Confederation> findById(Long id);
boolean existsById(Long id);
long count();
}
Deseo concedido 🧞♂️✨
Crea tus propios repositorios genéricos
Si tuviéramos varias clases de tipo entidad en la situación de Confederation
(deben contar con algunos métodos de lectura de CrudRepository
), lo más práctico será crear un repositorio genérico reutilizable a imagen y semejanza de CrudRepository
. Esto es, uno anotado con @NoRepositoryBean
y tipado para las clases T
(la entidad) e ID
(el identificador):
@NoRepositoryBean
public interface ReadCommonRepository<T, ID> extends Repository<T, ID> {
Optional<T> findById(ID id);
boolean existsById(ID id);
long count();
}
Ahora, usaremos ReadCommonRepository
como el repositorio padre de aquellos que precisen de sus operaciones:
public interface ConfederationReadRepository extends ReadCommonRepository<Confederation, Long> {
}

Puedes crear los repositorios genéricos que consideres oportunos. Contendrán cualquier tipo de operación permitida por Spring Data JPA.
Resumen
Recopilo los fundamentos del capítulo:
- Un repositorio es una interfaz que hereda de la interfaz
Repository
y que está tipada para un tipo de entidad y su identificador. Otra posibilidad es usar la anotación@RepositoryDefinition
. - Existen repositorios genéricos con operaciones predefinidas listos para ser heredados. También puedes copiar de ellos los métodos que quieras en tus repositorios.
- Puedes crear tus propios repositorios genéricos.
- Ten cuidado si heredas de otros repositorios, no sea que recibas métodos que no quieres. Por ejemplo, para la mayoría de entidades no querrás
findAll
(sin paginación) ydeleteAll
.
Código de ejemplo
El proyecto se encuentra en GitHub. Se explica en el capítulo dos. Para más información sobre cómo utilizar GitHub, consultar este artículo.
Otros tutoriales relacionados con Spring
Introducción a Spring Boot: API REST y Spring Data JPA
Spring Boot: Gestión de errores en aplicaciones web y REST
Testing en Spring Boot con JUnit 4 \ 5. Mockito, MockMvc, REST Assured, bases de datos embebidas
Spring JDBC Template: simplificando el uso de SQL
Testing Spring Boot: Docker con Testcontaines y JUnit 5
Spring Security \ Spring Boot: Segurización API REST con BASIC y base de datos (SQL y Spring Data).