¡Llegó el momento de añadir tus propias consultas a los repositorios! De las variadas alternativas que exploraremos en el curso, empecemos por la más sencilla y sorprendente.
Índice
- La primera consulta de búsqueda
- Palabras claves (keywords) para declarar consultas
- Consultas de conteo y existencia
- Borrado de entidades
- Tipos devueltos
- Resumen
- Código de ejemplo
La primera consulta de búsqueda
Creemos un nuevo repositorio para Country
con un método que permita recuperar los países que contengan cierta cadena en su nombre y ordenados por el nombre:
@Transactional(readOnly = true)
public interface CountryDerivedQueryRepository extends Repository<Country, Long> {
List<Country> findByNameContainingOrderByNameAsc(String name);
}
¡Fantástico! Ni siquiera tenemos que escribir la consulta; la declaración del método es suficiente.
Estás ante la funcionalidad más llamativa de Spring Data: las consultas derivadas (derived queries) definen consultas para la clase de dominio que gestiona el repositorio usando el nombre del propio método. Con otras palabras, la consulta se «deriva» o deduce del nombre del método. Consultas fáciles de escribir, pero también de leer.
El nombre se compone de un prefijo que indica el tipo de consulta, seguido de los criterios de selección de los resultados y finalmente, con carácter opcional, la ordenación. Con findByNameContainingOrderByNameAsc
se buscan los países (find..By
) cuyo atributo nombre de la entidad (Name
) contenga (Containing
) cierta cadena (el parámetro del método), y que los resultados se ordenen (OrderBy
) por el campo nombre (Name
) de forma ascendente (Asc
).

Las búsquedas se indican con los prefijos find...By
, read…By, get...By
, query...By
, search...By
o stream...By
. Su elección no es una decisión funcional (todos hacen lo mismo) sino semántica, pues dependerá del significado que quieras darle al método. En el curso usaré siempre find.
Los siguientes métodos son equivalentes:
List<Country> findByNameContainingOrderByNameAsc(String name);
List<Country> readByNameContainingOrderByNameAsc(String name);
List<Country> getByNameContainingOrderByNameAsc(String name);
List<Country> queryByNameContainingOrderByNameAsc(String name);
List<Country> streamByNameContainingOrderByNameAsc(String name);
List<Country> searchByNameContainingOrderByNameAsc(String name);
Lo que escribas en los «tres puntos» de los prefijos se ignora, con la excepción de las palabras Distinct
, Top
y First
que ya veremos. Por ello, el siguiente método equivale a cualquiera de los anteriores:
List<Country> findPaisesByNameContainingOrderByNameAsc(String name);
Si trabajas con un entorno de desarrollo que ofrezca integración con Spring, como Eclipse IDE con Spring Tools o IntelliJ Ultimate, disfrutarás de la completitud de código en la escritura de las consultas derivadas.

Cuando Spring arranque y cree las implementaciones proxy de cada repositorio también generará una consulta JPQL para cada método. Si no fuera posible, se lanzará una excepción de tipo QueryCreationException
con un mensaje que circunstancia el problema. Por ejemplo, este método describe una consulta imposible:
List<Country> findByNombreContaining(String capital);
El error explica que la clase Country
no tiene un atributo llamado «nombre»:
org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract java.util.List com.danielme.springdatajpa.repository.query.CountryDerivedQueryRepository.findByNombreContaining(java.lang.String); Reason: Failed to create query for method public abstract java.util.List com.danielme.springdatajpa.repository.query.CountryDerivedQueryRepository.findByNombreContaining(java.lang.String); No property 'nombre' found for type 'Country'
Palabras claves (keywords) para declarar consultas
En esta sección exploramos las palabras claves (keywords) disponibles para construir las expresiones lógicas de selección de entidades. Indicaré la palabra, un método de ejemplo presente en la interfaz CountryDerivedQueryRepository
y la parte más relevante de la consulta SQL para HyperSQL que Hibernate terminará ejecutando. Aunque no las muestre, todos los métodos cuentan con pruebas que encontrarás en la clase CountryDerivedQueryRepositoryTest
.
Selección de resultados
Equals o Is. Condición de igualdad.
«Países que fueron admitidos en la ONU en una fecha dada».
List<Country> findByUnitedNationsAdmissionEquals(LocalDate date);
where c1_0.united_nations_admission=?
Is puede aparecer delante de cualquier otra condición para facilitar la comprensión del método. Suele omitirse por ser innecesaria.
Ninguna keyword. Equivale a Equals
o Is. La siguiente consulta es igual que la anterior:
List<Country> findByUnitedNationsAdmission(LocalDate date);
Not, IsNot. La desigualdad.
«Países no asociados a una confederación dada»:
List<Country> findByConfederationIdNot(Long id);
where c1_0.confederation_id!=?
Se puede combinar con otras palabras claves, mas no todas. Indicaré las combinaciones válidas.
After, IsAfter. Útil para filtrar por fechas posteriores (o estrictamente mayores) a la indicada.
«Países que ingresaron en la ONU a partir de cierta fecha»:
List<Country> findByUnitedNationsAdmissionAfter(LocalDate date);
where c1_0.united_nations_admission>?
Before, isBefore. Ahora filtramos por una fecha anterior (estrictamente menor) a la indicada.
«Países que ingresaron en la ONU antes de cierta fecha»:
List<Country> findByUnitedNationsAdmissionBefore(LocalDate date);
where c1_0.united_nations_admission<?
Between, isBetween. El operador BETWEEN
de JPQL\SQL. Los dos extremos del rango se incluyen.
«Países que ingresaron en la ONU entre dos fechas»:
List<Country> findByUnitedNationsAdmissionBetween(LocalDate dateMin, LocalDate dateMax);
where c1_0.united_nations_admission between ? and ?
LessThan, isLessThan. Menor estricto.
«Países con una población menor a una dada»:
List<Country> findByPopulationLessThan(int population);
where c1_0.population<?
El menor o igual es LessThanEqual, IsLessThanEqual.
GreaterThan, IsGreaterThan. Mayor estricto.
«Países con una población mayor a una dada»:
List<Country> findByPopulationGreaterThan(int population);
where c1_0.population>?
El mayor o igual es GreaterThanEqual, IsGreaterThanEqual.
Null, IsNull. Imponen la condición de que una propiedad sea nula.
«Países que no son miembros de la ONU (asumiendo que son aquellos sin fecha de admisión)»:
List<Country> findByUnitedNationsAdmissionIsNull();
where c1_0.united_nations_admission is null
NotNull, IsNotNull. La propiedad no puede ser nula.
«Países que son miembros de la ONU»:
List<Country> findByUnitedNationsAdmissionIsNotNull();
where c1_0.united_nations_admission is not null
Like, IsLike. El operador LIKE
para cadenas de JPQL.
«Todos los países con el nombre de capital como el indicado»:
List<Country> findByCapitalLike(String capital);
where c1_0.capital like ?
Se niega con NotLike, IsNotLike.
Spring Data «higieniza» las variables de las consultas de tipo cadena de tal modo que si contienen marcadores admitidos por LIKE
, estos serán tratados como simples caracteres. Por eso en las consultas SQL verás expresiones como esta:
like ? escape '\'
Containing, IsContaining, Contains. Aplica el operador LIKE
envolviendo la cadena a buscar con el comodín «%».
«Todos los países cuyo nombre de capital contenga una cadena dada»:
List<Country> findByCapitalContaining(String capital);
where c1_0.capital like ?
Se niega con NotContaining, IsNotContaining.
El interés no está en el SQL sino en su variable de entrada:
countryRepository.findByCapitalContaining("City");
binding parameter [1] as [VARCHAR] - [%City%]
Por tanto, no hagas esto:
countryRepository.findByCapitalLike("%City%");
Es un error porque Spring Data convertirá la cadena de búsqueda en esto:
binding parameter [1] as [VARCHAR] - [%\%City\%%]
StartingWith, StartsWith, IsStartingWith. La cadena debe comenzar por cierto prefijo. Aplica LIKE
añadiendo al argumento el marcador «%» al final.
«Países cuyo nombre comienzan con una cadena dada»:
List<Country> findByNameStartingWith(String name);
countryRepository.findByNameStartingWith("The");
where c1_0.name like ?
binding parameter [1] as [VARCHAR] - [The%]
EndingWith, IsEndingWith, EndsWith. La cadena debe terminar con cierto sufijo. Aplica LIKE
agregando al argumento el marcador «%» al principio.
«Países cuya capital terminan con una cadena dada»:
List<Country> findByCapitalEndingWith(String capital);
countryRepository.findByCapitalEndingWith("City");
where c1_0.capital like ?
binding parameter [1] as [VARCHAR] - [%City]
In, IsIn. El operador lógico IN
aplicado a un conjunto de valores. Como parámetro del método se admite cualquier Collection
, un array o un varargs
.
«Todos los países cuya confederación futbolística sea alguna de las proporcionadas»:
List<Country> findByConfederationIdIn(Collection<Long> ids);
where c1_0.confederation_id in(?,?)
Fíjate que esta consulta selecciona los resultados por una propiedad de una relación en lugar de por un atributo. Se permite navegación por las relaciones.
Se niega con NotIn, IsNotIn.
True, False, IsTrue, IsFalse. Establece el valor de una propiedad lógica.
«Países que no forman parte de la OCDE»:
List<Country> findByOcdeFalse();
where not(c1_0.ocde)
IgnoreCase, IgnoringCase. Compara cadenas con el operador «=» ignorando la capitalización gracias al empleo de la función UPPER
.
«El país cuya capital sea la indicada, ignorándose la capitalización»:
Optional<Country> findByCapitalIgnoreCase(String capital);
where upper(c1_0.capital)=upper(?)
Dado que el resultado será único (*), puedes devolverlo tal cual o bien dentro de un Optional
cuando se dé la posibilidad de que no exista. En estos casos, si el resultado es múltiple se producirá el error IncorrectResultSizeDataAccessException
.
(*) ¡Ojo! Podría no serlo si existieran dos capitales con el mismo nombre pero distinta capitalización.
Estas palabras clave pueden combinarse con containing
, startingWith
y endingWith
:
List<Country> findByNameContainingIgnoreCase(String name);
List<Country> findByNameStartingWithIgnoreCase(String name);
List<Country> findByCapitalEndingWithIgnoreCase(String capital);
En los tres métodos anteriores la consulta SQL es la misma, solo cambia la aplicación del comodín «%» al argumento:
binding parameter [1] as [VARCHAR] - [%republic%]
binding parameter [1] as [VARCHAR] - [republic%]
binding parameter [1] as [VARCHAR] - [%republic]
Empty, IsEmpty. Aplica el operador EMPTY
de JPQL a una propiedad de una clase de entidad que sea una colección. En el proyecto de ejemplo no tenemos este caso. Hablo de él aquí.
Se niega con NotEmpty, IsNotEmpty
¿Necesitas seleccionar entidades por más de un atributo o relación? Tan fácil como encadenar los criterios con And
y Or
.
And. Une criterios.
«Todos los países que contengan cierta cadena en su nombre y pertenezcan a cierta confederación dada por su identificador»:
List<Country> findByNameContainingAndConfederationId(String name, Long id);
where c1_0.name like ? and c1_0.confederation_id=?
Or. La condición Or
de toda la vida.
«Países que contengan en su nombre o capital una cadena dada».
List<Country> findByNameContainingOrCapitalContaining(String name, String capital);
where c1_0.name like ? or c1_0.capital like ?
Ten en cuenta que el orden de los parámetros del método debe respetar el orden en el que aparecen los campos a los que están vinculados en el nombre del método: primero, el nombre del país, y segundo, la capital. Debido a esta convención, el nombre de los parámetros resulta irrelevante.
Asimismo, nótese que la unión de múltiples criterios causa que los nombres de los métodos se «desmadren» en cuanto a su extensión. Por paradójico que suene, un método con un nombre descriptivo demasiado largo no resulta legible. Volveré sobre esta cuestión en el próximo capítulo.
Nota. Las siguientes palabras claves definidas por Spring Data no están disponibles en Spring Data JPA: Near
, IsNear
, Within
, IsWithin
, MatchesRegex
, Matches
, Regex
y Exists
.
Ordenación
Con ya sabes, la expresión OrderBy equivale a la cláusula ORDER BY
de JPQL. A ella le añades los atributos por las que efectuar la ordenación, indicando para cada una el tipo de orden deseado: ascendente (Asc) o descendente (Desc).
«Todos los países ordenados según su fecha de admisión en la ONU en sentido ascendente»:
List<Country> findByOrderByUnitedNationsAdmission();
Si ordenas por un único campo de forma ascendente, no hace falta añadir la palabra Asc
; por eso no la ves en el método anterior. No debería sorprendernos: la ordenación ascendente suele ser la predeterminada en la mayoría de bases de datos y lenguajes de programación.
No es el caso del próximo ejemplo. Primero ordena los países por fecha de admisión en la ONU en sentido ascendente y luego por el nombre en sentido ascendente:
List<Country> findByOrderByUnitedNationsAdmissionAscName();
order by c1_0.united_nations_admission asc,c1_0.name asc
El Asc
que acompaña a UnitedNationsAdmission
es imprescindible porque separa la declaración de ese atributo de la de Name
. Sin él, el método no representa una consulta derivada válida.
Aun escribiendo consultas derivadas, los criterios de ordenación pueden ser dinámicos; esto es, establecidos en tiempo de ejecución. Lo veremos en el capítulo ocho. Aquí tienes un adelanto:
List<Country> findByNameContaining(String name, Sort sort);
Sort sortByPopulation = Sort.by("population");
List<Country> republics = repository.findByNameContaining("Republic", sortByPopulation);
Como ves, hay que agregar un parámetro de tipo Sort
.
Limitar los resultados
Puedes limitar el número de resultados a obtener escribiendo First o Top entre el prefijo de la consulta y By
. Tras First
o To
p, indica la cantidad máxima de resultados; si no lo haces, se asume uno.
«Los tres países menos poblados»:
List<Country> findTop3ByOrderByPopulationAsc();
order by c1_0.population asc fetch first ? rows only
La obtención de resultados de manera paginada la trataremos en un capítulo específico.
Si quieres quedarte con los resultados distintos en una consulta que pueda devolver duplicados, escribe Distinct entre find...By
.
Consultas de conteo y existencia
La búsqueda de entidades con find...By
y sus sinónimos se complementa con estos dos prefijos; admiten los mismos criterios de selección de entidades que find...By
:
count…By. El resultado de la función agregada de conteo.
«Número de países que pertenecen a una confederación»:
long countByConfederationId(Long id);
select count(c1_0.id) from countries c1_0 where c1_0.confederation_id=?
Ni que decir tiene que es más eficiente emplear count...By
que obtener las entidades y contarlas.
exists…By. Indica la existencia de al menos un resultado.
«Comprobar si existe una capital»:
boolean existsByCapitalIgnoringCase(String capital);
HIbernate, al menos en HyperSQL, recupera el identificador del primer resultado:
select c1_0.id from countries c1_0 where upper(c1_0.capital)=upper(?) fetch first ? rows only
De nuevo, no obtengas las entidades si solo quieres verificar su existencia.
Borrado de entidades
Nos queda por tratar el prefijo delete…By\remove…By. Su cometido es el que parece a simple vista: eliminar las entidades que cumplan ciertos criterios de selección. No olvides que las modificaciones de entidades deben solicitarse dentro de una transacción con permiso de escritura.
«Eliminar los países que pertenezcan a cierta confederación»:
@Transactional
long deleteByConfederationId(Long id);
delete...By
devuelve el número de países borrados —el ejemplo anterior—, las entidades o void
.
@Transactional
List<Country> deleteByConfederationId(Long id);
El borrado se realiza a nivel de entidades, de una en una, con la operación remove
del gestor de entidades. Por eso puedes devolver las entidades borradas, ya que se obtuvieron para poder llamar a remove
. Si las entidades a borrar son numerosas, la eliminación llevará cierto tiempo. Cuando te encuentres en esta situación, evalúa implementar el borrado con JPQL tal y como veremos en el próximo capítulo.
Tipos devueltos
En los ejemplos hemos usado los tipos de retornos de las consultas derivadas más comunes (List
, Optional
), además de los casos particulares de long
(count...By
) y boolean
(exists...By
). Aunque cubren la mayoría de situaciones, examinemos todas las opciones. Supondremos que el repositorio se ha creado para el tipo de entidad T
. En el capítulo dedicados a las proyecciones, veremos qué otras «cosas» puede representar esa T
.
Resultados múltiples
Los resultados múltiples se recolectan en Iterable<T>, Collection<T>, Set<T> y Stream<T> , así como Streamable<T>, tipo propio de Spring Data que facilita el empleo de Iterable
. También se admiten las implementaciones concretas de colecciones ArrayList<T>, LinkedList<T> y LinkedHashSet<T>.
El caso de Stream
es especial. Nos obliga a que el método invocante sea transaccional y a cerrar el stream
. Esto es para permitir que los proveedores de JPA ofrezcan implementaciones de Stream
capaces de iterar sobre un cursor abierto con la base de datos. Así, los datos que leemos en el Stream
se van recuperando a demanda. Insisto, esto depende del proveedor de JPA y la base de datos.
Este método ya no debería tener secretos para ti:
Stream<Country> findAsStreamByNameContainingOrderByNameAsc(String name);
Devuelve ordenados los países cuyo nombre contenga cierta cadena. La expresión «AsStream» no afecta a la consulta. He tenido que usarla para distinguir este método de otro que ya habíamos escrito denominado findByNameContainingOrderByNameAsc
. Una alternativa, más acertada, sería elegir el prefijo stream...By
:
Stream<Country> streamByNameContainingOrderByNameAsc(String name);
Invoquémoslo en esta prueba que cumple con los dos requisitos que indiqué:
@Test
@Transactional(readOnly = true)
void testFindByNameContainingAsStream() {
List<Country> republics;
try (Stream<Country> stream =
countryRepository.findAsStreamByNameContainingOrderByNameAsc("Republic")) {
republics = stream.toList();
}
assertThat(republics)
.extracting(Country::getId)
.containsExactly(KOREA_ID, DOMINICAN_ID);
}
Sin la transacción, el test causará esta excepción:
org.springframework.dao.InvalidDataAccessApiUsageException: You're trying to execute a streaming query method without a surrounding transaction that keeps the connection open so that the Stream can actually be consumed; Make sure the code consuming the stream uses @Transactional or any other way of declaring a (read-only) transaction
Sin importar cómo Hibernate haya implementado la obtención de streams, por razones de eficiencia no debes recuperar las entidades en un Stream
para filtrarlas. Esta selección siempre debe realizarse con la consulta con el fin traer desde la base de datos solo los registros y columnas imprescindibles. Asimismo, no olvides que los streams solo pueden consumirse una vez.
Por todo lo anterior, te recomiendo la obtención progresiva de entidades con paginación. Es un mecanismo fácil de aplicar con JPA, además de eficiente y compatible con todas las bases de datos.
Resultados únicos
En lo que respecta a los resultados únicos, el retorno de una entidad de tipo T
es directo. Puesto que puede ser nula si no se encuentra, es preferible recurrir a Optional<T>, tal y como hicimos al declarar el método findByCapitalIgnoreCase
. Basta con ver Optional
para deducir que el retorno del método puede que no exista. El otro gran beneficio de Optional
es que posibilita el tratamiento de la respuesta de manera funcional, evitándose comprobar nulos.
Cuando no haya retorno alguno, se usa void. Sería la situación de deleteByConfederationId
si no informara de las entidades eliminadas o de su número.
Los tipos primitivos y sus wrappers se reciben tal cual. En las consultas derivadas solo tiene sentido hablar de los numéricos de count...By
y el lógico de exists...By
.
Métodos asíncronos
Si trabajas de forma asíncrona, esto es, ejecutando métodos de repositorios en paralelo en otros hilos (threads), el resultado se debe empaquetar en un objeto de la interfaz Future<T> —o un subtipo— para leerlo cuando esté disponible.
El retorno de esos métodos se declara con las clases FutureTask
y CompletableFuture
de la API de Java, y AsyncResult
de Spring (marcada como obsoleta en Spring Boot 3 \ Spring 6). También puedes usar directamente Future
, en cuyo caso recibirás la implementación AsyncResult
en Spring Boot 2 y CompletableFuture
en Spring Boot 3.
Todo lo que necesitas saber para empezar a trabajar con métodos asíncronos te lo explico en el siguiente artículo:
En pocas palabras, debes anotar un método público de un bean con @Async
y activar el tratamiento de esta anotación marcando con @EnableAsync
una clase de configuración (aquellas anotadas con @SpringBootApplication
o @Configuration
). El resultado del método, si lo necesitas, estará disponible en el objeto Future
cuando el método haya finalizado .
He aquí un ejemplo:
@Async
CompletableFuture<List<Country>> findAsyncCountriesByNameContaining(String name);
@Test
void testFindAsyncCountriesByNameContaining() throws ExecutionException, InterruptedException {
Future<List<Country>> republicsAsyncList = countryRepository.findAsyncCountriesByNameContaining(REPUBLIC_STRING);
assertThat(republicsAsyncList.get())
.extracting(Country::getId)
.containsExactlyInAnyOrder(KOREA_ID, DOMINICAN_ID);
}
Spring ejecuta el método findAsyncCountriesByNameContaining
en un hilo distinto al del test, así que el código de ambos métodos siguen caminos distintos. La sentencia republicsAsyncList.get()
espera a que finalice la ejecución de findAsyncCountriesByNameContaining
y devuelve su resultado, o bien lanza una excepción si hubo un error, de ahí el throws
que ves en la signatura del método.
Otros tipos
En este enlace puedes consultar todos los tipos de resultados posibles para la versión más reciente de Spring Data. En adición a los que acabamos de ver, encontrarás otros tipos que no abordaremos en el curso. Se trata de aquellos compatibles con la librería de utilidades funcionales Vavr y la programación reactiva de Spring y RxJava. Los tipos geométricos no aplican a JPA.
Resumen
Aquí tienes las claves del capítulo:
- Las consultas derivadas se declaran con el nombre de un método en el repositorio. Esto se consigue combinando una serie de palabras claves: un prefijo, los criterios de selección de las entidades y la ordenación (opcional).
- Las variables de la consulta son parámetros del método que respetan el orden en el que aparecen en el nombre del método.
- Además de buscar entidades, puedes eliminarlas, contarlas y comprobar la existencia de algunas en particular.
- Existen diversas opciones para recoger el resultado de la consulta derivada. Las habituales son
List
yOptional
. - Los repositorios, al igual que cualquier bean de Spring, admiten la anotación
@Async
y el retorno deFuture
.
Código de ejemplo
El proyecto, explicado en el capítulo dos, se encuentra en GitHub. Para más información sobre cómo utilizar GitHub consulta este artículo.
Otros tutoriales relacionados con Spring
Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA
Spring Boot: Gestión de errores en aplicaciones web y REST
Testing en Spring Boot con JUnit 45. Mockito, MockMvc, REST Assured, bases de datos embebidas