Mostrando entradas con la etiqueta JPA. Mostrar todas las entradas
Mostrando entradas con la etiqueta JPA. Mostrar todas las entradas

martes, 18 de enero de 2022

Spring Boot: Configuracion de multiples datasources

Hoy vamos a ver como configurar un proyecto de Spring Boot para el uso de varias fuentes de datos. Y hay mucha información en internet sobre este tema, en la mayoría de los casos no se indica un ejemplo completo, tal y como haremos aquí, donde además también indicaremos distintas opciones de configuración. 

Para el ejemplo hemos utilizado las siguientes librerías:

  • spring-boot-starter-parent: 2.5.7
  • spring-boot-starter-web
  • spring-boot-starter-data-jpa
  • mysql-connector-java
Y para simular las dos bases de datos hemos utilizado un docker compose, con tres instancias de MySQL con mismo esquema y tablas pero desplegadas en puertos distintos y con datos distintos.. 

Comenzaremos configurando los dataSources. Para ello tendremos que indicar las propiedades de la conexión en un fichero properties. Estas propiedades estarán unificadas con un mismo sufijo por cada uno de las BBDD que creemos. Por defecto las podemos crear en el fichero fichero  application.properties. 

## Datasource properties
app.datasource.pre.url=jdbc:mysql://localhost:3306/library?createDatabaseIfNotExist=true&autoReconnect=true&useSSL=false
app.datasource.pre.username=reader
app.datasource.pre.password=password
app.datasource.pre.driverClassName=com.mysql.cj.jdbc.Driver
app.datasource.pre.schema=library

#hikari configuration
app.datasource.pre.poolConf.maximum-pool-size=30

El siguiente paso será configurar dicho datasource de forma programática. La configuración la debemos realizar en una clase con la anotación @Configuration, para que los bean sean tenidos en cuenta durante la ejecución. Y teniendo en cuenta que uno de los dataSources debe ser configurado como principal a través de la anotación @Primary. Lo mismo deberemos hacer con los entity o transaction managers, que configuraremos posteriormente.  

La configuración del dataSource la podemos realizar de distintas formas. En el siguiente ejemplo podemos ver la opción más completa. Por un lado configuraremos las propiedades del dataSource y por otro lado configuraremos las del pool de conexiones. Todo lo haremos con ayuda de la anotación @ConfigurationProperties que nos permitirá indicar un sufijo de las propiedades que queremos usar. 

@Bean
@Primary
@ConfigurationProperties("app.datasource.pre.poolConf")
public DataSource preDataSource() {
    return preDataSourceProperties().initializeDataSourceBuilder()
        .type(HikariDataSource.class).build();
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.pre")
public DataSourceProperties preDataSourceProperties() {
    return new DataSourceProperties();
}

El pool de conexiones por defecto es HikariCP. Y el conjunto de propiedades que permite configurar las puedes ver en la documentación oficial, aquí. 

Podemos realizar una configuración aún más básica, sin indicar la configuración del pool y creando el DataSource en un único método. Pero en dicho caso la propiedad url deberíamos renombrarla a jdbcUrl, que es la propiedad que conoce HikariCP. En el caso anterior, el bean preDataSourceProperties se encarga de dicha conversión. 

@Bean
@ConfigurationProperties("app.datasource.pro")
public DataSource proDataSource() throws Exception {
	return DataSourceBuilder.create().build();
}

También podemos hacer una configuración igual a la anterior a nivel de código Java pero cambiando el origen del fichero de propiedades. Con la anotación @PropertySource podemos indicar otro origen del fichero de configuración. Esto nos ayudaría a implementar el twelve-factor app al indicar ficheros que se encuentren en el entorno y no en el classpath. 

Ahora configuraremos el entityManager necesario para el funcionamiento de nuestros objetos DAOs/Repository. Y el transactionManager que nos posibilitará gestionar las transacciones de forma automática. Esto lo podremos realizar en tres sencillos pasos:
  • Crear el bean del entityManager a partir de un Datasource
  • Crear el bean del transactionManager a partir del EntityManager
  • Incluir la anotación @EnableJpaRepositories a nivel de clase. 
En la creación del entityManager deberemos al menos configurar el dataSource asociado. Aparte de esto podremos dar más o menos detalle a la configuración del mismo. De forma opcional, podremos indicar a que entidades se debe aplicar el objeto a crear y que propiedades tendrá la implementación de JPA que utilicemos, en este caso Hibernate. 

@Bean
@Primary
public LocalContainerEntityManagerFactoryBean preEntityManager(
		final @Qualifier("preDataSource") DataSource preDataSource) {

	LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
	em.setDataSource(preDataSource);
	em.setPackagesToScan(new String[] { "es.home.example.knowledge.entity" });

	HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
	em.setJpaVendorAdapter(vendorAdapter);
	HashMap<String, Object> properties = new HashMap<>();
	properties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
	properties.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
	em.setJpaPropertyMap(properties);

	return em;
}

La configuración del paquete de las entidades a tener en cuenta por el objeto entityManager lo podemos omitir si indicamos en cada una de las clases a que esquema esta asociado a través de la anotación @Table. Y las propiedades especificas de Hibernate las podemos recoger con un objeto Environment, de cualquiera de los ficheros de propiedades que hayamos configurado.

Para el otro dataSource, si queremos realizar una configuración más básica, lo podríamos realizar de la siguiente forma. 

@Bean
public LocalContainerEntityManagerFactoryBean proEntityManager(
		final EntityManagerFactoryBuilder builder,
		final @Qualifier("proDataSource") DataSource proDataSource) {
	return builder.dataSource(proDataSource)
		.packages("es.home.example.knowledge.entity").build();
}

La configuración del transactionManager será más sencilla. Bastará con la siguiente declaración:

@Bean
@Primary
public PlatformTransactionManager preTransactionManager(
	final @Qualifier("preEntityManager") LocalContainerEntityManagerFactoryBean preEntityManager) {
    return new JpaTransactionManager(preEntityManager.getObject());
}

Y por último nos quedaría la anotación a nivel de clase @EnableJpaRepositories. Con esta anotación podremos indicar un conjunto de objetos DAOs que sean configurados con el entityManager y transactionManager que deseemos. Realizando la configuración de forma automática. 

@EnableJpaRepositories(basePackages = "es.home.example.knowledge.pre.repository", 
    entityManagerFactoryRef = "preEntityManager", 
    transactionManagerRef = "preTransactionManager")

Con esto ya habriamos acabado la configuración y podriamos obtener la información de distintas BBDD sin problemas. Por un lado creariamos las interfaces Repository para cada una de las entidades de cada uno de los dataSource.

package es.home.example.knowledge.pre.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import es.home.example.knowledge.entity.Book;

public interface PreBookDao extends JpaRepository<Book, Integer> {}

Y por otro lado las usaríamos en nuestro controlador:

@RestController
@RequestMapping(path = "/book")
public class BookRestController {
	@Autowired
	private PreBookDao preRepository;
	@Autowired
	private ProBookDao proRepository;
	@Autowired
	private DevBookDao devRepository;

	@GetMapping
	public List<Book> findAll() {
		List<Book> books = devRepository.findAll();
		books.addAll(preRepository.findAll());
books.addAll(proRepository.findAll());
return books;
} }

Si quieres ver todo el código lo tienes, como siempre, aquí. 

domingo, 22 de agosto de 2021

Tests unitarios y de integración para JPA DAO

Hoy vamos a ver principalmente como hacer pruebas unitarias y de integración para la capa de acceso a datos, DAO, en una aplicación de JPA. Pero también veremos otros aspectos interesantes sobre pruebas que impliquen BBDD, como  puede ser el uso de librerías para la creación de un contexto JNDI hasta incluso BBDD para pruebas. 

Para empezar debemos aclarar un poco que son los test unitarios y que son los test de integración, con la ayuda de estas versiones resumidas de su definición: 

  • Test Unitario: Es aquel destinado a probar un único componente del código. El cual se puede ejecutar sin necesidad de otras dependencias.
  • Test de Integración: Es aquel destinado a probar una funcionalidad compleja del código. Implicará  la iteración entre distintos componentes e inclusos con sistemas externos al código. 

Otro de los puntos a tener en cuenta, es que los test serán sobre tecnología de Jakarta EE y no con Spring. Por lo que serán un poco más elaborados. Además cuando queramos realizar los tests de integración diferenciaremos entre si queremos utilizar una conexión JDBC o acceder a la BBDD a través de JNDI. Esta última solución será aplicable a otros tipos de tests que necesiten del acceso a través de JNDI, de hay que hagamos un ejemplo diferente para probar lo mismo.

Empezaremos creando la clase Entity que será un reflejo de una tabla que tenemos en la BBDD. 

@Data
@Entity
@Table(name = "BOOK")
@NamedQuery(name = "Book.findAll", query = "SELECT b FROM Book b")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private String author;
}

El siguiente paso será crear nuestra clase de acceso a datos. Esta parte la realizaremos en dos pasos. Primero crearemos una clase abstracta y genérica que contenga los métodos comunes. Esta clase implementará una interfaz que es la que declarará los métodos que tiene. 

public abstract class GenericDaoImpl<K, E> implements GenericDao<K, E> {

    private Class<E> persistentClass;

    @SuppressWarnings("unchecked")
    public GenericDaoImpl() {
	Type genericSuperClass = getClass().getGenericSuperclass();

	ParameterizedType parametrizedType = null;
	while (parametrizedType == null) {
	    if (genericSuperClass instanceof ParameterizedType) {
		parametrizedType = (ParameterizedType) genericSuperClass;
	    } else {
		genericSuperClass = ((Class<?>) genericSuperClass).getGenericSuperclass();
	    }
	}
	this.persistentClass = (Class<E>) parametrizedType.getActualTypeArguments()[1];
    }
    @Override
    public List<E> findAll() {
	return getEntityManager().createNamedQuery(
			String.format("%s.findAll", persistentClass.getSimpleName()), 
		persistentClass).getResultList();
    }
    @Override
    public E getByField(final String field, final String value) {
	return getEntityManager()
.createQuery(
			String.format("SELECT t FROM %s t where t.%s = :value", 
			persistentClass.getSimpleName(), field), persistentClass)
		.setParameter("value", value).getResultStream().findFirst().orElse(null);
    }
    @Override
    public E getById(final K key) {
	return getEntityManager().find(persistentClass, key);
    }
}

A continuación crearemos la clase de acceso a datos asociada a nuestra entidad. La cual extenderá la clase anterior y además se le incluirá la dependencia de la clase EntityManager, la cual nos permitirá acceder propiamente a los datos. 

@Stateless
public class BookDaoImpl extends GenericDaoImpl<Integer, Book> {
    @PersistenceContext(unitName = "persistence-unit")
    protected EntityManager em;

    @Override
    public EntityManager getEntityManager() {
	return em;
    }
}

Esta clase será identificada como un componente sin estado a ser gestionado por el servidor a través de la anotación @Stateless, la cual también nos permitirá inyectarla en otras clases. Aparte le será inyectada la clase EntityManager, cuyo comportamiento es definido en el fichero META-INF/persistence.xml y que nos dará el acceso a la BBDD y la gestión de transacciones. 

Empecemos ya con los ejemplos. Primero irá el test unitario, donde no necesitaremos ninguna BBDD o proveedor de persistencia. Verificaremos el funcionamiento de los métodos a través de mocks. Por tanto, estas serán las únicas librerías que necesitemos: 

  • La especificación de Java EE: javax.javaee-api.
  • Librerías propias de Testing: Junit para los tests, Hamcrest para las verificaciones y Mockito para la simulación de objetos. 
@ExtendWith(MockitoExtension.class)
public class GenericDaoImplUnitTest {
    BookDaoImpl dao;
    @Mock
    EntityManager eManager;
    @Mock
    TypedQuery<Book> mockQuery;

    private List<Book> _getBooks() {
	List<Book> lst = new ArrayList<Book>();
	lst.add(new Book());
	return lst;
    }
    @Test
    public void findAll() {
	when(dao.em.createNamedQuery(ArgumentMatchers.anyString(), ArgumentMatchers.eq(Book.class)))
		.thenReturn(mockQuery);
	when(mockQuery.getResultList()).thenReturn(_getBooks());

	List<Book> lst = dao.findAll();
	assertEquals(1, lst.size());
	verify(dao.em).createNamedQuery("Book.findAll", Book.class);
    }
    @BeforeEach
    public void setUp() {
	dao = new BookDaoImpl();
	dao.em = eManager;
    }
}

Estos métodos verificarán el correcto funcionamiento. Pero no podremos testear el funcionamiento del propio EntityManager, su configuración, o en caso de consultas más complejas si están correctamente desarrolladas. Por todo ello realizaremos a continuación un ejemplo de test de integración, en cual nos conectaremos a una BBDD, probando de esta forma la configuración y funcionamiento real de las consultas. 

Para poder llevar a cabo este tipo de pruebas, necesitaremos las siguientes librerías:

  • Un proveedor de persistencia, es decir la implementación de la API de JPA. En este caso utilizaremos org.eclipse.persistence.eclipselink.
  • Una base de datos. Para nuestro ejemplo usaremos com.h2database.h2.
El punto a favor de utilizar una BBDD, que podemos almacenar en memoria, como H2, es que podremos crear un test hibrido entre prueba unitaria y prueba de integración. Esto es debido a que evita que tengamos conexión con un sistema real, lo cual a veces puede ser problemático por que no exista entorno de pruebas o conexión directa. Además, los tests mantienen su aislamiento y al utilizar un sistema externo sin estado, verificando así el mismo comportamiento en cada ejecución.

En este caso, al arrancar la BBDD queremos que sea poblada con determinada información que nos permita realizar los tests. Para ello le indicaremos en la URL JDBC que ejecute el fichero schema-generator.sql. Por lo demás la configuración es bastante estándar.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="persistence-unit-test" transaction-type="RESOURCE_LOCAL">
        <class>com.home.example.dbjunittest.entity.Book</class>
        <properties>
            <property name="javax.persistence.jdbc.url" 
           	value="jdbc:h2:mem:test:sample;DB_CLOSE_ON_EXIT=FALSE;INIT=RUNSCRIPT FROM 'classpath:schema-generator.sql';" />
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="javax.persistence.schema-generation.database.action" value="none" />
            
            <property name="eclipselink.logging.level.sql" value="FINE"/>
            <property name="eclipselink.logging.parameters" value="true"/>
</properties> </persistence-unit> </persistence>

Y a continuación elaboraremos nuestro test de integración que hará uso de esta configuración.

public class BookDaoImplITTest {
    private static BookDaoImpl dao;

    @BeforeAll
    public static void before() {
	dao = new BookDaoImpl();
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence-unit-test");
	dao.em = emf.createEntityManager();
    }
    @Test
    public void findAll() {
	List<Book> lst = dao.findAll();
	assertThat(lst.size(), equalTo(1));
    }
}

Ahora veremos una modificación de este último ejemplo, utilizando en vez de una conexión JDBC, una conexión a través de JNDI. Pero para ello tendremos que añadir una nueva librería: com.github.h-thurow.simple-jndi. Esta librería nos permitirá poblar al objeto InitialContext con un Datasource que nosotros indiquemos, para que así pueda ser utilizado por el Entity Manager. 

El primer paso en este nuevo ejemplo es modificar el fichero persistence.xml, para indicar que dejamos de utilizar una conexión JDBC y queremos usar una JNDI. 

<persistence-unit name="persistence-unit-test" transaction-type="RESOURCE_LOCAL">
	<non-jta-data-source>java:comp/env/jdbc/myds</non-jta-data-source>
    <class>com.home.example.jpajndiinttest.entity.Book</class>
    <properties>
        <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
        <property name="javax.persistence.schema-generation.database.action" value="none" />
        
        <property name="eclipselink.logging.level.sql" value="FINE"/>
		<property name="eclipselink.logging.parameters" value="true"/>
    </properties>
</persistence-unit>

Para que la carga del Datasource en el contexto funcione correctamente debemos configurar dos ficheros. Uno será el encargado de configurar la librería que hemos indicado a través de un fichero denominado jndi.properties. En el aparte de configuración propia de la librería, indicaremos los caracteres de separación para la nomenclatura, el espacio de almacenaje de dicha información, y donde se ubicarán los ficheros para los distintos objetos a crear.  

java.naming.factory.initial=org.osjava.sj.SimpleContextFactory
org.osjava.sj.jndi.shared=true
org.osjava.sj.delimiter=.
jndi.syntax.separator=/
org.osjava.sj.space=java:comp/env
org.osjava.sj.root=src/test/resources/jndi

El segundo fichero, será el que configure el Datasource y en base a lo indicado anteriormente debe cumplir las siguientes reglas y contenido:

  • Debe estar almacenado en la carpeta src/test/resources/jndi
  • Debe llamarse jdbc.properties
  • Cada una de las propiedades del Datasource deben tener el sufijo myds. 

myds.type=javax.sql.DataSource
myds.driver=org.h2.Driver
myds.url=jdbc:h2:mem:test:sample;DB_CLOSE_ON_EXIT=FALSE;INIT=RUNSCRIPT FROM 'classpath:schema-generator.sql';
myds.user=sa
myds.password=password

Ya solo nos quedará realizar el test de la siguiente forma:

public class BookDaoImplIT {

    static BookDaoImpl dao;
    static InitialContext initContext;

    @BeforeAll
    public static void setup() throws Exception {
	initContext = new InitialContext();
	dao = new BookDaoImpl();
	EntityManagerFactory emf = Persistence.createEntityManagerFactory("persistence-unit-test");
	dao.em = emf.createEntityManager();
    }
    @Test
    public void findAll() throws InterruptedException {
	List<Book> lst = dao.findAll();
	assertThat(lst.size(), equalTo(1));
    }
}

Debemos tener en cuenta que ambos ejemplos no gestionan las transacciones al ser de tipo local. Por lo que si queremos hacer ejemplos más complejos, deberemos gestionar las transacciones manualmente. Esto es debido a que si utilizamos conexiones JTA, la clase EntityManagerFactory no funcionará. 

Indicar también que si queremos ejecutar los este tipo en una fase posterior o de forma independiente, también lo podemos hacer. Para ello tendremos que utilizar maven-failsafe-plugin. Y deberemos seguir las siguientes instrucciones:

  • El test debe seguir el siguiente patrón: "**/IT*.java, **/*IT.java o **/*ITCase.java
  • Debemos asociar su ejecución a una de las fases del ciclo de vida de Maven.

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-failsafe-plugin</artifactId>
	<version>2.22.2</version>
	<executions>
		<execution>
			<goals>
				<goal>integration-test</goal>
			</goals>
		</execution>
	</executions>
</plugin>

Como veis no solo hemos aprendido a hacer tests a la capa de acceso a datos sino que por el camino hemos aprendido un par detalles interesantes. Además, en el enlace al código fuente, podréis ver un test de cada uno los métodos para los distintos ejemplos, aquí. 

viernes, 11 de septiembre de 2020

Apache Camel: CRUD con JPA

 Ya hemos visto anteriormente otros posts de Apache Camel. Hoy veremos como hacer las operaciones de BBDD con JPA.

Como siempre empezaremos por la configuración. En cuanto a librerías añadimos las siguientes:

  • El componente camel-jpa-starter de Apache Camel que nos permitirá usar el componente JPA. 
  • La librería hibernate-entitymanager que nos permitirá utilizar las anotaciones de hibernate.
Debemos recordar un poco cual es el fundamento de JPA y es la cualidad de poder manejar datos relacionales como objetos Java. Permitiéndonos manejar más fácilmente dentro de clases Java la información de una BBDD. Estas clases serán las denominadas Entidades. Y como parte de la API de JPA se encuentra JPQL, un lenguaje que nos permitirá realizar consultas sobre esas entidades. 

Gracias a Spring boot y los starter no tendremos que realizar mucha configuración, pero a nivel de la clase Java deberemos añadir una cuantas anotaciones:

@Entity(name = "BOOK")
@Table(name = "BOOK")
@NamedQuery(name = "findAll", query = "SELECT b FROM BOOK b")
public class Book {

    @JsonProperty
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @JsonProperty
    private String name;
    @JsonProperty
    private String author;
}

@Entity nos permitirá indicar que es una clase a ser gestionada a través de JPA. Con @Id indicaremos que cual es su primary key y con @GeneratedValue se gestionará su generación, en el caso de MySQL será Auto. 

En el primer ejemplo que veremos será muy similar al del componente SQL solo que usaremos JPQL y debemos indicar cual es la entidad sobre la que vamos a realizar la consulta. 

rest().get("book/{id}").description("Details of an book by id")
   .outType(Book.class).produces(MediaType.APPLICATION_JSON_VALUE).route()
   .toD("jpa://" + Book.class.getName() + "?query=select b from " + Book.class.getName()+ " b where b.id= ${header.id}", 5)
   .log("--- select a book ${body} ---");

Lo primero que tenemos que hacer es indicar cual es la entidad sobre la cual vamos a trabajar. Luego si queremos hacer una consulta JPQL debemos añadir el parámetro 'query'. 

Además en este caso concreto queremos obtener un libro en base a un identificador que viene como parámetro de la request, a través del lenguaje de expresión. Para poder conseguir esto utilizaremos el método ToD en vez de To. El cual nos permitirá enrutar de forma dinámica el flujo y que sea interpretado el lenguaje de expresión.

Otra de las cualidades de JPA son las namedQuery, las cuales nos permiten definir consultas JPQL a nivel de la clase. En el siguiente ejemplo veremos como utilizar las namedQueries a través del componente JPA. La query a utilizar es findAll.

rest().get("book").produces(MediaType.APPLICATION_JSON_VALUE).route()
      .to("jpa://"+ Book.class.getName() + "?resultClass="+ Book.class.getName() + "&namedQuery=findAll")
      .log("---select all books---");

En este segundo ejemplo podemos ver la diferencia de usar el método outType para indicar el tipo de objeto a la salida. O si por lo contrario podemos indicarlo a través del query parameter resultClass del propio componente JPA. 

También podemos hacer operaciones que nos permitan guardar o persistir dichos objetos en la BBDD. Como podemos ver en el ejemplo, en el caso de almacenar el valor, el funcionamiento es muy sencillo. Solo debemos indicar el objeto a persistir y utilizar el query parameter usePersist para indicar que debe almacenar el objeto. Por supuesto, en el flujo deben ir los datos del objeto a guardar. 

rest().post("book").produces(MediaType.APPLICATION_JSON_VALUE).type(Book.class)
      .route().routeId("postBookRoute").log("--- binded ${body} ---")
      .to("jpa:" + Book.class.getName() + "?usePersist=true")
      .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(201)).setBody(constant(null));

Para el caso de las actualizaciones y borrado de datos, necesitamos utilizar el query parameter useExecuteUpdate. En este ejemplo haremos uso del método process para crear el objeto a actualizar a partir de los datos de la cabecera y el cuerpo del mensaje. 

rest().put("book/{id}").produces(MediaType.APPLICATION_JSON_VALUE).type(Book.class).route().choice()
      .when().simple("${header.id} < 1").bean(BookRouter.class, "negativeId").otherwise()
      .process(new Processor() {
          @Override
          public void process(final Exchange exchange) {
              Book book = exchange.getIn().getBody(Book.class);
              Integer id = Integer.valueOf(exchange.getIn().getHeader("id").toString());
              book.setId(id);
              exchange.getIn().setBody(book, Book.class);
          }
      }).to("jpa:" + Book.class.getName() + "?useExecuteUpdate=true")
      .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200)).setBody(constant(null));

Por último veremos el ejemplo asociado al borrado de datos. Y de paso veremos como realizar consultas nativas. Es decir utilizaremos lenguaje SQL común y no JPQL. 

rest().delete("book/{id}").produces(MediaType.APPLICATION_JSON_VALUE).route()
      .toD("jpa:com.example.home.ApacheCamelRestExample.pojo.Book"
            + "?nativeQuery=DELETE FROM library.BOOK where id = ${header.id}&useExecuteUpdate=true")
      .setHeader(Exchange.CONTENT_TYPE, constant(MediaType.APPLICATION_JSON_VALUE))
      .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200)).setBody(constant(null));

Como veis algunas consultas pueden ser algo más difíciles que usar simple SQL. Pero también otras se simplifican muchísimo y siempre estamos trabajando con objetos. Lo cual, al programar en un lenguaje orientado a objetos es muy de agradecer. 

jueves, 18 de octubre de 2018

Como crear e inicializar con datos una BBDD de H2

Estaba haciendo un ejemplo de JPA, Hibernate y H2. Y estaba intentando que al arrancar el ejemplo se crease la BBDD en base a las entidades JPA y a la misma vez ejecutase un script para añadir datos a la misma. 

Mi configuración inicial era la siguiente: 

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
 version="2.0">
 <persistence-unit name="es.home.example.jpa.config">
  <class>es.home.example.pojo.Book</class>
  <properties>
   <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
   <property name="hibernate.show_sql" value="true" />
   <property name="hibernate.hbm2ddl.auto" value="create" />
   <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
   <property name="javax.persistence.jdbc.url"
    value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;
    INIT=create schema if not exists test\;
    RUNSCRIPT FROM 'classpath:script/data.sql'\;" />
   <property name="javax.persistence.jdbc.user" value="sa" />
   <property name="javax.persistence.jdbc.password" value="" />
  </properties>
 </persistence-unit>
</persistence>

Estaba usando la propiedad 'hibernate.hbm2ddl.auto' a 'true', por lo que me crearía la BBDD al iniciarla en base a las entidades creadas (en este caso la entidad 'Book'). Y además en la cadena de conexión de H2 habiamos indicado que al iniciar se crease el esquema 'test' si no existiese y que se ejecutase un script (data.sql) almacenado en la carpeta 'script' del classpath (src/main/resources).

Pero al arrancar la aplicación, me daba error debido a que estaba intentando ejecutar el script 'data.sql' (con inserts en la tabla 'Book') antes de crear la propia tabla.

La solución: Pasa por hacer ambas cosas a través de hibernate. Es decir, que sea la propia Hibernate la que se encargue de crear la BBDD (cosa que ya tenemos configurada) y la encargada de ejecutar un fichero que nos permita iniciar con datos dicha BBDD. ¿Pero como?.

Podemos hacerlo de dos formas, pero en ambas partimos de que debemos quitar la parte de la configuración de H2 para ejecutar el script.
  • La primera es creando un fichero 'import.sql' en el classpath y almacenando en el todas las sentencias SQL que queremos que se ejecuten.
  • Indicando los scripts y el orden de los mismos que queremos que se ejecuten mediante otra propiedad de hibernate 'hibernate.hbm2ddl.import_files'. Antentos que esta solo se puede usar si la propiedad 'hibernate.hbm2ddl.auto' esta puesta a 'create' o 'create-drop'
He aquí la solución:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
 version="2.0">
 <persistence-unit name="es.home.example.jpa.config">
  <class>es.home.example.pojo.Book</class>
  <properties>
   <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
   <property name="hibernate.show_sql" value="true" />
   <property name="hibernate.hbm2ddl.auto" value="create" />
   <property name="hibernate.hbm2ddl.import_files" value="script/data.sql" />   
   <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
   <property name="javax.persistence.jdbc.url"
    value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;" />
   <property name="javax.persistence.jdbc.user" value="sa" />
   <property name="javax.persistence.jdbc.password" value="" />
  </properties>
 </persistence-unit>
</persistence>

domingo, 27 de agosto de 2017

Web Service con Spring Boot, JPA y H2

Bueno, vamos a dar un paso más en el ejemplo de los web services con spring boot. Ya los hicimos básico, ahora lo haremos obteniendo los datos de BBDD. Y además no de una BBDD relacional corriente, sino H2 que será almacenada en memoria.

Empezamos con la configuración básica y es que debemos añadir las librerias necesarias para el uso de JPA y H2.

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
 <groupId>com.h2database</groupId>
 <artifactId>h2</artifactId>
</dependency>

Para este ejemplo añadiremos una pequeña configuración extra, la parte de la configuración a BBDD la realizaremos directamente sobre el fichero 'application.properties'. Sin necesidad de crear un fichero 'persistence.xml'. La parte más importante es la correspondiente a la propiedad 'javax.persistence.sql-load-script-source', que nos permite indicar un fichero SQL que ejecutar al levantar la BBDD.


## DATASOURCE PROPERTIES

spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.platform=org.hibernate.dialect.H2Dialect

## JPA PROPERTIES

spring.jpa.show-sql=true
#spring.jpa.database=MYSQL
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.javax.persistence.sharedCache.mode=DISABLE_SELECTIVE
spring.jpa.properties.javax.persistence.schema-generation.database.action=drop-and-create
spring.jpa.properties.javax.persistence.sql-load-script-source=script/data.sql

Ahora crearemos las clases JPA que serán el reflejo de las entidades de BBDD. Para nuestro ejemplo crearemos una clase 'Book' que contendrá información básica de libros y una clase 'GenericEntity' que nos servirá para aglutinar aquellos atributos que serán iguales en todas las futuras entidades, como son el campo clave y los campos de auditoría. Todo ello gracias a la anotación 'MappedSuperclass'. Sobre como hacer la auditoría ya hicimos un post anteriormente, aquí.

El siguiente paso es crear el DAO o Repository que nos permita acceder a los datos y/o modificarlos. Para ello crearemos una clase 'BookDao' y 'GenericDao'. De esta forma tendremos clases de acceso a datos con un conjunto de métodos por defecto. La anotación '@NoRepositoryBean' indicará a Spring que no cree un proxy de acceso a datos para la interfaz, cosa que si hará con 'BookDao'.

@NoRepositoryBean
public interface GenericDao<E extends GenericEntity<K>, K extends Serializable>
  extends JpaRepository<E, K> { }
....
public interface BookDao extends GenericDao<Book, Integer> { }

Por último nos queda crear el controlador


@RestController("/book")
public class BookRestController {
 @Autowired
 private BookDao repository;
 @GetMapping
 public List<Book> findAll() {
  return repository.findAll();
 }
 @GetMapping("/{id}")
 public Book findById(@PathVariable("id") final Integer id) {
  return repository.findOne(id);
 }
}

Si arrancamos la aplicación podremos acceder a los datos por ejemplo con la siguiente consulta: http://localhost:8082/book/3