Estás viendo una versión antigua de esta página. Ve a la versión actual.

Comparar con el actual Ver el historial de la página

« Anterior Versión 7 Siguiente »

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.

Para poder utilizar JpaRepository es necesario incluir las siguientes dependencias en el pom.xml:

<dependency>
<!-- JPA -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</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:

spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.Oracle10gDialect
spring.jpa.generate-ddl = false


# Datasource
spring.datasource.url=jdbc:oracle:thin:@hydra-prescan.atica.um.es:1526/ZEUSDESA
spring.datasource.username=USUARIOBD
spring.datasource.password=CONTRASEÑABD

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:

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 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í:

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í:

@SequenceGenerator(name = "comunicacionID", sequenceName = "PORTALFUNDEWEB.SEQ_COMUNICACIONES", allocationSize = 1)

Después, dentro de la clase, tendremos que poner la etiqueta @GeneratedValue al getter corresponiente:

@GeneratedValue(generator = "comunicacionID")

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:

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:

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:

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:

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:

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:

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í:

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:

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

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <scope>provided</scope>
</dependency>


Para información más detallada, podemos consultar la documentación oficial de Spring.

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:

@Query(nativeQuery = true, value = "SELECT paquete.nombre_funcion(?1, ?2, ?3) FROM dual" )
String funcionJava(String param1, int param2, String param3)

En este caso, especificamos con ? y un número (empezando por 1) los parámetros que corresponderán a los parámetros de la función Java, que se mapean en orden de aparición.

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:

@Procedure( "paquete.nombre_procedure" )
void funcionJava(@Param("curso") String curso, @Param("alumno") String alumno );

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:

import org.springframework.jdbc.core.JdbcTemplate;

@Autowired
private JdbcTemplate jdbcTemplate;

Un ejemplo simple de cómo se puede utilizar podría ser este:

jdbcTemplate.execute( new CallableStatementCreator() {

	@Override
	public CallableStatement createCallableStatement( Connection con ) throws SQLException {
		// Definimos la llamada, dejando con ? lo que serán parámetros, tanto de salida como de entrada
		final CallableStatement cs = con.prepareCall( "{ ? = call PAQUETE_BD.METODO(?, ?) }" );

		// Registramos los parámetros de salida
		cs.registerOutParameter( 1, Types.VARCHAR );

		// Asignamos los parámetros de entrada
		cs.setInt( 2, id );
		cs.setString( 2, texto );

		return cs;
	}
}, new CallableStatementCallback<String>() {

	@Override
	public String doInCallableStatement( CallableStatement cs ) throws SQLException {
		// Ejecutamos la llamada y devolvemos el valor correspondiente
		cs.execute();
		try {
			// Devolvemos el string correspondiente al primer parámetro
			return cs.getString( 1 );
		} catch ( final Exception e ) {
			return null;
		}
	}
} );

Podemos ver cómo se pone la llamada, especificando con ? cada uno de los parámetros, que pueden ser tanto de entrada  como de salida. En este caso, se llama a una función, y vemos cómo se registra el primer parámetro como salida con su tipo correspondiente, y en los dos siguientes se les indica su valor.

En la segunda parte del código, donde se define new CallableStatementCallback debemos poner el tipo de retorno, tanto entre <> como en el tipo de retorno de la función doInCallableStatement. Dentro ejecutamos la llamada y devolvemos el valor correspondiente. 

Hay que notar que todo esto es la instrucción jdbcTemplate.execute, que está devolviendo un String, en este caso. Aunque el código sea largo, esto es una función que, como cualquier otra, podemos asignar el valor de retorno a una variable.

Con esto no sólo podemos ejecutar una función o un procedimiento, podemos indicar bloques del tipo DECLARE ... BEGIN ... END que contengan varias instrucciones, especificándolo como un String en con.prepareCall.

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.

  • Sin etiquetas