| Tabla de contenidos |
|---|
...
Para el acceso a base de datos, utilizando JpaRepository podemos obtener objetos de base de datos sin necesidad de implementar ningún método, tan solo declararlos con la notación adecuada.
Pre-requisitos
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 proporcionar las credenciales de la fuente de datos, y esto lo haremos en nuestro src/main/resources/application.properties, por ejemplo:
| Bloque de código |
|---|
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.Oracle10gDialect
spring.jpa.generate-ddl = false
spring.jpa.hibernate.ddl-auto = validate
# Datasource
spring.datasource.url=jdbc:oracle:thin:@hydra-prescan.atica.um.es:1526/ZEUSDESA
spring.datasource.username=USUARIOBD
spring.datasource.password=CONTRASEÑABD |
...
Además, debemos completar las credenciales de la fuente de datos, y esto lo haremos en nuestro src/main/resources/application.properties, por ejemplo:
| Bloque de código |
|---|
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.Oracle10gDialect
spring.jpa.generate-ddl = false
spring.jpa.hibernate.ddl-auto = validate
# Datasource
spring.datasource.url=jdbc:oracle:thin:@hydra-prescan.atica.um.es:1526/ZEUSDESA
spring.datasource.username=USUARIOBD
spring.datasource.password=CONTRASEÑABD |
Entidades y DTOs
En nuestros backends Spring Boot, utilizamos el patrón DTO, con el que separamos los objetos de negocio de los objetos para transferencia de datos. Este último sería el DTO (Data Transfer Object) un objeto equivalente a las entidades (objetos de negocio) con los datos que queramos devolver, y sin las anotaciones que tiene la entidad (es un POJO), y a través del cuál también podemos obtener la entidad de base de datos. Devolver una entidad en un servicio no es una buena práctica, pues estos objetos pueden llevar información relacionada que no se debe trasnferir, y por ello es marcado como una vulnerabilidad en SonarQube, de ahí la necesidad de utilizar este patrón.
Una vez hayamos proporcionado a nuestra aplicación acceso a base de datos, debemos hacernos la clase java que mapee los objetos de base de datos que se van a obtener con el JpaRepository. Para auto generar las entidades, podemos consultar esta página de la wiki. También podemos generarlas a mano. Para ilustrar esto, tenemos la clase Usuario:documentación: Creación de Entidades JPA a partir de tablas de Base de Datos.
Para la conversión entre entidades y DTOs tenemos dos opciones que veremos en el siguiente sub-apartado, utilizar ModelMapper, o incluir tanto en la entidad como en el DTO un constructor que tome como parámetro el otro tipo. En el siguiente ejemplo se incluye ese constructor, y la entidad nos quedaría así:
| Bloque de código | ||
|---|---|---|
| ||
package es. | ||
| Bloque de código | ||
| ||
package es.um.atica.--------;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import es.um.atica..--------.UsuarioDTO;
@Entity
@Table( name = "USUARIOS" )
public class Usuario {
private String login;
public Usuario() {
}
public Usuario(UsuarioDTO usuario) {
this(usuario.getLogin());
}
public Usuario( String login ) {
this.login = login;
}
//////////////////////////////////// GETTERS & SETTERS ////////////////////////////////////
@Id
public String getLogin() {
return this.login;
}
public void setLogin( String login ) {
this.login = login;
}
} |
Vemos Mientras que especificamos el nombre de la tabla. Tanto esto como los nombres de las columnas se pueden omitir si se corresponden con los nombres que tienen en base de datos, cambiando los guiones bajos por mayúsculas. Si en base de datos tenemos, por ejemplo, la columna USUARIO_ROL, nuestro atributo en Java lo llamaremos usuarioRol. Tenemos que incluir los getters y setters, poniendo en los get las anotaciones correspondientes, como @Id o @OneToMany, por poner ejemplos.
Nos puede llamar la atención UsuarioDTO. Los DTO, Data Transfer Objects, son clases simples, sin ninguna anotación; POJOs. Estos se utilizarán en los servicios REST para evitar vulnerabilidades al mandar objetos que van enlazados a entidades persistentes de base de datos. Aunque en los repositories no utilizaremos los DTOs, para que lo veamos con un ejemplo, UsuarioDTO sería así:
el DTO correspondiente sería este:
| Bloque de código | ||
|---|---|---|
| ||
package es.um.atica..--------;
import es.um.atica.--------.Usuario;
public class UsuarioDTO {
private String login;
public UsuarioDTO() {
}
public UsuarioDTO(Usuario usuario) {
this(usuario.getLogin());
}
public UsuarioDTO( String login ) {
this.login = login; | ||
| Bloque de código | ||
| ||
package es.um.atica..--------; import es.um.atica.--------.Usuario; public class UsuarioDTO { private String login; public UsuarioDTO() { } public UsuarioDTO(Usuario usuario) { this(usuario.getLogin()); } public UsuarioDTO( String login ) { this.login = login; } //////////////////////////////////////////////// GETTERS & SETTERS //////////////////////////////////// public String getLogin() { return this.login; } public void setLogin( String login ) { this.login = login; } } |
Secuencias de base de datos
Utilizaremos la etiqueta @SequenceGenerator, a la que tendremos que especificarle el nombre de la secuencia (allocationSize) y el allocationSize. Este último parámetro es importante, pues define el incremento entre cada número de la secuencia, y por defecto tiene un valor de 50, así que lo ponddremos al valor necesario, que normalmente será 1. Esta etiqueta se pone fuera de la clase, y quedaría así:
| Bloque de código | ||
|---|---|---|
| ||
@SequenceGenerator(name = "comunicacionID", sequenceName = "PORTALFUNDEWEB.SEQ_COMUNICACIONES", allocationSize = 1) |
Después, dentro de la clase, tendremos que poner la etiqueta @GeneratedValue al getter corresponiente:
| Bloque de código | ||
|---|---|---|
| ||
@GeneratedValue(generator = "comunicacionID") |
JpaRepository
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 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();
} |
Tenemos definidos métodos para los que no hace falta implementación. Por un lado, tenemos buscar un usuario por un login dado, y después tenemos obtener todos los usuarios ordenados por login. Se pueden hacer múltiples operaciones filtrando, ordenando, etc., sobre base de datos. Podemos consultar los diferentes métodos disponibles en la documentación oficial. También hay métodos que no hace falta declarar, como findAll(). Estos nos saldrán en el autocompletado de Eclipse.
Para utilizar nuestro UsuarioRepository, es simple:
| Bloque de código | ||
|---|---|---|
| ||
private final UsuarioRepository usuarioRepo;
Usuario usuario = usuarioRepo.findByLogin("login.usuario"); |
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:
| Bloque de código | ||
|---|---|---|
| ||
int pageSize = 10;
// Iteramos 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).
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 | ||
|---|---|---|
| ||
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
} |
Para implementar toPredicate, la forma más sencilla sería la siguiente:
...
| language | java |
|---|
...
Pasar listas de entidades a DTOs y viceversa
Este punto se explica en Devolver DTO como respuesta de peticiones REST
Explicación alternativa (anterior)
| Bloque de código | ||
|---|---|---|
| ||
//Como hemos visto, para pasar un solo objeto de entidad a DTO o viceversa disponemos del constructor correspondiente.
//Para todos los objetos de una lista a su otra forma, podríamos hacerlo con un for sin problemas, pero podemos hacerlo de una forma más simple utilizando expresiones lambda.
//A continuación tenemos un ejemplo con ambas formas de hacerlo, y podemos ver que el código queda más breve utilizando la expresión lambda:
final List<AlumnoTitulacion> lista = alumnoTitulacionRepository.findAll();
// Con un for
final List<AlumnoTitulacionDTO> listaDTOfor = new ArrayList<AlumnoTitulacionDTO>();
for(AlumnoTitulacion x : lista) {
listaDTOfor.add( new AlumnoTitulacionDTO(x) );
}
// Con una expresión lambda
final List<AlumnoTitulacionDTO> listaDTOlambda = lista.stream().map( x -> new AlumnoTitulacionDTO( x ) ).collect( Collectors.toList() );
//Se pueden usar las dos formas indistintamente, según la forma que se prefiera. |
ModelMapper
Una herrramienta que nos puede simplificar la conversión entre entidades y DTOs es ModelMapper. Es posible mapear los objetos complejos, es decir, los que tienen otros tipos anidados. Para utilizarlo, primero tenemos que declarar el bean en nuestro SecurityConfig.java, incluyendo la configuración de la estrategia de matching para que, como hemos dicho, se pueda hacer el mapeo complejo:
| Bloque de código | ||
|---|---|---|
| ||
@Bean
public ModelMapper modelMapper() {
final ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy( MatchingStrategies.LOOSE );
return modelMapper;
} |
Para utilizarlo en nuestros servicios, simplemente tendremos que instanciarlo con Autowired:
| Bloque de código | ||
|---|---|---|
| ||
@Autowired
private ModelMapper modelMapper; |
El uso básico que haremos de éste será la conversión, para la que utilizaremos el método map, indicando el objeto a transformar y la clase destino:
| Bloque de código | ||
|---|---|---|
| ||
final Diploma diploma = diplomaRepository.findById( 1 ).get();
final DiplomaDTO diplomaDto = modelMapper.map( diploma, DiplomaDTO.class ); |
Esto lo podemos utilizar con listas de la misma forma que hemos explicado previamente, con expresiones lambda o sin ellas:
| Bloque de código | ||
|---|---|---|
| ||
final List<Diploma> lista = diplomaRepository.findAll();
final List<DiplomaDTO> listaDTO = lista.stream().map( x -> modelMapper.map( x, DiplomaDTO.class ) ).collect( Collectors.toList() ); |
Secuencias de base de datos
Utilizaremos la etiqueta @SequenceGenerator, a la que tendremos que especificarle el nombre de la secuencia (allocationSize) y el allocationSize. Este último parámetro es importante, pues define el incremento entre cada número de la secuencia, y por defecto tiene un valor de 50, así que lo ponddremos al valor necesario, que normalmente será 1. Esta etiqueta se pone fuera de la clase, y quedaría así:
| Bloque de código | ||
|---|---|---|
| ||
@SequenceGenerator(name = "comunicacionID", sequenceName = "PORTALFUNDEWEB.SEQ_COMUNICACIONES", allocationSize = 1) |
Después, dentro de la clase, tendremos que poner la etiqueta @GeneratedValue al getter corresponiente:
| Bloque de código | ||
|---|---|---|
| ||
@GeneratedValue(generator = "comunicacionID") |
Evitar dependencias circulares: @JsonBackReference y @JsonManagedReference
Para evitar dependencias circulares al obtener objetos de base de datos y serializarlos, disponemos de las anotaciones @JsonBackReference y @JsonManagedReference, que utilizaremos sobre los atributos que forman la relación bidireccional.
- @JsonManagedReference se utiliza en la clase "padre" de la relación bidireccional, es decir, la que se mostrará de forma normal.
- @JsonBackReference es en el que sería el "hijo", la parte que se omitirá al ser serializado.
Podemos poner el siguiente ejemplo, con una clase User que tiene una lista de Item, y la clase Item que tiene un atributo User. Si queremos que al obtener el Item se incluya su User, pero que al mostrar este User se omita la lista de Items (lo que generaría un bucle infinito), pondríamos las anotaciones así:
| Bloque de código | ||
|---|---|---|
| ||
public class User {
public int id;
public String name;
public List<Item> userItems;
@JsonBackReference
public List<Item> getUserItems() {
return userItems;
}
}
public class Item {
public int id;
public String itemName;
public User owner;
@JsonManagedReference
public User getOwner() {
return owner;
}
} |
Así, un ejemplo de respuesta al obtener un Item sería este:
| Bloque de código | ||
|---|---|---|
| ||
{
"id":2,
"itemName":"book",
"owner":
{
"id":1,
"name":"John"
}
} |
JpaRepository
Completados los pasos previos podemos definirnos nuestro repositorio. Se ha de crear una interfaz que extienda a JpaRepository parametrizado con el tipo correspondiente (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 es.um.atica.------.Usuario;
public interface UsuarioRepository extends JpaRepository<Usuario,String> {
public Usuario findByLogin(String login);
public List<Usuario> findAllByOrderByLogin();
} |
Tenemos definidos métodos para los que no hace falta implementación. Por un lado, tenemos buscar un usuario por un login dado, y después tenemos obtener todos los usuarios ordenados por login.
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 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 | ||
|---|---|---|
| ||
// Buscar por dos campos (and y or)
public Usuario findByLastnameAndFirstname(String lastname, String Firstname);
public Usuario findByLastnameOrFirstname(String lastname, String Firstname);
// Buscar según si un campo es mayor o menor que un valor, o que esté entre dos valores
public Usuario findByAgeLessThan(int age);
public Usuario findByAgeGreaterThan(int age);
public Usuario findByAgeBetween(int min, int max);
// Buscar si un campo empieza por
public Usuario findByFirstnameStartingWith(String startOfFirstName);
// Buscar por un campo y ordenar por otro
public Usuario findByAgeOrderByLastnameDesc(int age); |
Para utilizar nuestros JpaRepository es simple, podemos añadirlo a nuestras clases con @Autowired:
| Bloque de código | ||
|---|---|---|
| ||
@Autowired
private final UsuarioRepository usuarioRepo;
Usuario usuario = usuarioRepo.findByLogin("asdf"); |
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());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:
| Bloque de código | ||
|---|---|---|
| ||
int pageSize = 10;
// Iteramos 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 |
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. Vemos que hace referencia a las clases seguidas de un guión bajo, seguido de la propiedad en mayúsculas. Estas clases es lo que se llama metamodelo, y no es necesario implementarlas a mano, se pueden generar automáticamente teniendo esta dependencia en nuestro pom.xml:
| Bloque de código | ||
|---|---|---|
| ||
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<scope>provided</scope>
</dependency> |
...
Llamadas a métodos de base de datos
Tenemos que diferenciar entre funciones y procedimientos:
- Las funciones (Function) siempre devuelven un valor utilizando return. Aún así, se pueden especificar parámetros de salida, pero no son recomendables. Las funciones se pueden utilizar en instrucciones típicas como SELECT, INSERT, UPDATE, DELETE, MERGE, mientras que los procedimientos no. Las funciones se utilizan habitualmente para hacer cálculos.
- Los procedimientos (Procedure) pueden devolver valores o no, pero si se devuelve algo, se tiene que especificar con parámetros de salida. Los procedimientos se suelen utilizar para ejecutar lógica de negocio.
Funciones
Para llamar a funciones de base de datos, podemos hacerlo directamente desde un JPA Repository, indicándo una custom query con SELECT FUNCION(?1, ?2) FROM DUAL:
...
Hay funciones que desde la propia base de datos se especifica que no se puedan llamar de esta forma, con SELECT FROM DUAL, y en estos casos deberemos hacer la llamada con JdbcTemplate, que podemos ver en el último apartado de esta sección.
Procedimientos
Para llamar a procedimientos desde un JPA Repository, podemos utilizar la anotación @Procedure, de esta manera:
...
Como vemos, utilizamos @Param para indicar a qué parámetro del procedure corresponde cada parámetro de la función java, y se debe indicar el nombre definido en base de datos. En este caso, no se está devolviendo ningún valor. A falta de realizar más investigación y más pruebas en esta vía, podemos utilizar JdbcTemplate, que nos da más flexibilidad.
JdbcTemplate
Para llamar tanto a funciones como a procedimientos de base de datos, podemos utilizar JdbcTemplate. En primer lugar, declararemos la instancia en nuestra clase para el servicio REST:
...
En cuanto a la conexión que se crea, JdbcTemplate se encarga de la obtención y liberación de recursos, como dicha conexión. Por lo tanto, no hace falta cerrarla explícitamente, pues ya lo hace JdbcTemplate.