Mostrando entradas con la etiqueta MYSQL. Mostrar todas las entradas
Mostrando entradas con la etiqueta MYSQL. 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í. 

miércoles, 15 de agosto de 2018

Flyway: Como migrar la base de datos

Hoy vamos a hablar de Flyway, un software que nos facilitará la migración de bases de datos. Sobre todo es util para la migración de los esquemas a lo largo de los distintos entornos de prueba, preproducción y producción. Ya que es facil que en el pase de un desarrollo de un entorno a otro podamos fallar y/o olvidar ejecutar algún script necesario.
  • Funcionamiento
Su funcionamiento se basa en la creación de un tabla de gestión que almacenará la información de los distintos script de migración que contenga la aplicación. En función de estos scripts, su orden de ejecución y la tabla de gestión, tendremos la hoja de ruta para poder pasar una base de datos de un entorno a otro. 
  • Ejecución
Flyway, al comenzar buscará la tabla de ejecución, denominada 'flyway_schema_history, y en caso de no encontrala la creará. Posteriormente, buscará los scripts en formato .sql o .java en una ruta configurable y comprobará si se han ejecutado ya o no, en caso negativo los ejecutará y almacenará la información asociada a dichos scripts en la tabla de gestión.

Dentro de esta información asociada se encuentra el nombre del fichero, la versión o el checksum del mismo. Al contener el checksum, en caso de que modifiquemos el fichero, flyway se daría cuenta de que la versión actual es diferente a la ejecutada y lanzaría una excepción. Por tanto si queremos modificar un script ya ejecutado, es obligatorio crear un nuevo script ya que no es modificable el anterior.

A tener en cuenta respecto a lo indicado anteriormente, que ejecutará el script siempre y cuando no se haya ejecutado anteriormente tal como hemos dicho y la versión del mismo sea posterior a la última versión registrada en la tabla de gestión.  
  • Configuración
Vamos a ver un ejemplo con gradle donde podemos ver como funciona. Lo configuramos indicando que vamos a usar el plugin de flyway y creando las variables de configuración necesarias.

plugins {
    id "org.flywaydb.flyway" version "5.1.4"
}
flyway {
    url = 'jdbc:mysql://localhost:3306/'
    user = 'root'
    password = 'root'
    schemas = ['flywayExample']
}

Lo siguiente será crear los ficheros tipo 'v${number}__${script_name}.sql' dentro de la carpeta 'src/main/resources/db/migration', que son el formato y ubicación por defecto.

Por último ejecutarlo a través del comando './gradlew flywayMigrate'. Y ya podremos acceder a la base de datos y ver como se han ejecutado los scripts. Aquí teneis un ejemplo que podeis usar para comprobar su funcionamente, como base de datos usa un docker de MySQL.

sábado, 29 de abril de 2017

Mejorar el rendimiento en una búsqueda de texto con SQL

Normalmente cuando queremos comparar texto en una sentencia SQL usamos el LIKE y los porcentajes para comparar parte del texto del campo. Además cuando usamos caracteres latinos, solemos hacer una pequeña operación de traducción de texto. Ya indicamos en su día que lo correcto sería crear una función para entre otras cosas hacer demasiado engorroso la consulta, aquí el post. Un ejemplo de este tipo de consulta sería:

SELECT c.ID AS ID,c.NAME AS NAME
FROM CARS c
JOIN MODELS m ON c.ID = m.CAR
WHERE (
REPLACE(UPPER(TRIM(c.DESCRIPTION)),'ÁÀÂÄÉÈÊËÍÌÎÏÓÒÔÖÚÙÛÜÑÇ', 'AAAAEEEEIIIIOOOOUUUUNC') LIKE REPLACE(UPPER(TRIM('%CACTUS%')),'ÁÀÂÄÉÈÊËÍÌÎÏÓÒÔÖÚÙÛÜÑÇ','AAAAEEEEIIIIOOOOUUUUNC')
OR REPLACE(UPPER(TRIM(m.DESCRIPTION)),'ÁÀÂÄÉÈÊËÍÌÎÏÓÒÔÖÚÙÛÜÑÇ', 'AAAAEEEEIIIIOOOOUUUUNC') LIKE REPLACE(UPPER(TRIM('%CACTUS%')),'ÁÀÂÄÉÈÊËÍÌÎÏÓÒÔÖÚÙÛÜÑÇ','AAAAEEEEIIIIOOOOUUUUNC')
)

Suponiendo que el campo 'DESCRIPTION' en ambas tablas sea campos destinados a guardar mucha información, las búsquedas se pueden hacer muy pesadas. Sobretodo si se estas búsquedas tienen mucha repeticiones. Para poder realizar búsquedas de textos en campos amplios de SQL necesitamos del uso combinado de los indices 'FULLTEXT' y la función 'MATCH/AGAINST'.

La función 'MATCH/AGAINST' te permite realizar las búsquedas de texto de forma más optimizada pero solo se puede usar con campos que tienen un indice FULLTEXT. Estos indices se pueden asociar a cualquier campo SQL que almacene caracteres, pero lo normal es que se use para campos tipo TEXT que son los que tienen la dificultad de las búsquedas. Por lo que el primer paso sería modificar las tablas para crear dichos indices.


ALTER TABLE 'CARS'
ADD FULLTEXT INDEX 'IDX_CARS_TEXT' ('DESCRIPTION');

ALTER TABLE 'MODELS'
ADD FULLTEXT INDEX 'IDX_MODELS_TEXT' ('DESCRIPTION');

Una vez creados ya podremos usar la funcion 'MATCH/AGAINST'. Esta función al menos existen en MYSQL y SQL Server. Y en ejemplo veremos la versión de MYSQL, donde en 'AGAINST' ademas indicamos el módo de búsqueda del texto que deseamos buscar. 'IN NATURAL LANGUAGE MODE' permite buscar la cadena de texto dentro del texto completo que contiene dicho campo. 


SELECT DISTINCT c.ID AS ID, c.NAME as NAME
FROM CARS c
JOIN MODELS m ON c.ID = m.CAR
WHERE (
MATCH(c.DESCRIPTION) AGAINST ( 'CACTUS' IN NATURAL LANGUAGE MODE)
OR MATCH(m.DESCRIPTION) AGAINST ( 'CACTUS' IN NATURAL LANGUAGE MODE)
)


Como veis, la búsqueda se ha vuelto mucho más básica, más rápida y además no se ve afectada por los caracteres latinos o mayúsculas. 

sábado, 1 de octubre de 2016

Como hacer que Spring Data detecte correctamente el nombre de la tabla de MySQL con Spring Boot


En un principio, la anotación JPA 'Table' te permite indicar a que tabla de BBDD esta asociado una clase concreta. Pero al menos en MySQL puede haber casos en los que la traducción no sea correcta. Como en el siguiente caso.

Tenemos una tabla en MySQL con su nombre completo en mayúsculas (BOOK) y así  lo indicamos con las anotaciones:

@Entity
@Table(name = "BOOK", catalog = "mySchema")
public class Book implements java.io.Serializable {

Pero al intentar acceder a los datos de la tabla, ocurre el siguiente error:

com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'mySchema.book' doesn't exist

Como vemos, parece que el nombre de la tabla lo pasa a minúsculas cuando le indicamos lo contrario. Y todo es debido a la estrategia de nombres establecida por JPA. Por tanto, para arreglar nuestro problema debemos modificar dicha estrategia. Y así es como lo haremos en una aplicación de Spring Boot:

spring.jpa.hibernate.naming_strategy=org.hibernate.cfg.EJB3NamingStrategy
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl 
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

 Como vemos hemos indicado que utilice la clase 'EJB3NamingStrategy' para manejar los nombre de las tablas. Spring Boot por defecto utiliza 'SpringNamingStrategy' que extiende de 'ImprovedNamingStrategy'. Mientras que la estrategia de Spring manipula el nombre de la tabla según sus propias reglas, con la estrategia de EJB3 el nombre se quedará tal y como lo indicamos. 

sábado, 13 de agosto de 2016

Spring Boot Security: nivel básico

En este post, vamos a intentar explicar como configurar de forma básica Spring Security 4 con Spring Boot. Accediendo a los roles a través de JDBC y con los passwords encriptados. Se puede configurar en 4 sencillas clases y tenéis todo el código aquí (ExampleSpringSecurity) (en src/test/resources teneis incluidos los scripts BBDD).

1. Application: Esta clase tendrá la anotación 'SpringBootApplication'  y un método main que nos permitirá arrancar la aplicación.
2. MvcConfig: En esta clase tendremos la configuración para la navegación url-pantallas/vistas. Y también indicaremos a través de 'viewResolver' como resolver las  el viewResolver, que nos permitirá indicar la pantalla/vista.


@Override
public void addViewControllers(final ViewControllerRegistry registry) {
 registry.addViewController("/home").setViewName("home");
 registry.addViewController("/").setViewName("home");
 registry.addViewController("/hello").setViewName("hello");
 registry.addViewController("/login").setViewName("login");
 registry.addViewController("/403").setViewName("403");
}
@Bean
public InternalResourceViewResolver viewResolver() {
 InternalResourceViewResolver resolver = new InternalResourceViewResolver();
 resolver.setPrefix("/WEB-INF/jsp/");
 resolver.setSuffix(".jsp");
 return resolver;
}

En el código podemos ver que si indicamos una vista denominada 'hello', buscará y redireccionará a la pantalla /WEB-INF/jsp/hello.jsp'.

3. AuthenticationProviderConfig: En esta clase declararemos el 'dataSource' que nos permitirá conectarnos a la BBDD. Y lo mas importante el bean 'userDetailService', en el cual implementaremos como obtener el usuario y contraseña almacenados en BBDD y como obtener los roles asociados al usuario.

@Bean(name = "userDetailsService")
public UserDetailsService userDetailsService() {
 JdbcDaoImpl jdbcImpl = new JdbcDaoImpl();
 jdbcImpl.setDataSource(dataSource());
 String usersByUserNameQuery = "select username,password, enabled from users where username=?";
 String authoritiesByUsernameQuery = "select u.username, ur.authority from user_roles ur, users u where username=? and u.user_id = ur.user_id";
 jdbcImpl.setUsersByUsernameQuery(usersByUserNameQuery);
 jdbcImpl.setAuthoritiesByUsernameQuery(authoritiesByUsernameQuery);
 return jdbcImpl;
}

4. WebSecurityConfig: En esta clase realizaremos la configuración propia de Spring Security, en el aspecto de que indicamos como se va a realizar la obtención de los datos (a través de userDetailsService). Como vamos a realizar la encriptación de la contraseña (a través de una clase incluida dentro de Spring como es: BCryptPasswordEncoder). Y por último configurar todas las urls de acceso de la aplicación e indicar si son accesible por todos los usuarios, si solo son accesible por un determinado rol y configuraciones normales como cuales son las paginas de login y logout.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 @Autowired
 UserDetailsService userDetailsService;
 @Bean(name = "passwordEncoder")
 public PasswordEncoder passwordencoder() {
  return new BCryptPasswordEncoder();
 }
 @Autowired
 public void configAuthentication(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordencoder());
 }
 @Override
 protected void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/hello").access("hasRole('ROLE_ADMIN')").anyRequest()
    .permitAll()
    .and().formLogin().loginPage("/login").usernameParameter("username")
    .passwordParameter("password")
    .and().logout().logoutSuccessUrl("/login?logout")
    .and().exceptionHandling().accessDeniedPage("/403")
    .and().csrf();
 }
}

Toda la configuración se puede crear en un único fichero sin problemas. Pero la idea de realizarlo así es aumentar la claridad en la configuración de la aplicación.

Este ejemplo lo he sacado de esta Web, donde además podemos ver un post algo más complejo con JPA Data.

miércoles, 16 de marzo de 2016

Llamar a una función con parámetro de salida y JDBC

Hoy vamos ha hacer algo sencillo. Como obtener el valor de retorno de una función mediante una básica llamada JDBC.

Para empezar creamos una función básica que nos devuela algo:

CREATE FUNCTION hello (s CHAR(20)) RETURNS CHAR(50)
RETURN CONCAT('Hello, ',s,'!');

La forma de obtener el parámetro de salida es muy sencillo. Lo más importante es saber como indicar que vamos a obtener un parámetro de salida y como registrarlo a través del 'CallableStatement'. Por lo demás es una operación JDBC normal. Aquí un ejemplo:

Connection conn = DriverManager.getConnection(${JDBC_URL}, ${BBDD_USER}, ${BBDD_PWD});
String procedure = "{? = call hello (?)}";
CallableStatement statement = conn.prepareCall(procedure);
statement.registerOutParameter(1, java.sql.Types.VARCHAR);
statement.setString(2, "WORLD");
statement.execute();
String salida = statement.getString(1);

En la variable 'salida' tendremos la salida de la función con el valor 'hello WORLD!'.

Si la variable de salida tiene un nombre, podemos utilizar esta otra llamada para registrar su valor.

statement.registerOutParameter(1, java.sql.Types.VARCHAR, ${var_name});

lunes, 15 de abril de 2013

SQLException: Field 'xxxx' doesn't have a default value

Haciendo uso de JPA e intentando actualizar objeto con una relacion OneToMany tal y como comentaba en el post anterior, me he encontrado con el error que reza en el titulo de este.

Y es que parece que aunque todo esta bien configurado no funciona, y me indica que el campo a añadir requiere de algún valor que no sea nulo.

Expliquemos el modelo de la base de datos para dar a entender el problema. Tenemos una tabla Padre y una tabla Hijo, en la cual hay una Foreign Key a la tabla Padre. Esa FK esta indicada como Not Null, como es normal para evitar que se introduzca ningún registro sin referencia a la tabla Padre.

El asunto es que ahí se encuentra el fallo, y hay que modificar el modelo de base de datos y no indicar la FK como Not Null permitiendo incluir un registro sin referencia a la tabla Padre y que posteriormente se encargue JPA de actualizar dicho valor en otra sentencia asociada a la transacción