...
Para poder utilizar JpaRepository, en principio no es necesario realizar ninguna configuración de librerías.
Si fuera necesario, incluir la es necesario incluir las siguientes dependencias en el pom.xml:
| Bloque de código | ||
|---|---|---|
| ||
<dependency> <!-- JPA --> <dependency> <groupId>org.springframework.boot< <groupId>mysql</groupId> <artifactId>spring<artifactId>mysql-boot-starter-data-jpa<connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Base de datos --> <dependency> <groupId>com.oracle</groupId> <artifactId>jdbc.driver</artifactId> <version>11.2.0.3.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> |
Además, debemos completar las credenciales de la fuente de datos, y Además, debemos proporcionar las credenciales de la fuente de datos, y esto lo haremos en nuestro src/main/resources/application.properties, por ejemplo:
...
Completados los pasos previos podemos definirnos nuestro repositorio. Se ha de crear una interfaz que extienda a JpaRepository parametrizado con el tipo correspondiente , y utilizando (al extender de JpaRepository no hace falta añadir la anotación @Repository). Siguiendo con nuestro ejemplo, tendríamos:
| Bloque de código | ||
|---|---|---|
| ||
package es.um.atica.------; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import es.um.atica.------.Usuario; @Repository public interface UsuarioRepository extends JpaRepository<Usuario,String> { public Usuario findByLogin(String login); public List<Usuario> findAllByOrderByLogin(); } |
...
Se pueden hacer múltiples operaciones filtrando, ordenando, etc., sobre base de datos. Las operaciones que implementa por defecto JpaRepository las podemos ver en su documentación, pero las que más usaremos serán findAll, findAllById y getById para obtener todos los registros, obtener los que tengan una lista de IDs que se le pase, o un único registro, pasando el ID. También están save y delete para guardar en base de datos, o para borrar.
| Bloque de código | ||
|---|---|---|
| ||
// Para guardar un nuevo elemento Customer en un jparepository de ese tipo
customerRepository.save(new Customer("Jack", "Bauer")); |
Además, podemos declarar métodos para otras consultas sin necesidad de implementarlos. Esto lo hace Además, podemos declarar métodos para otras consultas sin necesidad de implementarlos. Esto lo hace JpaRepository a través del nombre que le pongamos al método, que tendrá que seguir una notación concreta, utilizando una serie de operadores que trae predefinido y utilizando los nombres de los campos de la entidad con la que trata. Podemos ver todas las opciones en la documentación oficial. Algunos ejemplos podrían ser estos:
...
| Bloque de código | ||
|---|---|---|
| ||
@Autowired
private final UsuarioRepository usuarioRepo;
Usuario usuario = usuarioRepo.findByLogin("asdf"); |
Paginación y reordenación
A la hora de llamar al repositorio, al parámetro, nos ofrece métodos con un argumento Pageable. Esto es para realizar la paginación, y crearemos ese objeto con PageRequest.of, de la siguiente forma:
| Bloque de código | ||
|---|---|---|
| ||
Pageable page1 = PageRequest.of(0,10);
Page<Product> products = productRepository.findAllByPrice(10, page1); |
...
Tipo Optional
En versiones recientes de JpaRepository se ha introducido el tipo Optional. Este es un tipo contenedor, que puede contener o no un valor no nulo. Si tiene un valor, el método isPresent() devolverá true, y el método get() devolverá el valor.
Incluye otros métodos adicionales que dependen de la presencia o ausencia del valor, como orElse() (devuelve lo que indiquemos si no está presente), o ifPresent() (ejecuta un bloque de código si el valor está presente).
Podemos consultar su documentación para ver la especificación completa del tipo Optional.
Algunos ejemplos de casos de uso podrían ser los siguientes:
Si no se encuentra la entidad, devolvemos un objeto del mismo tipo con los valores por defecto:
Bloque de código language java
...
Foo
...
foo =
...
repository.
...
findById(
...
id) .orElse(new Foo());
Como vemos, con .by se indica la propiedad por la que ordenar, seguido del tipo de orden, ascendente o descendente. Además, podemos concatenar las expresiones con .and, pudiendo tener ordenación por diferentes atributos al mismo tiempo.
...
O podemos indicar que se devuelva null:
Bloque de código language java
...
Foo
...
foo =
...
repository.findById(id) .orElse(null);Podemos indicar que si no se encuentra el valor, se lance una excepción:
Bloque de código language java return repository.findById(id) .orElseThrow(() -> new EntityNotFoundException(id));Si queremos personalizar el proceso a seguir según si se encuentra o no la entidad:
Bloque de código language java Optional<Foo> fooOptional = fooRepository.findById(id); if (fooOptional.isPresent()) { Foo foo = fooOptional.get(); // processing with foo ... } else { // alternative processing.... }
Paginación y reordenación
A la hora de llamar al repositorio, al parámetro, nos ofrece métodos con un argumento Pageable. Esto es para realizar la paginación, y crearemos ese objeto con PageRequest.of, de la siguiente forma:
| Bloque de código | ||
|---|---|---|
| ||
Pageable page1 = PageRequest.of(0,10);
Page<Product> products = productRepository.findAllByPrice(10, page1); |
A este método se le pasa como primer parámetro la página a devolver, y como segundo el tamaño de la página. Además, podemos añadir un tercer parámetro indicándole un orden. Tanto para este tercer parámetro, como para ordenar sin paginación, utilizamos expresiones Sort:
| Bloque de código | ||
|---|---|---|
| ||
Sort sort = Sort.by("name").ascending().and(Sort.by("code").descending()); |
Como vemos, con .by se indica la propiedad por la que ordenar, seguido del tipo de orden, ascendente o descendente. Además, podemos concatenar las expresiones con .and, pudiendo tener ordenación por diferentes atributos al mismo tiempo.
Juntando todo esto en un ejemplo
En vez de usar la clase List, que podríamos, es mejor utilizar objetos tipo Page. De estos objetos podemos obtener sus elementos con getContent(), pero además podemos saber el número total de elementos y de páginas con getTotalElements() y getTotalPages(), respectivamente. Esto nos puede servir para, por ejemplo, pasarle el total de elementos a un componente tabla, cosa que no podríamos obtener con List. Los objetos Page por defecto no son serializables, no se pasarán correctamente como JSON.
Custom queries
Aparte de los métodos vistos hasta ahora, podemos definir directamente queries SQL:
| Bloque de código | ||
|---|---|---|
| ||
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
nativeQuery = true)
Page<User> findByLastname(String lastname, Pageable pageable);
} |
Utilizamos la anotación @Query. Es importante indicar que ?1 hace referencia al primer parámetro definido en el método (lastname).
Transacciones en JpaRepository
Por defecto, los métodos CRUD de los repositorios son transaccionales, es decir, incluyen la anotación @Transactional por defecto. Sí que tendremos que incluirla en los métodos query que definamos, es decir, en los métodos del repositorio que utilicen la anotación @Query, o en métodos externos que encapsulen varias llamadas a métodos de repositorios. También podríamos incluirla en los métodos CRUD normales de los repositorios para modificar su comportamiento por defecto. Esto podemos leerlo con más detalle en la documentación de Spring Data JPA.
Filtrar utilizando Specification
Otra opción para hacer queries propias es programarlas a través de Criteria y Specification. Specification es una interfaz para definir predicados reutilizables, teniendo métodos que podemos pasar directamente a llamadas a un JpaRepository. La parte negativa es que no se pueden combinar los filtros que aplican los métodos del repositorio con las Specification, pues sólo se pueden utilizar con el método findAll. La interfaz Specification es así:
| Bloque de código | ||
|---|---|---|
| ||
publicint interfacepageSize Specification<T>= {10; // Iteramos Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb); } |
Por tener nuestro proyecto organizado, podemos hacernos una clase Java para implementar nuestros métodos que devuelvan objetos Specificacion. Estos métodos tendrán que implementar toPredicate, la forma más sencilla sería la siguiente:
| Bloque de código | ||
|---|---|---|
| ||
public static Specification<Comunicacion> metodo(String autor, String titulo, List<Long> ids) {
return (root, query, cb) -> {
// Predicado que iremos formando con ANDs, para tener múltiples condiciones
Predicate predicado = null;
if (autor != null)
predicado = cb.like(root.get(Comunicacion_.AUTOR), "%" + autor + "%");
if (titulo != null) {
if (predicado == null)
predicado = cb.like(root.get(Comunicacion_.TITULO), "%" + titulo + "%");
else
predicado = cb.and(predicado, cb.like(root.get(Comunicacion_.TITULO), "%" + titulo + "%"));
}
// Subquery que puede acceder a cada elemento seleccionado por la query padre y
// añadir condiciones. La query externa quedaría así:
if (ids != null) {
Subquery<Long> subquery = query.subquery(Long.class);
Root<ComunicacionComponente> subRoot = subquery.from(ComunicacionComponente.class);
// Contamos para comparar más tarde con la longitud de la lista, pues tendrán
// que ser igual
subquery.select(cb.count(subRoot));
// Cogemos todos los ComunicacionComponente cuya comunicación tenga el mismo ID
// que el seleccionado por la query exterior
Predicate mismaComunicacion = cb.equal(root.get(Comunicacion_.ID),
subRoot.get(ComunicacionComponente_.COMUNICACION));
Predicate componenteEnLista = subRoot.get(ComunicacionComponente_.COMPONENTE).in(ids);
subquery.where(cb.and(mismaComunicacion, componenteEnLista));
if (predicado == null)
predicado = cb.equal(subquery, ids.size());
else
predicado = cb.and(predicado, cb.equal(subquery, ids.size()));
}
return predicado;
};
} |
En este ejemplo, se devuelve un Predicate en el que se devuelven Comunicaciones filtrando por título, autor y/o componentes. Se va elaborando el predicado, con sus condiciones, utilizando la instancia de CriteriaBuilder. Query hace referencia a la query actual, y se pueden sacar subqueries, por ejemplo, para añadir condiciones a los elementos obtenidos por la query padre. Con root estamos accediendo a los datos, a los objetos Comunicación. Para información más detallada, podemos consultar la documentación oficial de Spring. Métodos como este los podemos indicar como parámetro de findAll en el JpaRepository correspondiente:
| Bloque de código | ||
|---|---|---|
| ||
comunicacionRepository.findAll(metodo()); |
En la implementación del método podemos ver referencias a clases seguidas de un guión bajo, estas son las clases del metamodelo, vamos a ver cómo generarlas.
Generar clases del metamodelo
Podemos generar automáticamente estas clases, incluyendo esta dependencia en nuestro pom.xml:
| Bloque de código | ||
|---|---|---|
| ||
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<scope>provided</scope>
</dependency> |
...
sobre 5 páginas y mostramos los productos y la página a la que pertenecen
for(i=0; i<5; i++) {
Page<Product> products = productRepository.findAllByPrice(10, PageRequest.of(i*pageSize, pageSize, Sort.by("name").ascending());
for(Product p : products.getContent()) {
System.out.println("Page " + i + " - Product: " + p.getName());
}
} |
En vez de usar la clase List, que podríamos, es mejor utilizar objetos tipo Page. De estos objetos podemos obtener sus elementos con getContent(), pero además podemos saber el número total de elementos y de páginas con getTotalElements() y getTotalPages(), respectivamente. Esto nos puede servir para, por ejemplo, pasarle el total de elementos a un componente tabla, cosa que no podríamos obtener con List. Los objetos Page por defecto no son serializables, no se pasarán correctamente como JSON.
Custom queries
Aparte de los métodos vistos hasta ahora, podemos definir directamente queries SQL:
| Bloque de código | ||
|---|---|---|
| ||
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
nativeQuery = true)
Page<User> findByLastname(String lastname, Pageable pageable);
} |
Utilizamos la anotación @Query. Es importante indicar que ?1 hace referencia al primer parámetro definido en el método (lastname).
Transacciones en JpaRepository
Por defecto, los métodos CRUD de los repositorios son transaccionales, es decir, incluyen la anotación @Transactional por defecto. Sí que tendremos que incluirla en los métodos query que definamos, es decir, en los métodos del repositorio que utilicen la anotación @Query, o en métodos externos que encapsulen varias llamadas a métodos de repositorios. También podríamos incluirla en los métodos CRUD normales de los repositorios para modificar su comportamiento por defecto. Esto podemos leerlo con más detalle en la documentación de Spring Data JPA.
Imprimir por consola las queries SQL que se ejecutan
Podemos hacer que se impriman en la consola las queries que ejecuta Hibernate para depurar las llamadas a base de datos que se hacen añadiendo las siguientes dos propiedades al application.properties:
| Bloque de código |
|---|
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true |
Llamadas a métodos de base de datos
...