Quantcast
Channel: Planeta Código
Viewing all 2713 articles
Browse latest View live

Blog Bitix: El patrón Open Session in View, qué es, ventajas, problemas y alternativas

$
0
0

En patrón Open Session in View mantiene abierta durante toda la petición a un servidor la conexión a la base de datos. Esto tiene la ventaja de que en cualquier momento es posible recuperar datos de la base de datos, incluso desde las vistas pero tiene inconvenientes ya que las conexiones a la base de datos son un recurso escaso. Si además durante la petición se hacen peticiones a otros servicios que añaden tiempo de procesamiento la aplicación es posible que tenga problemas de escalabilidad con muchos usuarios y peticiones durante un corto periodo de tiempo.

Java

La librería Hibernate proporciona persistencia del modelo de objetos del lenguaje Java al modelo de las bases de datos relacionales de una forma sin que el programador necesite lanzar las consultas SQL directamente por lo general. Con Hibernate los objetos de las relaciones se cargan de los datos de las filas de la base de datos cuando son solicitadas si la relación es lazy o al obtener el objeto en el modo eager.

El modo lazy tiene la ventaja de que los datos de las relaciones solo se cargan si se necesitan pero tiene el inconveniente de producir más SQLs a la base de datos. El modelo eager carga los datos con menos SQLs pero carga más datos de los necesarios si no se necesitan.

Para que el modo lazy funcione se ha de mantener la conexión a la base de datos abierta para cargar los datos cuando se soliciten. Mantener la sesión y conexión de base de datos abierta es lo que define el patrón Open Session in View. Sin embargo, mantener la conexión abierta durante toda la petición incluida la parte de generación de la vista tiene inconvenientes, incluso llegando a considerar el patrón Open Session in View un antipatrón que no se de debe usar.

Qué es y como funciona

En este diagrama se aprecia su funcionamiento. La primera acción en una petición es abrir una sesión para obtener datos de la base de datos, lo que se traduce en apropiarse de una conexión a la base de datos. El flujo del programa procesa la petición invocando la lógica de la aplicación y empleando los diferentes servicios en las diferentes capas formadas por el controlador, servicio y DAO para el acceso a la base de datos. El último paso es generar el resultado que es devuelto al cliente, puede ser contenido HTML o un resultado en formato JSON si es un servicio REST. En este punto se accede de nuevo a la base de datos para recuperar las relaciones lazy de los objetos que fueron devueltas por el servicio, esto es habitual en el caso de emplear un ORM como Hibernate o JPA.

Diagrama del patrón open session in view

Diagrama del patrón open session in view Fuente: vladmihalcea.com

Las ventajas

Con el patrón Open Session in View durante toda la petición se mantiene la conexión a la base de datos abierta de modo que al solicitar las relaciones de una entidad las excepciones LazyInitializationException de Hibernate no se producen en las relaciones cargadas en modo lazy. Sin mantener la conexión abierta todos los datos que se necesiten han de cargarse con antelación de lo contrario al acceder a las relaciones de un objeto provocará esa excepción LazyInitializationException. El modo lazy permite solicitar los datos según se necesiten sin necesidad de hacerlo con antelación.

En Spring hay una variable de configuración con la que se aciva o desactiva un filtro que implement el patrón Open Session in View.

1
spring.jpa.open-in-view=false
SpringJpaOpenSessionInView.properties

Los problemas, por que se considera una mala práctica

El patrón Open Session in View tiene varios problemas. Uno de ellos es que al mantener la sesión abierta durante toda la petición y permitir en todo momento acceso a la base de datos no se es consciente de las consultas que se lanzan más usando Hibernate que hace precisamente esto más fácil. El resultado es que hay que tener especial cuidado en no generar el problema 1+N donde se ejecuta una consulta para recuperar una lista de objetos y N para cargar una relación de cada uno de los objetos de la lista anteriores recuperados.

Estos problemas tienen solución en cierta medida con la anotación @BatchSize para recuperar listas de objetos en lotes y FetchMode.SUBSELECT para lanzar una consulta adicional que recupere los objetos de las relaciones. Su inconveniente es que es poco flexible ya que su uso con anotaciones afectan a todas las consultas.

El segundo problema es que la vista es capaz de generar consultas a la base de datos las cuales pueden producir excepciones y las vistas no suelen estar preparadas para manejar excepciones.

Además, las conexiones a la base de datos son un recurso escaso, más incluso que los threads de modo que mantener abierta la conexión durante más tiempo limita la escalabilidad de una aplicación.

Establecer las consultas en modo FetchType.EAGER para recuperar las relaciones cuanto antes aún no conociendo si se usarán los datos no es una solución ya que tampoco puede cambiarse a nivel de consulta. Por estas razones las asociaciones suelen configurarse en modo FetchType.LAZY.

La alternativa

La alternativa al patrón Open Session in View es usar objetos DTO para proporcionar a la vista todos los datos que necesite sin que esta al usar esos datos lance consultas. Esto obliga al controlador del patrón modelo-vista-controlador o MVC a conocer y recuperar de antemano los datos que necesite la vista.

Hibernate es una gran librería por la funcionalidad que ofrece al abstraer el modelo relacional del modelo orientado a objetos del lenguaje relacionar, tanto que es capaz de lanzar las consultas adecuadas a la base de datos relacional tanto en la lectura como en la escritura según las operaciones realizadas en los objetos.

Por otro lado Hibernate en la correspondencia que hace entre el modelo relacional y las entidades de objetos se cargan todos los datos de la entidad aunque muchos no se necesiten en la vista, lo que lo hace algo ineficiente en el acceso de lectura a la base de datos.

En el modelo DTO usando la lógica que recupera los datos ha de estar sincronizada con la lógica de la vista. Por ejemplo, si un dato en la vista solo es necesario dada cierta condición esa misma condición ha de estar en el código que del la vista, o en la vista ser suficiente la presencia del dato para mostrarlo.

Cada vista necesitará unos datos específicos de modo que serán necesarias consultas específicas para recuperar cada uno de los datos. Para el acceso en modo lectura y recuperar algunas de las consultas en vez de usar Hibernate se puede usar la librería jOOQ que proporciona una API en el lenguaje Java para la construcciones de consultas con comprobación de tipos proporcionado por el compilador.

Conclusión

En muchas aplicaciones usar el patrón Open Session in View con Hibernate no supone un gran problema y simplifica el código. Para aquellas aplicaciones que necesitan escalabilidad y soportar un gran número de usuarios concurrentes o hagan operaciones que impliquen operaciones de red se aconseja usar DTO en las vistas ya sean mapeando las entidades Hibernate recuperadas por el controlador a esos DTO con una librería específica para el propósito como ModdelMapper y recuperar únicamente los datos que necesita la vista usando librerías como jOOQ que ofrecen mayor control sobre las columnas de la base de datos datos recuperadas para reducir los datos recuperados de la base de datos a únicamente lo necesario.


Bitácora de Javier Gutiérrez Chamorro (Guti): Depurar aplicaciones DOS con Watcom C y OpenWatcom C++

$
0
0


Quizás por su dilatada historia, tal vez por ir en contra de los grandes, o por su elevado rendimiento, o porque era verdaderamente difícil de conseguir, Watcom C fue el entorno de desarrollo más deseado para mi. Uno de sus encantos es la posibilidad de realizar compilación cruzada o cross compile, es decir, que desde …

Depurar aplicaciones DOS con Watcom C y OpenWatcom C++ Leer más »



Artículo publicado originalmente en Bitácora de Javier Gutiérrez Chamorro (Guti)

Blog Bitix: Copiar datos de un tipo de objeto a otro con ModdelMapper

$
0
0

Las clases DTO son clases usadas como contenedores de datos sin ninguna lógica o con muy poca, se construyen con datos copiados de otras clases. Un uso de estas clases DTO es para evitar emplear el uso del patrón Open Session in View ya que aunque ofrece algunos beneficios también tiene algunos inconvenientes. La librería ModelMapper permite realizar los copiados de datos de un objeto origen a una nueva instancia destino de otra clase.

Java

En ocasiones es necesario copiar datos de un tipo de objeto a otro tipo, no es una operación complicada basta con llamar al método getter de la propiedad a copiar para obtener su valor del objeto origen y posteriormente llamar al setter para establecer el valor en objeto destino. Aún siendo una operación sencilla es tediosa y puede complicarse si se han de copiar listas de objetos y si esos objetos a copiar tienen referencias a otros objetos que también hay que copiar. Si además esta es una operación común en el código es conveniente utilizar una librería específica para este propósito, una de ellas es ModelMapper.

ModelMapper es una librería Java para copiar o mapear propiedades de un tipo de objeto a otro tipo de objeto, permitiendo copiar también los datos de las referencias a los objetos que contengan. Soporta diferentes convenciones, copiados explícitos, conversiones y proveedores para construir los objetos destino e integraciones con diferentes librerías, una de ellas jOOQ.

Un posible caso de uso es para evitar emplear el patrón Open Session in View ya que tiene varios inconvenientes. Con una librería como ModelMapper es posible hacer uso de simples objetos contenedores de datos en la vista copiando los datos de las entidades a los objetos DTO. O si para obtener los datos de la vista en vez de usar una librería como Hibernate se opta por una librería como jOOQ permitir copiar los datos de los registros de jOOQ a los mismos DTOs.

El siguiente ejemplo se compone de tres clases que tienen relaciones entre ellas, estas clases podrían ser las entidades si se persistiesen en base de datos con Hibernate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
packageio.github.picodotdev.blogbitix.modelmapper.classes;publicclassOrder{    privateCustomercustomer;    privateAddressbillingAddress;    publicOrder(Customercustomer,AddressbillingAddress){        this.customer=customer;        this.billingAddress=billingAddress;    }    publicCustomergetCustomer(){        returncustomer;    }    publicAddressgetBillingAddress(){        returnbillingAddress;    }}
Order.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
packageio.github.picodotdev.blogbitix.modelmapper.classes;publicclassCustomer{    privateStringfirstName;    privateStringlastName;    publicCustomer(StringfirstName,StringlastName){        this.firstName=firstName;        this.lastName=lastName;    }    publicStringgetFirstName(){        returnfirstName;    }    publicStringgetLastName(){        returnlastName;    }}
Customer.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
packageio.github.picodotdev.blogbitix.modelmapper.classes;publicclassAddress{    privateStringstreet;    privateStringcity;    publicAddress(Stringstreet,Stringcity){        this.street=street;        this.city=city;    }    publicStringgetStreet(){        returnstreet;    }    publicStringgetCity(){        returncity;    }}
Address.java

La clase DTO es simplemente una nueva clase POJO que contiene los datos de las clases anteriores, para evitar el patrón Open Session in View la vista recibiría una instancia de esta clase.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
packageio.github.picodotdev.blogbitix.modelmapper.classes;publicclassOrderDTO{    privateStringcustomerFirstName;    privateStringcustomerLastName;    privateStringbillingAddressStreet;    privateStringbillingAddressCity;    publicStringgetCustomerFirstName(){        returncustomerFirstName;    }    publicvoidsetCustomerFirstName(StringcustomerFirstName){        this.customerFirstName=customerFirstName;    }    publicStringgetCustomerLastName(){        returncustomerLastName;    }    publicvoidsetCustomerLastName(StringcustomerLastName){        this.customerLastName=customerLastName;    }    publicStringgetBillingAddressStreet(){        returnbillingAddressStreet;    }    publicvoidsetBillingAddressStreet(StringbillingAddressStreet){        this.billingAddressStreet=billingAddressStreet;    }    publicStringgetBillingAddressCity(){        returnbillingAddressCity;    }    publicvoidsetBillingAddressCity(StringbillingAddressCity){        this.billingAddressCity=billingAddressCity;    }}
OrderDTO.java

En esta aplicación de Spring Boot se construye una instancia de la clase ModelMapper y posteriormente con su configuración y convenciones por defecto realiza el copiado de datos de una instancia de la clase Order a una nueva instancia de la clase OrderDTO. En la salida del programa en la consola se muestran los valores de las propiedades de OrderDTO copiadas de la clase Order.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
packageio.github.picodotdev.blogbitix.modelmapper;importio.github.picodotdev.blogbitix.modelmapper.classes.Address;importio.github.picodotdev.blogbitix.modelmapper.classes.Customer;importio.github.picodotdev.blogbitix.modelmapper.classes.Order;importio.github.picodotdev.blogbitix.modelmapper.classes.OrderDTO;importorg.modelmapper.ModelMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.annotation.Bean;@SpringBootApplicationpublicclassMainimplementsCommandLineRunner{@AutowiredprivateModelMappermodelMapper;@BeanModelMappermodelMapper(){returnnewModelMapper();}@Overridepublicvoidrun(String...args)throwsException{Customercustomer=newCustomer("Francisco","Ibáñez");AddressbilligAddress=newAddress("c\\ Rue del Percebe, 13","Madrid");Orderorder=newOrder(customer,billigAddress);OrderDTOorderDTO=modelMapper.map(order,OrderDTO.class);System.out.printf("Customer First Name: %s%n",orderDTO.getCustomerFirstName());System.out.printf("Customer Last Name: %s%n",orderDTO.getCustomerLastName());System.out.printf("Billing Address Street: %s%n",orderDTO.getBillingAddressStreet());System.out.printf("Billing Address City: %s%n",orderDTO.getBillingAddressCity());}publicstaticvoidmain(String[]args){SpringApplication.run(Main.class,args);}}
Main.java
1
2
3
4
Customer First Name: Francisco
Customer Last Name: Ibáñez
Billing Address Street: c\ Rue del Percebe, 13
Billing Address City: Madrid
System.out

El archivo de contrucción Gradle contiene la dependencia de ModelMapper.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
plugins{    id'java'    id'application'}group='io.github.picodotdev.blogbitix.modelmapper'version='1.0'java{    sourceCompatibility=JavaVersion.VERSION_11}application{    mainClass='io.github.picodotdev.blogbitix.modelmapper.Main'}repositories{    mavenCentral()}dependencies{    implementationplatform('org.springframework.boot:spring-boot-dependencies:2.3.0.RELEASE')    implementation'org.springframework.boot:spring-boot-starter'    implementation'org.modelmapper:modelmapper:2.3.7'}
build.gradle
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run

Blog Bitix: Copiar datos de un tipo de objeto a otro con ModelMapper

$
0
0

Las clases DTO son clases usadas como contenedores de datos sin ninguna lógica o con muy poca, se construyen con datos copiados de otras clases. Un uso de estas clases DTO es para evitar emplear el uso del patrón Open Session in View ya que aunque ofrece algunos beneficios también tiene algunos inconvenientes. La librería ModelMapper permite realizar los copiados de datos de un objeto origen a una nueva instancia destino de otra clase.

Java

En ocasiones es necesario copiar datos de un tipo de objeto a otro tipo, no es una operación complicada basta con llamar al método getter de la propiedad a copiar para obtener su valor del objeto origen y posteriormente llamar al setter para establecer el valor en objeto destino. Aún siendo una operación sencilla es tediosa y puede complicarse si se han de copiar listas de objetos y si esos objetos a copiar tienen referencias a otros objetos que también hay que copiar. Si además esta es una operación común en el código es conveniente utilizar una librería específica para este propósito, una de ellas es ModelMapper.

ModelMapper es una librería Java para copiar o mapear propiedades de un tipo de objeto a otro tipo de objeto, permitiendo copiar también los datos de las referencias a los objetos que contengan. Soporta diferentes convenciones, copiados explícitos, conversiones y proveedores para construir los objetos destino e integraciones con diferentes librerías, una de ellas jOOQ.

Un posible caso de uso es para evitar emplear el patrón Open Session in View ya que tiene varios inconvenientes. Con una librería como ModelMapper es posible hacer uso de simples objetos contenedores de datos en la vista copiando los datos de las entidades a los objetos DTO. O si para obtener los datos de la vista en vez de usar una librería como Hibernate se opta por una librería como jOOQ permitir copiar los datos de los registros de jOOQ a los mismos DTOs.

El siguiente ejemplo se compone de tres clases que tienen relaciones entre ellas, estas clases podrían ser las entidades si se persistiesen en base de datos con Hibernate.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
packageio.github.picodotdev.blogbitix.modelmapper.classes;publicclassOrder{    privateCustomercustomer;    privateAddressbillingAddress;    publicOrder(Customercustomer,AddressbillingAddress){        this.customer=customer;        this.billingAddress=billingAddress;    }    publicCustomergetCustomer(){        returncustomer;    }    publicAddressgetBillingAddress(){        returnbillingAddress;    }}
Order.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
packageio.github.picodotdev.blogbitix.modelmapper.classes;publicclassCustomer{    privateStringfirstName;    privateStringlastName;    publicCustomer(StringfirstName,StringlastName){        this.firstName=firstName;        this.lastName=lastName;    }    publicStringgetFirstName(){        returnfirstName;    }    publicStringgetLastName(){        returnlastName;    }}
Customer.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
packageio.github.picodotdev.blogbitix.modelmapper.classes;publicclassAddress{    privateStringstreet;    privateStringcity;    publicAddress(Stringstreet,Stringcity){        this.street=street;        this.city=city;    }    publicStringgetStreet(){        returnstreet;    }    publicStringgetCity(){        returncity;    }}
Address.java

La clase DTO es simplemente una nueva clase POJO que contiene los datos de las clases anteriores, para evitar el patrón Open Session in View la vista recibiría una instancia de esta clase.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
packageio.github.picodotdev.blogbitix.modelmapper.classes;publicclassOrderDTO{    privateStringcustomerFirstName;    privateStringcustomerLastName;    privateStringbillingAddressStreet;    privateStringbillingAddressCity;    publicStringgetCustomerFirstName(){        returncustomerFirstName;    }    publicvoidsetCustomerFirstName(StringcustomerFirstName){        this.customerFirstName=customerFirstName;    }    publicStringgetCustomerLastName(){        returncustomerLastName;    }    publicvoidsetCustomerLastName(StringcustomerLastName){        this.customerLastName=customerLastName;    }    publicStringgetBillingAddressStreet(){        returnbillingAddressStreet;    }    publicvoidsetBillingAddressStreet(StringbillingAddressStreet){        this.billingAddressStreet=billingAddressStreet;    }    publicStringgetBillingAddressCity(){        returnbillingAddressCity;    }    publicvoidsetBillingAddressCity(StringbillingAddressCity){        this.billingAddressCity=billingAddressCity;    }}
OrderDTO.java

En esta aplicación de Spring Boot se construye una instancia de la clase ModelMapper y posteriormente con su configuración y convenciones por defecto realiza el copiado de datos de una instancia de la clase Order a una nueva instancia de la clase OrderDTO. En la salida del programa en la consola se muestran los valores de las propiedades de OrderDTO copiadas de la clase Order.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
packageio.github.picodotdev.blogbitix.modelmapper;importio.github.picodotdev.blogbitix.modelmapper.classes.Address;importio.github.picodotdev.blogbitix.modelmapper.classes.Customer;importio.github.picodotdev.blogbitix.modelmapper.classes.Order;importio.github.picodotdev.blogbitix.modelmapper.classes.OrderDTO;importorg.modelmapper.ModelMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.annotation.Bean;@SpringBootApplicationpublicclassMainimplementsCommandLineRunner{@AutowiredprivateModelMappermodelMapper;@BeanModelMappermodelMapper(){returnnewModelMapper();}@Overridepublicvoidrun(String...args)throwsException{Customercustomer=newCustomer("Francisco","Ibáñez");AddressbilligAddress=newAddress("c\\ Rue del Percebe, 13","Madrid");Orderorder=newOrder(customer,billigAddress);OrderDTOorderDTO=modelMapper.map(order,OrderDTO.class);System.out.printf("Customer First Name: %s%n",orderDTO.getCustomerFirstName());System.out.printf("Customer Last Name: %s%n",orderDTO.getCustomerLastName());System.out.printf("Billing Address Street: %s%n",orderDTO.getBillingAddressStreet());System.out.printf("Billing Address City: %s%n",orderDTO.getBillingAddressCity());}publicstaticvoidmain(String[]args){SpringApplication.run(Main.class,args);}}
Main.java
1
2
3
4
Customer First Name: Francisco
Customer Last Name: Ibáñez
Billing Address Street: c\ Rue del Percebe, 13
Billing Address City: Madrid
System.out

El archivo de contrucción Gradle contiene la dependencia de ModelMapper.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
plugins{    id'java'    id'application'}group='io.github.picodotdev.blogbitix.modelmapper'version='1.0'java{    sourceCompatibility=JavaVersion.VERSION_11}application{    mainClass='io.github.picodotdev.blogbitix.modelmapper.Main'}repositories{    mavenCentral()}dependencies{    implementationplatform('org.springframework.boot:spring-boot-dependencies:2.3.0.RELEASE')    implementation'org.springframework.boot:spring-boot-starter'    implementation'org.modelmapper:modelmapper:2.3.7'}
build.gradle
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run

Variable not found: Enlaces interesantes 406

$
0
0
Enlaces interesantes

El código de estado HTTP 406 (Not Acceptable), es retornado por los servidores cuando no pueden generar una respuesta debido a que son incapaces de cumplir las condiciones indicadas en los encabezados de negociación de contenidos (Accept, Accept-Charset, Accept-Encoding y Accept-Language). Por ejemplo, podríamos obtenerlo si solicitásemos un recurso especificando application/json en el encabezado Accept, pero el servidor no fuera capaz de generar una representación del contenido en dicho formato.

Y ahora, vamos a por los enlaces recopilados durante la semana pasada, que espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET

Azure / Cloud

Conceptos / Patrones / Buenas prácticas

Machine learning / IA / Bots

Web / HTML / CSS / Javascript

Visual Studio / Complementos / Herramientas

Xamarin

Otros

Publicado en Variable not found.

Fixed Buffer: Cómo controlar un ordenador Windows utilizando solo los ojos

$
0
0
Tiempo de lectura:6minutos
Imagen ornamental para la entrada "Cómo controlar un ordenador Windows utilizando solo los ojos"

Esta semana vengo a hablar de un tema un poco diferente y que se sale un poco de la temática del blog. No va a ser una entrada relacionada con ejecutar pruebas de código en Docker o escribir código de alto rendimiento en .Net Core ni ningún tema relacionado con el desarrollo.

Hoy vamos a hablar de accesibilidad y de cómo poder controlar un ordenador con el movimiento de los ojos.

¿Por qué esta entrada?

Sinceramente me hubiese gustado descubrir que esto era posible por simple investigación o por ocio, pero el motivo que hay detrás de investigar sobre esto es que mi padre tiene ELA en un estado muy avanzado y pronto será la única manera de comunicarse que va a disponer. Es por eso que he investigado sobre el tema para encontrar una manera sencilla de controlar un ordenador simplemente con la mirada.

ELA es una enfermedad degenerativa incurable actualmente que hace que poco a poco pierdas la capacidad de controlar tu cuerpo hasta el punto de no ser capaz de mover ni un músculo.

En lo personal, conocía de la existencia de este tipo de tecnologías, ya que por ejemplo Stephen Hawking (que también padecía ELA) controlaba su ordenador con la mirada. Lo que desconocía y ha sido una grata sorpresa, es que la tecnología de seguimiento ocular ha avanzado mucho en los últimos años y es fácil encontrar el hardware necesario para ello. Es por eso que escenarios como controlar un ordenador solo con la mirada es ahora posible con un coste realmente bajo. El único hardware extra cuesta unos 150€.

Antes de seguir, decir que voy a hablar de productos comerciales y no es ningún tipo de publicidad encubierta ni nada por el estilo. Simplemente pretendo poner las cosas más fáciles a gente que igual no tiene la facilidad que tengo yo para manejarme en la era digital.

Una vez hecha la introducción, ¡vamos a ponernos manos a la obra!

¿Qué necesito para controlar un ordenador con los ojos?

Esta pregunta es muy sencilla, lo primero de todo es un ordenador, unos ojos, y un sistema que permita seguir el movimiento de los ojos y traducirlo a movimientos del ratón.

No se requiere ninguna maravilla de ordenador, de hecho, todo esto puede funcionar en una tablet sin mayor esfuerzo. El único requisito mínimo es que el equipo sea Windows 8 o superior y que tenga instalado .Net Framework 4.6.

Respecto al hardware para hacer el seguimiento de los ojos y poder controlar así el ordenador, no está definido un modelo exacto pero en mi caso concreto yo me he decantado por Tobii Eye Tracker 4C.

La imagen muestra el Tobii Eye Tracker 4C utilizado para controlar el ordenador con los ojos

Tiene un coste de 169€ al momento de escribir esta publicación y funciona muy bien con Windows 10.

Instalar y calibrar el Tobii Eye Tracker 4C

Una vez recibido el aparato, el propio paquete contiene todo lo necesario para poder colocarlo. El sistema de sujeción está compuesto por una pegatina para pegar en la parte inferior de la pantalla y la barra se fija a la pegatina mediante imanes, por lo que se puede poner y quitar el tracker de manera sencilla.

La imagen muestra el Tobii Eye Tracker 4C montado en la parte inferior de la pantalla para controlar el ordenador con los ojos

Una vez que lo hemos colocado en la pantalla, hay que instalar el software del fabricante que podemos encontrar en la sección de descargas de su web.

Una vez que hemos descargado e instalado el software (Tobii Eye Tracking), basta con arrancarlo para que la primera vez se nos lance un asistente de calibración de manera automática. Durante el proceso de calibración del dispositivo, se nos pedirán cosas como indicarle la posición del dispositivo en la pantalla gracias a unas marcas que trae el propio dispositivo y después tendremos que mirar fijamente una serie de puntos para que se haga la calibración. Ese es todo el trabajo que requiere calibrar el dispositivo.

Una vez que lo hayamos hecho, se quedará un icono en la barra de notificaciones de Windows para poder volver a acceder a la configuración del aparato.

La imagen muestro el icono del programa Tobii Eye Tracking en la barra de iconos

¿Con solo esto ya podemos controlar el ordenador con los ojos? No, pero estamos cerca ya.

Instalar OptiKey para controlar el ordenador con los ojos

Ahora mismo gracias al hardware de seguimiento ocular, ya tenemos información sobre donde estamos mirando en la pantalla. Para poder controlar el ordenador con la mirada solo nos falta algo que convierta esa información en movimientos del ratón.

Esto es lo que vamos a conseguir gracias al software Open Source OptiKey. OptiKey es un programa hecho por Julius Sweetland y cuyo código fuente está disponible en Github que va a ser el encargado de convertir ese movimiento de los ojos en movimientos del ratón. Este software es gratuito y el proyecto sale adelante a través de Patreon.

Basta con descargarlo desde su página web donde se encuentra disponible en primera página.

La imagen muestra el botón de download de la página web de OptiKey

Donde nos lleva a otro apartado para seleccionar la versión que mejor se adapta a las necesidades. Puede ser control total, solo chat, solo ratón… En este caso cómo lo que queremos es controlar el ordenador entero con la mirada, vamos a seleccionar la versión pro (que es solo nomenclatura, no es que sea de pago).

La imagen señala el botón download de la version pro de OptiKey

Una vez que lo hemos descargado, lo vamos a instalar con el propio asistente de instalación que ofrece. Cuando lo tenemos instalado basta con ejecutar el programa OptiKeyPro desde la propia barra de herramientas.

La imagen muestra el programa OptiKeyPro en la barra de busqueda de Windows

Esto va a dividir la pantalla en 2, dejando arriba un teclado con botones gracias a los cuales vamos a poder controlar el ordenador con la mirada.

La imagen muestra OptiKeyPro controlando un ordenador con el movimiento de los ojos

El funcionamiento, aunque pueda parecer complejo es muy simple, basta con mirar a una tecla en concreto y mantener la mirada para que aparezca un círculo que al completarse pulsa la tecla.

Conclusiones

Se que es una entrada diferente al contenido habitual de este blog y aunque está relacionada con la informática, está más cerca de la accesibilidad que del desarrollo. Como bien decía al principio, la finalidad es dar a conocer este tipo de tecnología a la gente que tiene mayores dificultades para acceder a la información y dar una guía fácil de seguir sobre como poder controlar un ordenador simplemente con la mirada.

Que nadie se preocupe porque la próxima entrada volveremos a los temas habituales de FixedBuffer, ya que últimamente he trabajado con varias cosas interesantes de las que hablaremos muy pronto.

Por último, y esto es la primera vez que lo hago, pido a todos los lectores que lleguen hasta aquí que comportan la entrada para intentar llegar ese grupo de personas como puede ser mi madre, que no son frikis tecnológicos pero que que pueden necesitar en algún momento saber que esto existe.

Y ya para terminar, ¿tu sabias que era tan sencillo controlar un ordenador solo con la mirada?

**La entrada Cómo controlar un ordenador Windows utilizando solo los ojos se publicó primero en Fixed Buffer.**

Variable not found: Cómo personalizar el mensaje "Loading" de las aplicaciones Blazor WebAssembly

$
0
0
BlazorLa verdad es que el mensaje "Loading..." que aparece en el navegador mientras la aplicación Blazor WebAssembly está cargando es bastante insulso: un fondo blanco, con el texto sin estilo alguno. Es cierto que aparece sólo unos segundos, y a veces mucho menos que eso, pero si queremos que nuestra aplicación tenga un aspecto profesional, deberíamos hacer algo por mejorarlo.

Veamos cómo.

1. Primero: definir el look del mensaje de carga

No vamos a complicarnos mucho en este momento porque probablemente tengáis bastante mejor gusto y seáis más creativos que yo, por lo que vamos a tomar un camino bastante sencillo, suficiente para que podáis ver cómo implementar vuestra propia imagen de carga. Lo que pretendemos lograr es lo siguiente:

Aplicación Blazor WebAssembly cargando

Para crear el spinner me he basado en una de las múltiples herramientas online que existen para la creación de este tipo de elementos, basándose exclusivamente en CSS. Concretamente he utilizado Spinkit, de Tobias Ahlin.

El código CSS generado desde esta herramienta, que retocamos ligeramente para adaptarlo a nuestras necesidades, lo guardamos en un archivo llamado loader.css en la carpeta wwwroot/css de nuestro proyecto:
.loading-page {
height: 100vh;
width: 100vw;
background-color: #2c3e50;
}

.sk-chase {
width: 80px;
height: 80px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: sk-chase 2.5s infinite linear both;
}

.sk-chase-dot {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
animation: sk-chase-dot 2.0s infinite ease-in-out both;
}

.sk-chase-dot:before {
content: '';
display: block;
width: 25%;
height: 25%;
background-color: cyan;
border-radius: 100%;
animation: sk-chase-dot-before 2.0s infinite ease-in-out both;
}

.sk-chase-dot:nth-child(1) { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2) { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3) { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4) { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5) { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6) { animation-delay: -0.6s; }
.sk-chase-dot:nth-child(1):before { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2):before { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3):before { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4):before { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5):before { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6):before { animation-delay: -0.6s; }

@keyframes sk-chase {
100% { transform: rotate(360deg); }
}

@keyframes sk-chase-dot {
80%, 100% { transform: rotate(360deg); }
}

@keyframes sk-chase-dot-before {
50% { transform: scale(0.4); }
100%, 0% { transform: scale(1.0); }
}

2. Poner en marcha el loader

La puesta en marcha de este loader es bastante sencillo, pues sólo tenemos que introducir unos pequeños cambios en la página HTML que aloja la aplicación, wwwroot/index.html.

En primer lugar, debemos incluir en su sección <head> una referencia hacia la hoja de estilos que hemos creado anteriormente:
<link href="css/loader.css" rel="stylesheet" />
A continuación, ya en el cuerpo de la página, hay que sustituir el contenido de la etiqueta <app> por el HTML generado también desde Spinkit, con algún retoque para adaptarlo mejor a nuestros intereses:
<app>
<div class="loading-page">
<div class="sk-chase">
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
</div>
</div>
</app>
¡Y eso es todo! Ya podéis ejecutar vuestra aplicación Blazor WebAssembly y disfrutar de una pantalla de carga más atractiva. Para verlo bien os recomiendo que utilicéis algún tipo de ralentizador de peticiones, como el disponible en las Developer Tools de Chrome, de forma que podáis simular una conexión lenta.

Publicado en Variable not found.

Bitácora de Javier Gutiérrez Chamorro (Guti): Lenguaje de programación Scratch

$
0
0


Continuamos con la programación informática, una temática que había dejado algo abandonada, y que después del ataque retro de Depurar aplicaciones DOS con Watcom C y OpenWatcom C++, hoy atacaré con una nueva, y original tendencia: el lenguaje Scracth. Desde hace mucho que me fascinan los lenguajes de programación orientados a niños. Creo que la …

Lenguaje de programación Scratch Leer más »



Artículo publicado originalmente en Bitácora de Javier Gutiérrez Chamorro (Guti)

Blog Bitix: La herramienta SDKMAN para instalar varias versiones del JDK y software de la plataforma Java

$
0
0

En ocasiones es necesario tener instaladas varias versiones de JDK según el proyecto, unos quizá usen Java 8 y otros quizá usen Java 11 o posterior. El gestor de software SDKMAN permite instalar múltiples versiones del JDK de forma simultánea y usar la deseada a conveniencia. Adicionalmente también permite instalar otras herramientas de la plataforma Java como Gradle y Maven o lenguajes como Groovy entre otros SDK comunes disponibles.

SDKMAN

Cuando usaba Windows para instalar el JDK para programar en Java siempre me lo descargaba de la página de Oracle, el típico instalador de Windows con un asistente que dejaba el JDK instalado en el sistema. Desde que uso GNU/Linux siempre he usado la versión del JDK del proyecto OpenJDK del que el JDK de Oracle toma como fuente y posteriormente la empresa de Larry Ellison añade sus modificaciones y extensiones algunas no libres.

El inconveniente surge cuando hay que instalar una nueva versión, lo que posiblemente implica desinstalar la antigua y repetir el proceso de instalación con la nueva. En Windows hasta donde he conocido un proceso manual que ahora con WinGet al estilo de los gestores de paquetes de GNU/Linux lo mantenga actualizado al actualizar el sistema sin apenas intervención por parte del usuario.

Pero ahora con el nuevo ciclo de desarrollo del JDK que publica una nueva versión cada seis meses y una versión de soporte a largo plazo cada tres años las versiones son enormemente frecuentes a los que los usuarios de Java estábamos acostumbrados con una publicación cada dos o más años. Al mismo tiempo han surgido múltiples implementaciones o distribuciones del JDK, por ejemplo Amazon ofrece Corretto también están AdoptOpenJDK entre algunas otras menos populares. Tener múltiples versiones o implementaciones del JDK en el sistema implica habitualmente tener que cambiar una variable de entorno para indicar cual es el directorio home del JDK que se desea usar. Esta gestión de la versión del JDK es manual y poco práctica ya que afecta a nivel global del sistema lo que impide usar diferentes versiones de JDK según la aplicación o proyecto, quizá unos proyectos usen Java 8 y otros Java 11 u otro más reciente.

Qué es SDKMAN y qué ventajas tiene

SDKMAN es un gestor de paquetes basado en línea de comandos para instalar y mantener actualizado software de la plataforma Java no solo para el JDK sino que para otras muchas librerías, utilidades y SDK comunes como las herramientas de construcción Gradle y Maven, lenguajes para la plataforma como Groovy, Ceylon o Scala, la nombrada utilidad para crear software de código nativo GraalVM com importantes mejoras en rendimiento en tiempo de ejecución y consumo de memoria. La herramienta SDKMAN tiene las ventajas de poder instalar varias versiones del JDK, múltiples implementaciones y poder usar una versión distinta según el proyecto, permite solucionar los problemas anteriores.

En los últimos años hasta ahora siempre he usado el paquete de la distribución Arch Linux que uso para el JDK y Gradle pero a partir de ahora empezaré a usar SDKMAN ya que permite tener instaladas simultáneamente varias versiones, tiene versiones adicionales que no están disponibles en la distribución y permite cambiar fácilmente entre versiones sin necesidad de permisos de superusuario. La desventaja que tiene SDKMAN es que al no usar el paquete de la distribución no se puede instalar ningún paquete que requiera Java, en el caso de Arch Linux los paquetes del JDK como jdk11-openjdk proporcionan la depdendencia java-environment que es necesaria para algunos otros paquetes como intellij-idea-community-edition. La solución es instalar los programas mediante Flatpak si están disponibles en esta forma de distribución de software.

Instalación

La instalación de SDKMAN consiste en ejecutar los siguientes comandos, el primero descarga e instala los scripts de SDKMAM en la carpeta personal del usuario, ~/.sdkman. También modifica los archivos de configuración del intérprete de comandos bash o zsh que permiten establecer las variables de entorno que necesita cuando se inicia una nueva terminal. Los dos últimos permiten comprobar que la instalación se ha realizado correctamente.

SDKMAN solo está disponible de forma nativa para los sistemas UNIX entre ellos GNU/Linux pero no Windows dado que está basado en scripts de bash. Para Windows es necesario utilizar Windows Linux Subsystem o WLS, Cygwin o MSYS+MinGW.

1
2
3
$ curl -s "https://get.sdkman.io"| bash
$ source"$HOME/.sdkman/bin/sdkman-init.sh"
$ sdk version
install.sh

Uso

La utilidad de línea de comandos SDKMAN es sdk, una utilidad sencilla con varias operaciones para instalar, actualizar, desinstalar o usar la última versión o una determinada versión. Las notas de ayuda de la utilidad son bastante explicativas de cómo usar esta herramienta.

El término candidate hace referencia al paquete de software, puede ser un JDK o un SDK. El término versión indica la versión afectada por el comando del paquete de software candidate previamente indicado. Si estas notas no son suficientes hay una guía de uso.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ sdk

Usage: sdk <command> [candidate][version]      sdk offline <enable|disable>

  commands:
      install   or i    <candidate> [version][local-path]      uninstall or rm   <candidate> <version>
      list      or ls   [candidate]      use       or u    <candidate> <version>
      default   or d    <candidate> [version]      current   or c    [candidate]      upgrade   or ug   [candidate]      version   or v
      broadcast or b
      help      or h
      offline           [enable|disable]      selfupdate        [force]      update
      flush             <broadcast|archives|temp>

  candidate  :  the SDK to install: groovy, scala, grails, gradle, kotlin, etc.
                use list commandfor comprehensive list of candidates
                eg: $ sdk list
  version    :  where optional, defaults to latest stable if not provided
                eg: $ sdk install groovy
  local-path :  optional path to an existing local installation
                eg: $ sdk install groovy 2.4.13-local /opt/groovy-2.4.13
usage.sh

En la página de SDKMAM hay una lista de JDK y SDK disponibles en este gestor de software. Los comandos más habituales son list para listar candidatos y versiones disponibles, install para instalar software, upgrade para actualizar a la última versión el candidato indicado o todos los instalados si no se indica ninguno, use para usar una versión específica de un candidato durante la sesión de la terminal, default para establecer la versión por defecto de un candidato y uninstall para desinstalar un candidato y versión. En el siguiente ejemplo se muestra como instalar varias versiones del JDK, usarlas y cambiar entre versiones.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
$ sdk list java================================================================================
Available Java Versions================================================================================
 Vendor        | Use | Version      | Dist    | Status     | Identifier
--------------------------------------------------------------------------------
 AdoptOpenJDK  |     | 14.0.1.j9    | adpt    |            | 14.0.1.j9-adpt      
               |     | 14.0.1.hs    | adpt    |            | 14.0.1.hs-adpt      
               |     | 13.0.2.j9    | adpt    |            | 13.0.2.j9-adpt      
               |     | 13.0.2.hs    | adpt    |            | 13.0.2.hs-adpt      
               |     | 12.0.2.j9    | adpt    |            | 12.0.2.j9-adpt      
               |     | 12.0.2.hs    | adpt    |            | 12.0.2.hs-adpt      
               |     | 11.0.7.j9    | adpt    |            | 11.0.7.j9-adpt      
               |     | 11.0.7.hs    | adpt    |            | 11.0.7.hs-adpt      
               |     | 8.0.252.j9   | adpt    |            | 8.0.252.j9-adpt     
               |     | 8.0.252.hs   | adpt    |            | 8.0.252.hs-adpt     
 Amazon        |     | 11.0.7       | amzn    |            | 11.0.7-amzn         
               |     | 8.0.252      | amzn    |            | 8.0.252-amzn        
...
 GraalVM       |     | 20.1.0.r11   | grl     |            | 20.1.0.r11-grl      
...
 Java.net      |     | 15.ea.26     | open    |            | 15.ea.26-open       
               |     | 14.0.1       | open    |            | 14.0.1-open         
               |     | 13.0.2       | open    |            | 13.0.2-open         
               |     | 12.0.2       | open    |            | 12.0.2-open         
               |     | 11.0.7       | open    |            | 11.0.7-open         
               |     | 10.0.2       | open    |            | 10.0.2-open         
               |     | 9.0.4        | open    |            | 9.0.4-open          
               |     | 8.0.252      | open    |            | 8.0.252-open        
...  
================================================================================
Use the Identifier for installation:

    $ sdk install java 11.0.3.hs-adpt
================================================================================

$ sdk install java 8.0.252-open
$ sdk install java 11.0.7-open
$ sdk install java 14.0.1-open
$ sdk default java 11.0.7-open

$ sdk use java 8.0.252-open
$ java -version
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (build 1.8.0_252-b09)
OpenJDK 64-Bit Server VM (build 25.252-b09, mixed mode)

$ sdk use java 11.0.7-open
$ java -version
openjdk version "11.0.7" 2020-04-14
OpenJDK Runtime Environment 18.9 (build 11.0.7+10)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.7+10, mixed mode)

$ sdk use java 14.0.1-open
$ java -version
openjdk version "14.0.1" 2020-04-14
OpenJDK Runtime Environment (build 14.0.1+7)
OpenJDK 64-Bit Server VM (build 14.0.1+7, mixed mode, sharing)

$ sdk upgrade
commands.sh

En la distribución inmutable Fedora Silverblue esta herramienta es especialmente útil ya que todo el software que instala lo hace en la carpeta personal del usuario, ni pedir permisos de administrador del sistema para instalar o desinstalar candidatos.

Variable not found: Mostrar HTML "crudo" en componentes Blazor

$
0
0
Blazor Hoy va un truquillo rápido sobre Blazor que puede resultar útil en muchos escenarios. Como probablemente sepáis, al igual que ocurre con otras tecnologías, Blazor codifica la salida HTML por motivos de seguridad, de forma que cualquier contenido con código de marcado será mostrado por defecto tal cual, sin interpretar las etiquetas.

Por ejemplo, considerad el siguiente código en un componente Blazor:
<p>This text is encoded: @myHtml</p>

@code {
string myHtml = "Hello, <b>this is bold</b>";
}
El resultado que enviaremos al navegador es el siguiente:
<p>This text is encoded: Hello, &lt;b&gt;this is bold&lt;/b&gt;</p>
Y, por tanto, nuestros usuarios podrán leer literalmente este párrafo:
This text is encoded: Hello, <b>this is bold</b>
Normalmente no es eso lo que queremos mostrarles, ¿verdad?

Para conseguir que un contenido HTML sea interpretado apropiadamente debemos anular la codificación por defecto de Blazor, para lo que utilizaremos el tipo de datos MarkupString. Esta estructura, definida en Microsoft.AspNetCore.Components, permite indicar al motor de renderizado HTML que su contenido no debe ser codificado y, por tanto, debe ser incluido literalmente en la página.

El siguiente código mostrará en el navegador el texto "This text is not encoded: Hello this is bold" (negrita incluida):
<p>This text is not encoded: @myMarkup</p>

@code {
MarkupString myMarkup = new MarkupString("Hello, <b>this is bold</b>");
}
Para simplificar un poco su implementación, MarkupString define la operación de conversión explícita desde string, por lo que podemos hacer casting directo sobre la marcha en el momento del renderizado:
<p>This text is encoded: @myHtml</p>
<p>This text is not encoded: @((MarkupString)myHtml)</p>

@code {
string myHtml = "Hello, <b>this is bold</b>";
}
Y el resultado visible en el navegador será el esperado:
This text is encoded: Hello, &lt;b&gt;this is bold&lt;/b&gt;
This text is not encoded: Hello, this is bold
Espero que os sea de utilidad :)

Publicado en: www.variablenotfound.com.

Blog Bitix: Pruebas de integración con Testcontainers, ejemplo de JPA con la base de datos PostgreSQL

$
0
0

Algunas partes del código no es posibles probarlas con teses unitarios ya que tienen dependencias como una base de datos. En estos casos es necesario realizar un test de integración, la dificultad reside en tener esta dependencia en el entorno de pruebas. La herramienta Testcontainers permite iniciar un contenedor Docker con la dependencia cuando el test se ejecuta.

Testcontainers

Las pruebas automatizadas permiten validar que el comportamiento del software es el esperado. Permiten verificar que ante cambios no se introducen errores, generan mayor confianza en que el software funciona como se espera y evitan la necesidad de realizar pruebas manuales lentas y repetitivas.

Las pruebas más numerosas son las unitarias que prueban un componente del software de forma aislada de sus dependencias sin invocar operaciones de red. Las dependencias del componente bajo la prueba no son las reales, las dependencias simulan el comportamiento de los componentes a los que sustituyen por stubs o mocks para analizar cómo se comporta el sujeto bajo la prueba o SUT. En el artículo qué es un SUT, fake, stub, mock y spy describo este tipo de objetos que sustituyen a las dependencias reales.

El objetivo de sustituir una dependencia real por un doble es programar el comportamiento del doble, ejecutar la prueba de forma aislada y de forma rápida. Una dependencia real es una base de datos como PostgreSQL, NoSQL como MongoDB o un servicio que requiere comunicación por red como GraphQL, al sustituir las dependencias no son necesarias en el entorno de la prueba lo que lo hace más sencillo.

Sin embargo, el código a ejecutar en la aplicación finalmente hace uso de las dependencias reales, y estas no se prueban en las pruebas unitarias, lo que significa los mismos problemas de errores y pruebas manuales de un código que no tiene pruebas automatizadas o pruebas que se basan en dobles que sustituyen a los reales y en algunos casos quiza se comporten de forma diferente. Las pruebas de integración permiten probar el funcionamiento de dos componentes relacionados.

Testcontainers es una herramienta que permite realizar pruebas de integración utilizando las mismas dependencias que usa la aplicación en su funcionamiento normal y disponer de estas dependencias en el entorno de prueba. Si usa una base de datos PostgreSQL las pruebas usan esta base de datos, lo mismo si las pruebas necesitan, MongoDB, RabbitMQ o Redis. Testcontainers es una tecnología Java que se basa en el uso de contenedores Docker para las pruebas. Al iniciar las pruebas de integración Testcontainers se encarga de iniciar un contenedor efímero por cada una de las dependencias que se necesite, al finalizar las pruebas el contenedor es destruido.

Una aplicación que use una base de datos SQL lanza consultas SQL, aunque el lenguaje de consulta SQL está estandarizado las bases de datos incluyen diferencias en las funciones, sintaxis y palabras clave específicas de esa base de datos que no son compatibles con otras bases de datos. El caso es que probar esas consultas contra una base de datos en memoria puede hacer que el test funcione pero sobre la base de datos real no, la aplicación tiene consultas con condiciones complejas o procedimientos almacenados que es necesario que tengan pruebas, para garantizar mayor fiabilidad de las pruebas es mejor usar la base de datos real y no otra base de datos en memoria como H2, HSQL que a veces se utilizan por no disponer en el entorno de pruebas la base de datos real.

Las partes del código que se puedan probar con teses unitarios es mejor probarlas con este tipo de teses ya que se ejecutan más rápidamente, son más fiables y no necesitan dependencias. Si ciertas partes del código no se pueden probar con una prueba unitaria por tener dependencias las pruebas de integración son la opción aconsejada para que también tengan sus correspondientes teses.

En el siguiente ejemplo muestro una clase repositorio que accede a una base de datos PostgreSQL implementado con Spring Data con JPA e Hibernate que en su ejecución lanza consultas SQL a la base de datos relacional. Para probar su comportamiento en una prueba de integración se usa Testcontainers que arranca el contenedor Docker de PostgreSQL. La prueba está implementada con JUnit 5 y la aplicación hace uso de Spring Boot. La prueba de integración realiza un par de pruebas para esa clase repositorio insertando los datos de prueba de dos formas diferentes.

Esta es la entidad de Hibernate que la clase repositorio persiste en la base de datos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
packageio.github.picodotdev.blogbitix.testcontainers;importjava.util.Objects;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.Table;@Entity@Table(name="Person")publicclassPerson{    @Id    @GeneratedValue    privateLongid;    privateStringname;    publicPerson(){    }    publicPerson(Stringname){        this.name=name;    }    publicLonggetId(){        returnid;    }    publicStringgetName(){        returnname;    }    publicvoidsetName(Stringname){        this.name=name;    }    @Override    publicinthashCode(){        returnObjects.hash(id);    }    @Override    publicbooleanequals(Objecto){        if(this==o)            returntrue;        if(o==null)            returnfalse;        if(!(oinstanceofPerson))            returnfalse;        Personthat=(Person)o;        returnsuper.equals(that)            &&Objects.equals(this.id,that.id);    }}
Person.java

La clase repositorio es una implementación para el acceso a la base de datos haciendo uso de las facilidades que proporciona Spring Data. La interfaz CrudRepository ofrece métodos con las oepraciones de lectura, guardado, actualización y eliminación básicas que en su invocación generan las consultas SQL select, insert, update y delete correspondientes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
packageio.github.picodotdev.blogbitix.testcontainers;importorg.springframework.data.jpa.repository.Modifying;importorg.springframework.data.jpa.repository.Query;importorg.springframework.data.repository.CrudRepository;publicinterfacePersonRepositoryextendsCrudRepository<Person,Long>{    @Override    @Modifying    @Query("delete from Person")    voiddeleteAll();}
PersonRepository.java

Testcontainers necesita iniciar contenedores para lo que es necesario instalar previamente según la guía para Docker este software de contenedores. En archivo de construcción es necesario incluir las dependencias de Testcontainers, de Spring y el controlador para la base de datos PostgreSQL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
plugins{id'java'id'application'}group='io.github.picodotdev.blogbitix.testcontainers'version='1.0'java{    sourceCompatibility=JavaVersion.VERSION_11}application{    mainClass='io.github.picodotdev.blogbitix.testcontainers.Main'}repositories{mavenCentral()}dependencies{implementationplatform('org.springframework.boot:spring-boot-dependencies:2.3.0.RELEASE')implementation'org.springframework.boot:spring-boot-starter-data-jpa'implementation'org.liquibase:liquibase-core'testImplementation('org.springframework.boot:spring-boot-starter-test'){excludegroup:'org.junit.vintage',module:'junit-vintage-engine'}testImplementation'org.testcontainers:testcontainers:1.14.3'testImplementation'org.testcontainers:junit-jupiter:1.14.3'testImplementation'org.testcontainers:postgresql:1.14.3'testImplementation'org.postgresql:postgresql:42.2.12'testImplementation'redis.clients:jedis:3.3.0'}test{useJUnitPlatform()}
build.gradle

La clase DefaultPostgresContainer permite encapsular el inicio del contenedor para PostgresSQL y configurar las variables spring.datasource.url, spring.datasource.username y spring.datasource.password con la URL de conexión, usuario y contraseña antes de que el contexto de Spring se inicie. La clase DefaultPostgresContainer permite reutilizar esta conguración en diferentes teses y hacer uso de ella donde sea necesario con la anotación ContextConfiguration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
packageio.github.picodotdev.blogbitix.testcontainers;importorg.springframework.boot.test.util.TestPropertyValues;importorg.springframework.context.ApplicationContextInitializer;importorg.springframework.context.ConfigurableApplicationContext;importorg.testcontainers.containers.PostgreSQLContainer;publicclassDefaultPostgresContainerextendsPostgreSQLContainer<DefaultPostgresContainer>{    privatestaticfinalStringIMAGE_VERSION="postgres:12";    privatestaticDefaultPostgresContainercontainer;    privateDefaultPostgresContainer(){        super(IMAGE_VERSION);    }    publicstaticDefaultPostgresContainergetInstance(){        if(container==null){            container=newDefaultPostgresContainer();        }        returncontainer;    }    @Override    publicvoidstart(){        super.start();    }    @Override    publicvoidstop(){        super.stop();    }    publicstaticclassInitializerimplementsApplicationContextInitializer<ConfigurableApplicationContext>{        publicvoidinitialize(ConfigurableApplicationContextconfigurableApplicationContext){            DefaultPostgresContainercontainer=DefaultPostgresContainer.getInstance();            container.start();            TestPropertyValues.of(                    "spring.datasource.url="+container.getJdbcUrl(),                    "spring.datasource.username="+container.getUsername(),                    "spring.datasource.password="+container.getPassword()            ).applyTo(configurableApplicationContext.getEnvironment());        }    }}
DefaultPostgresContainer.java

Se puede iniciar cualquier contenedor de forma genérica con el siguiente código, indicando la imagen del contenedor y etiqueta además del puerto que expone. Testcontainer los expone de forma local usando un puerto aleatorio, se necesita el host y puerto que permite la conexión al servicio obtenidos de la referencia del contenedor. En este caso se inicia un contenedor Redis accedido con la librería Jedis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
packageio.github.picodotdev.blogbitix.testcontainers;importorg.junit.Assert;importorg.junit.jupiter.api.BeforeAll;importorg.junit.jupiter.api.BeforeEach;importorg.junit.jupiter.api.Test;import staticorg.junit.jupiter.api.Assertions.*;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.util.TestPropertyValues;importorg.springframework.context.ApplicationContextInitializer;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.test.context.ContextConfiguration;importorg.testcontainers.containers.GenericContainer;importorg.testcontainers.junit.jupiter.Container;importorg.testcontainers.junit.jupiter.Testcontainers;importredis.clients.jedis.Jedis;@SpringBootTest@Testcontainers@ContextConfiguration(initializers={RedisTest.Initializer.class})publicclassRedisTest{    @Container    privateGenericContainerredis=newGenericContainer<>("redis:6").withExposedPorts(6379);    privateJedisjedis;    @BeforeEach    voidbeforeEach(){        Stringhost=redis.getHost();        Integerport=redis.getFirstMappedPort();        jedis=newJedis(host,port);    }    @Test    voidredisTest(){        jedis.set("foo","bar");        Assert.assertEquals("bar",jedis.get("foo"));    }    publicstaticclassInitializerimplementsApplicationContextInitializer<ConfigurableApplicationContext>{        publicvoidinitialize(ConfigurableApplicationContextconfigurableApplicationContext){            DefaultPostgresContainercontainer=DefaultPostgresContainer.getInstance();            container.start();            TestPropertyValues.of("spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration")                    .applyTo(configurableApplicationContext.getEnvironment());        }    }}
RedisTest.java

La prueba está implementada con JUnit 5 y Spring Boot, con la anotación ContextConfiguration se indica a JUnit y a Spring que inicie el contenedor de PostgreSQL antes de iniciar el contexto de Spring que configura las variables de conexión a la base de datos y antes de ejecutar los métodos de prueba. Los métodos de prueba son muy sencillos simplemente persisten en la base de datos varias entidades y se prueba que el número de entidades presentes en la base de datos al contarlas es el esperado.

Los datos iniciales de prueba o fixture se insertan de dos formas diferentes en cada método de prueba, en uno haciendo uso de la propia clase repositorio y en otro con la anotación Sql que contiene las sentencias SQL de inserción equivalentes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
packageio.github.picodotdev.blogbitix.testcontainers;importjava.util.List;importorg.junit.jupiter.api.Test;importorg.junit.jupiter.api.AfterEach;import staticorg.junit.jupiter.api.Assertions.*;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;importorg.springframework.test.context.jdbc.Sql;importorg.springframework.test.context.ContextConfiguration;importorg.springframework.beans.factory.annotation.Autowired;@SpringBootTest@ContextConfiguration(initializers={DefaultPostgresContainer.Initializer.class})publicclassPersonRepositoryTest{    @Autowired    privatePersonRepositorypersonRepository;    @AfterEach    voidafterEach(){        personRepository.deleteAll();    }    @Test    voidrepositoryPersonCount(){        // given
        List<Person>persons=List.of(newPerson("James Gosling"),newPerson("Linus Torvalds"),newPerson("Richard Stallman"),newPerson("Bill Gates"),newPerson("Steve Jobs"),newPerson("Dennis Ritchie"));        personRepository.saveAll(persons);        // then
        assertEquals(persons.size(),personRepository.count());    }    @Test    @Sql("/sql/persons.sql")    voidsqlPersonCount(){        // then
        assertEquals(6,personRepository.count());    }}
PersonRepositoryTest.java
1
2
3
4
5
6
insertintoPerson(id,name)values(1,'James Gosling');insertintoPerson(id,name)values(2,'Linus Torvalds');insertintoPerson(id,name)values(3,'Richard Stallman');insertintoPerson(id,name)values(4,'Bill Gates');insertintoPerson(id,name)values(5,'Steve Jobs');insertintoPerson(id,name)values(6,'Dennis Ritchie');
persons.sql

En la salida de trazas de los teses se observa como Testcontainers inicia el contenedor PostgreSQL y como los teses generan las sentencias SQL de insert, count y delete para eliminar los datos del fixture.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
...

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.0.RELEASE)

2020-06-07 09:49:25.003  INFO 4244 --- [    Test worker] tAndSystemPropertyClientProviderStrategy : Found docker client settings from environment
2020-06-07 09:49:25.004  INFO 4244 --- [    Test worker] o.t.d.DockerClientProviderStrategy       : Found Docker environment with Environment variables, system properties and defaults. Resolved dockerHost=unix:///var/run/docker.sock
2020-06-07 09:49:25.120  INFO 4244 --- [    Test worker] org.testcontainers.DockerClientFactory   : Docker host IP address is localhost
2020-06-07 09:49:25.140  INFO 4244 --- [    Test worker] org.testcontainers.DockerClientFactory   : Connected to docker: 
  Server Version: 19.03.11-ce
  API Version: 1.40
  Operating System: Arch Linux
  Total Memory: 31986 MB
...
2020-06-07 09:49:27.387  INFO 4244 --- [tream-916764928] org.testcontainers.DockerClientFactory   : Starting to pull image
2020-06-07 09:49:27.430  INFO 4244 --- [tream-916764928] org.testcontainers.DockerClientFactory   : Pulling image layers:  0 pending,  0 downloaded,  0 extracted, (0 bytes/0 bytes)
2020-06-07 09:49:28.813  INFO 4244 --- [tream-916764928] org.testcontainers.DockerClientFactory   : Pulling image layers:  2 pending,  1 downloaded,  0 extracted, (486 KB/? MB)
2020-06-07 09:49:32.147  INFO 4244 --- [tream-916764928] org.testcontainers.DockerClientFactory   : Pulling image layers:  1 pending,  2 downloaded,  0 extracted, (4 MB/? MB)
2020-06-07 09:49:32.600  INFO 4244 --- [tream-916764928] org.testcontainers.DockerClientFactory   : Pulling image layers:  0 pending,  3 downloaded,  0 extracted, (5 MB/5 MB)
2020-06-07 09:49:32.784  INFO 4244 --- [tream-916764928] org.testcontainers.DockerClientFactory   : Pulling image layers:  0 pending,  3 downloaded,  1 extracted, (5 MB/5 MB)
2020-06-07 09:49:32.869  INFO 4244 --- [tream-916764928] org.testcontainers.DockerClientFactory   : Pulling image layers:  0 pending,  3 downloaded,  2 extracted, (5 MB/5 MB)
2020-06-07 09:49:33.006  INFO 4244 --- [tream-916764928] org.testcontainers.DockerClientFactory   : Pulling image layers:  0 pending,  3 downloaded,  3 extracted, (5 MB/5 MB)
2020-06-07 09:49:33.027  INFO 4244 --- [tream-916764928] org.testcontainers.DockerClientFactory   : Pull complete. 3 layers, pulled in 5s (downloaded 5 MB at 1 MB/s)
2020-06-07 09:49:33.785  INFO 4244 --- [    Test worker] org.testcontainers.DockerClientFactory   : Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2020-06-07 09:49:33.785  INFO 4244 --- [    Test worker] org.testcontainers.DockerClientFactory   : Checking the system...
2020-06-07 09:49:33.786  INFO 4244 --- [    Test worker] org.testcontainers.DockerClientFactory   : ✔︎ Docker server version should be at least 1.6.0
2020-06-07 09:49:34.060  INFO 4244 --- [    Test worker] org.testcontainers.DockerClientFactory   : ✔︎ Docker environment should have more than 2GB free disk space
2020-06-07 09:49:34.070  INFO 4244 --- [    Test worker] 🐳 [postgres:12]                         : Pulling docker image: postgres:12. Please be patient; this may take some time but only needs to be done once.
2020-06-07 09:49:36.059  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Starting to pull image
2020-06-07 09:49:36.064  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  0 pending,  0 downloaded,  0 extracted, (0 bytes/0 bytes)
2020-06-07 09:49:37.729  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers: 13 pending,  1 downloaded,  0 extracted, (1 KB/? MB)
2020-06-07 09:49:44.773  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers: 12 pending,  2 downloaded,  0 extracted, (9 MB/? MB)
2020-06-07 09:49:46.205  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers: 11 pending,  3 downloaded,  0 extracted, (10 MB/? MB)
2020-06-07 09:49:49.709  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers: 10 pending,  4 downloaded,  0 extracted, (14 MB/? MB)
2020-06-07 09:49:50.684  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  9 pending,  5 downloaded,  0 extracted, (16 MB/? MB)
2020-06-07 09:49:51.585  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  8 pending,  6 downloaded,  0 extracted, (17 MB/? MB)
2020-06-07 09:49:57.752  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  7 pending,  7 downloaded,  0 extracted, (25 MB/? MB)
2020-06-07 09:49:58.883  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  6 pending,  8 downloaded,  0 extracted, (26 MB/? MB)
2020-06-07 09:50:00.392  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  5 pending,  9 downloaded,  0 extracted, (28 MB/? MB)
2020-06-07 09:50:01.935  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  4 pending, 10 downloaded,  0 extracted, (30 MB/? MB)
2020-06-07 09:50:03.377  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  3 pending, 11 downloaded,  0 extracted, (32 MB/? MB)
2020-06-07 09:50:04.884  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  2 pending, 12 downloaded,  0 extracted, (34 MB/? MB)
2020-06-07 09:50:23.813  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  1 pending, 13 downloaded,  0 extracted, (58 MB/? MB)
2020-06-07 09:50:24.979  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  1 pending, 13 downloaded,  1 extracted, (59 MB/? MB)
2020-06-07 09:50:25.172  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  1 pending, 13 downloaded,  2 extracted, (60 MB/? MB)
2020-06-07 09:50:25.263  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  1 pending, 13 downloaded,  3 extracted, (60 MB/? MB)
2020-06-07 09:50:25.397  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  1 pending, 13 downloaded,  4 extracted, (60 MB/? MB)
2020-06-07 09:50:25.819  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  1 pending, 13 downloaded,  5 extracted, (61 MB/? MB)
2020-06-07 09:50:25.899  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  1 pending, 13 downloaded,  6 extracted, (61 MB/? MB)
2020-06-07 09:50:25.957  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  1 pending, 13 downloaded,  7 extracted, (61 MB/? MB)
2020-06-07 09:50:26.020  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  1 pending, 13 downloaded,  8 extracted, (61 MB/? MB)
2020-06-07 09:51:02.496  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  0 pending, 14 downloaded,  8 extracted, (108 MB/108 MB)
2020-06-07 09:51:05.070  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  0 pending, 14 downloaded,  9 extracted, (108 MB/108 MB)
2020-06-07 09:51:05.160  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  0 pending, 14 downloaded, 10 extracted, (108 MB/108 MB)
2020-06-07 09:51:05.241  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  0 pending, 14 downloaded, 11 extracted, (108 MB/108 MB)
2020-06-07 09:51:05.303  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  0 pending, 14 downloaded, 12 extracted, (108 MB/108 MB)
2020-06-07 09:51:05.363  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  0 pending, 14 downloaded, 13 extracted, (108 MB/108 MB)
2020-06-07 09:51:05.446  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pulling image layers:  0 pending, 14 downloaded, 14 extracted, (108 MB/108 MB)
2020-06-07 09:51:05.464  INFO 4244 --- [tream-812574485] 🐳 [postgres:12]                         : Pull complete. 14 layers, pulled in 89s (downloaded 108 MB at 1 MB/s)
2020-06-07 09:51:05.475  INFO 4244 --- [    Test worker] 🐳 [postgres:12]                         : Creating container for image: postgres:12
2020-06-07 09:51:05.629  INFO 4244 --- [    Test worker] 🐳 [postgres:12]                         : Starting container with ID: 7f7754ae9a7903583c6a0b1f3cde15a651f0c5fb4394491e1f73a83d9b812c6c
2020-06-07 09:51:06.209  INFO 4244 --- [    Test worker] 🐳 [postgres:12]                         : Container postgres:12 is starting: 7f7754ae9a7903583c6a0b1f3cde15a651f0c5fb4394491e1f73a83d9b812c6c
2020-06-07 09:51:07.193  INFO 4244 --- [    Test worker] 🐳 [postgres:12]                         : Container postgres:12 started in PT1M42.629448S
2020-06-07 09:51:07.202  INFO 4244 --- [    Test worker] i.g.p.b.t.PersonRepositoryTest           : Starting PersonRepositoryTest on archlinux with PID 4244 (started by picodotdev in ../blog-ejemplos/Testcontainers)
2020-06-07 09:51:07.204  INFO 4244 --- [    Test worker] i.g.p.b.t.PersonRepositoryTest           : No active profile set, falling back to default profiles: default
2020-06-07 09:51:07.564  INFO 4244 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFERRED mode.
2020-06-07 09:51:07.612  INFO 4244 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 42ms. Found 1 JPA repository interfaces.
2020-06-07 09:51:07.950  INFO 4244 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-06-07 09:51:08.021  INFO 4244 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2020-06-07 09:51:08.711  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM public.databasechangeloglock
2020-06-07 09:51:08.726  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : CREATE TABLE public.databasechangeloglock (ID INTEGER NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP WITHOUT TIME ZONE, LOCKEDBY VARCHAR(255), CONSTRAINT DATABASECHANGELOGLOCK_PKEY PRIMARY KEY (ID))
2020-06-07 09:51:08.729  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM public.databasechangeloglock
2020-06-07 09:51:08.732  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : DELETE FROM public.databasechangeloglock
2020-06-07 09:51:08.733  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : INSERT INTO public.databasechangeloglock (ID, LOCKED) VALUES (1, FALSE)
2020-06-07 09:51:08.735  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : SELECT LOCKED FROM public.databasechangeloglock WHERE ID=1
2020-06-07 09:51:08.741  INFO 4244 --- [    Test worker] l.lockservice.StandardLockService        : Successfully acquired change log lock
2020-06-07 09:51:09.808  INFO 4244 --- [    Test worker] l.c.StandardChangeLogHistoryService      : Creating database history table with name: public.databasechangelog
2020-06-07 09:51:09.809  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : CREATE TABLE public.databasechangelog (ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED TIMESTAMP WITHOUT TIME ZONE NOT NULL, ORDEREXECUTED INTEGER NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35), DESCRIPTION VARCHAR(255), COMMENTS VARCHAR(255), TAG VARCHAR(255), LIQUIBASE VARCHAR(20), CONTEXTS VARCHAR(255), LABELS VARCHAR(255), DEPLOYMENT_ID VARCHAR(10))
2020-06-07 09:51:09.812  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM public.databasechangelog
2020-06-07 09:51:09.813  INFO 4244 --- [    Test worker] l.c.StandardChangeLogHistoryService      : Reading from public.databasechangelog
2020-06-07 09:51:09.813  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : SELECT * FROM public.databasechangelog ORDER BY DATEEXECUTED ASC, ORDEREXECUTED ASC
2020-06-07 09:51:09.815  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : SELECT COUNT(*) FROM public.databasechangeloglock
2020-06-07 09:51:09.823  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : CREATE TABLE public.department (id INTEGER NOT NULL, name VARCHAR(50) NOT NULL, active BOOLEAN DEFAULT TRUE, CONSTRAINT DEPARTMENT_PKEY PRIMARY KEY (id))
2020-06-07 09:51:09.825  INFO 4244 --- [    Test worker] liquibase.changelog.ChangeSet            : Table department created
2020-06-07 09:51:09.826  INFO 4244 --- [    Test worker] liquibase.changelog.ChangeSet            : ChangeSet classpath:/db/changelog/db.changelog-master.xml::1::bob ran successfully in 4ms
2020-06-07 09:51:09.826  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : SELECT MAX(ORDEREXECUTED) FROM public.databasechangelog
2020-06-07 09:51:09.828  INFO 4244 --- [    Test worker] liquibase.executor.jvm.JdbcExecutor      : INSERT INTO public.databasechangelog (ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, MD5SUM, DESCRIPTION, COMMENTS, EXECTYPE, CONTEXTS, LABELS, LIQUIBASE, DEPLOYMENT_ID) VALUES ('1', 'bob', 'classpath:/db/changelog/db.changelog-master.xml', NOW(), 1, '8:47afc11dcd196aca25eebfad16683784', 'createTable tableName=department', '', 'EXECUTED', NULL, NULL, '3.8.9', '1516269815')
2020-06-07 09:51:09.831  INFO 4244 --- [    Test worker] l.lockservice.StandardLockService        : Successfully released change log lock
...
Hibernate: create sequence hibernate_sequence start 1 increment 1
Hibernate: create table person (id int8 not null, name varchar(255), primary key (id))
...
Hibernate: select nextval ('hibernate_sequence')
Hibernate: select nextval ('hibernate_sequence')
Hibernate: select nextval ('hibernate_sequence')
Hibernate: select nextval ('hibernate_sequence')
Hibernate: select nextval ('hibernate_sequence')
Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: insert into person (name, id) values (?, ?)
Hibernate: select count(*) as col_0_0_ from person person0_
Hibernate: delete from person
Hibernate: select count(*) as col_0_0_ from person person0_
Hibernate: delete from person
System.out

Si fuera necesario Liquibase permite aplicar cambios en la base con archivos de migración, por ejemplo, para crear algunas tablas, procedimientos almacenados o crear los datos básicos. En el ejemplo el archivo de cambios en base de datos incluye la tabla Department. En las trazas se ve la SQL de creación de la tabla, para las pruebas del ejemplo no es necesario pero suele ser una funcionalidad necesaria en un proyecto real.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?><databaseChangeLogxmlns="http://www.liquibase.org/xml/ns/dbchangelog"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
         http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">    <changeSetid="1"author="picodotdev">        <createTabletableName="department">            <columnname="id"type="int">                <constraintsprimaryKey="true"nullable="false"/>            </column>            <columnname="name"type="varchar(50)">                <constraintsnullable="false"/>            </column>            <columnname="active"type="boolean"defaultValueBoolean="true"/>        </createTable>    </changeSet></databaseChangeLog>
db.changelog-master.xml
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew test

Variable not found: Enlaces interesantes 407

$
0
0
Enlaces interesantes

El (desconocido, al menos para mí) código de estado HTTP 407 es enviado al cliente cuando la petición que intentó no pudo ser completada debido a que un proxy requiere autenticación. Cuando este error se recibe, viene acompañado de un encabezado Proxy-Authenticate con información sobre cómo autenticarse.

Y ahora, ahí van los enlaces recopilados durante la semana pasada, que espero que os resulten interesantes. :-)

Por si te lo perdiste...

.NET Core / .NET

ASP.NET Core / ASP.NET

Azure / Cloud

    Data

    Machine learning / IA / Bots

    Web / HTML / CSS / Javascript

    Visual Studio / Complementos / Herramientas

    Xamarin

    Otros

    Publicado en Variable not found.

    Variable not found: Capturar todos los parámetros enviados a un componente Blazor

    $
    0
    0
    BlazorNormalmente un componente Blazor declara parámetros para permitir que otros componentes o páginas que lo utilicen puedan pasarle la información que necesita para funcionar.

    Por ejemplo, el siguiente código en el interior de un componente definiría un par de propiedades llamadas Text y Repeat, que utiliza para repetir un texto tantas veces como se le indique:
    @* File: Repeater.razor *@
    @for (var i = 0; i < Repeat; i++)
    {
    <p>@Text</p>
    }

    @code {
    [Parameter]
    public string Text { get; set; }
    [Parameter]
    public int Repeat { get; set; }
    }
    De esta forma, podríamos utilizar el componente desde otros puntos de la aplicación suministrándole los valores necesarios en cada caso:
    <Repeater Text="Hola" Repeat="6" />
    Sin embargo, alguna vez podría resultar interesante poder obtener todos los parámetros que se han enviado a un componente, pero sin necesidad de conocerlos previamente.

    Para ello, Blazor dispone de una especie de mecanismo catch all que nos permite tener acceso a todos los parámetros suministrados al utilizar un componente. Esto lo conseguimos estableciendo a cierto el valor del parámetro CaptureUnmatchedValues al definir un parámetro de tipo Dictionary<string, object>:
    @* File: Capturer.razor *@
    <ul>
    @if (ExtraParams != null)
    {
    foreach (var (key, value) in ExtraParams)
    {
    <li>@key = @value (@value.GetType().Name)</li>
    }
    }
    </ul>

    @code {
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> ExtraParams { get; set; }
    }
    Obviamente, en un componente sólo puede existir un parámetro configurado de esta manera.
    Así, al utilizar el componente anterior de la siguiente forma...
    <Capturer Key1="Value1" Key2="1" Key3 />
    ... el resultado obtenido sería el siguiente:
    • Key1 = Value1 (String)
    • Key2 = 1 (String)
    • Key3 = True (Boolean). Fijaos que este caso es especial porque se trata de un atributo booleano: su simple presencia ya lo establece a cierto.
    Es importante tener en cuenta que los parámetros suministrados que mapeen correctamente a los declarados en el componente no los recibiremos a través de este diccionario. Es decir, sólo recibiremos los parámetros suministrados que no coincidan con parámetros declarados en el componente.

    Por ejemplo, imaginad que los parámetros de nuestro componente se definen de la siguiente manera:
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> ExtraParams { get; set; }

    [Parameter]
    public string Text { get; set; }
    En este caso, si instanciamos el componente de la siguiente manera, el valor de Text será establecido correctamente, mientras que en ExtraParams sólo encontraremos el valor de Foo:
    <MyComponent Text="Hola" Foo="Bar" />
    También es interesante saber que este diccionario con parámetros arbitrarios puede ser utilizado para establecer posteriormente los atributos de otro componente usando la directiva @attributes. Por ejemplo, imaginad que queremos implementar un componente llamado InputWithLabel que encapsule un tag <input> con su correspondiente etiqueta, y que podamos utilizar de la siguiente forma:
    <InputWithLabel Text="Enter your full name" type="text" maxlength="50"></InputWithLabel>
    En este caso, lo que nos interesaría hacer en InputWithLabel.razor sería establecer una propiedad para recuperar el valor de Text, y luego un diccionario para el resto de parámetros, que serían asignados tal cual al <input>. El código podría ser como el siguiente:
    @* File: InputWithLabel.razor *@
    <label>@Text</label>
    <input @attributes="Params" />

    @code {
    [Parameter]
    public string Text { get; set; }

    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> Params { get; set; }
    }
    ¡Espero que os sea útil!

    Publicado en Variable not found.

    Variable not found: Cómo mostrar y ocultar elementos en Blazor

    $
    0
    0
    BlazorEn este post vamos a echar un vistazo a algo muy básico, pero que seguro que ayuda a los que estáis comenzando con Blazor para haceros una mejor idea su funcionamiento: cómo mostrar u ocultar elementos en la página.

    La idea sería conseguir que, en el siguiente componente, la pulsación del botón etiquetado como "Toggle" haga que el bloque <div> donde se encuentra el "Hello world!" se oculte y se muestre sucesivamente:
    <h1>Visibility demo</h1>
    <div>
    <h2>Hello world!</h2>
    </div>

    <button class="btn btn-primary" @onclick="Toggle">Toggle</button>

    @code {
    private bool _isVisible = true;

    private void Toggle()
    {
    _isVisible = !_isVisible;
    }
    }
    Como es de esperar, al ejecutar este código veremos que cuando el elemento está visible aparece tanto en la página como en los elementos de la misma (es decir, en el DOM):

    El elemento aparece en la página y en el DOM

    Como en otros marcos de trabajo para la construcción de webs SPA, con Blazor podemos utilizar distintas técnicas para mostrar u ocultar elementos:
    • Añadiendo o eliminando físicamente el elemento del DOM.
    • Utilizando estilos en línea que modifiquen su visibilidad.
    • Aplicando clases CSS que modifiquen su visibilidad.

    Añadir o eliminar físicamente el elemento del DOM

    En este caso, lo que pretendemos conseguir es que el elemento sólo sea incluido físicamente en la página cuando _isVisible sea establecido a true.

    Esto es sencillo de hacer con la sintaxis Razor que, como sabemos, permite combinar código C# con HTML utilizando el carácter de escape "@", por lo que podemos utilizar la instrucción condicional if de la siguiente manera:
    <h1>Visibility demo</h1>
    @if (_isVisible)
    {
    <div>
    <h2>Hello world!</h2>
    </div>
    }
    De esta forma, cuando el elemento esté visible y pulsemos el botón, se volverá a renderizar el contenido y el  @if  hará que ese bloque no sea incluido en el DOM (ni, por tanto, en la página):

    El elemento no aparece en la página ni en el DOM

    Al usar esta técnica hay que tener en cuenta algunas cosas, sobre todo si el bloque de código que eliminamos contiene en su interior otros componentes Blazor:
    • Todos los componentes en el interior del bloque eliminado serán destruidos, con toda la información de estado almacenada en ellos.

    • Al incluir el contenido de nuevo, los componentes del interior del bloque deberán ser instanciados e inicializados, lo cual puede llevar algún tiempo si la inicialización es compleja o depende de algún servicio externo (APIs, bases de datos, etc.)

    Utilizando estilos en línea que modifiquen su visibilidad

    Otra posibilidad bastante socorrida suele ser modificar sobre la marcha el atributo style del elemento, asignándole un display: none (o similar) cuando _isVisible sea falso:
    <h1>Visibility demo</h1>
    <div style="@(_isVisible? string.Empty: "display: none")">
    <h2>Hello world!</h2>
    </div>
    Al pulsar el botón para ocultar el elemento, el resultado que obtendríamos sería el siguiente:

    El elemento no aparece visible en la página pero sí en el DOM

    La diferencia principal con el enfoque anterior es que en este caso el bloque <div> sí se encontraría en el DOM, y su contenido ocuparía memoria aunque en la página fuera visible. Es decir, los objetos no serían destruidos.

    Aplicando clases CSS que modifiquen su visibilidad

    Como una variante más limpia que la anterior, y que además facilita la inclusión de efectos como animaciones, sería asignar una clase CSS al elemento en función de su visibilidad, como en el siguiente ejemplo:
    <h1>Visibility demo</h1>
    <div class="@(_isVisible? "visible": "invisible")">
    <h2>Hello world!</h2>
    </div>
    Si acompañamos este código con un CSS como el siguiente, conseguiremos que el elemento aparezca y desaparezca con una suave animación:
    <style>
    .visible, .invisible {
    transition: all 1s ease-in-out;
    }

    .visible {
    opacity: 1;
    }

    .invisible {
    opacity: 0;
    }
    </style>
    A nivel de DOM conseguiríamos un efecto parecido al anterior: el elemento existirá siempre en el DOM independientemente de su visibilidad:

    El elemento no aparece visible en la página pero sí en el DOM

    ¡Espero que os sea de ayuda!

    Publicado en Variable not found.

    Arragonán: Mejorando la velocidad de la suite de tests

    $
    0
    0

    Cuando tienes la buena costumbre de escribir tests, ya sea antes o después, vas generando una red de seguridad que nos da feedback para saber que los escenarios cubiertos siguen funcionando correctamente. Eso nos facilita el cambiar el código con mayor confianza ya sea para refactorizar o añadir alguna nueva funcionalidad.

    Ese ciclo de feedback debería ser razonablemente corto, debería ayudarnos a concentrarnos y fluir en lo que estemos haciendo en ese momento. Si resulta lento seguramente acabaremos poniéndonos a hacer otras cosas mientras esperamos y seremos menos efectivos.

    Tira cómica de XKCD de Compiling

    En las suites de tests normalmente los principales cuellos de botella, además de los end-to-end, suelen ser los que hacen uso de sistemas externos como pueden ser la base de datos o el sistema de ficheros. Por eso, en la medida de lo posible, es saludable que las suites de tests traten de parecerse a la pirámide de test.

    Pero aún así los proyectos van creciendo y de la mano lo hacen las suites de tests, por lo que ese feedback irremediablemente se va ralentizando. Hay soluciones que minimizan el problema, como usar herramientas que ejecutan algunos tests al guardar el código o usar tags en los tests para ejecutar sólo un subconjunto de ellos.

    Pero eso no nos resolverá la raíz de los problemas, puede que incluso los enmascare. Lo mejor es dedicar un tiempo en analizar la suite de tests para detectar qué origina que sea lenta y ver si vale la pena intentar mejorarlo.

    Medir y mejorar

    Por ejemplo hace poco trabajando en el backend de Devengo, donde tenemos costumbre de escribir bastantes tests, tenía la percepción de que en unas pocas semanas los tiempos de ejecución de la suite de tests que ejecuta conjuntamente los unitarios y de integración habían degradado mucho. Tenía la hipótesis de que revisando los tests de integración podríamos encontrar puntos donde optimizar y obtener feedback antes.

    Podría haberme puesto a lo loco a cambiar tests e ir probando si bajaban los tiempos, pero suele ser más productivo utilizar herramientas que te ayuden a detectar las fuentes de los problemas y tomar decisiones en base a los datos que te dan. Así que empecé dedicando algunos ratos de holgura en analizar la suit de tests con TestProf.

    Con TestProf pude comprobar qué tipo de artefactos eran en los que se iba más tiempo y cuáles eran concretamente los tests más lentos, que era información bastante interesante. Pero lo más clarificador fue comprobar cómo nuestros tests hacían uso de las factorías de factory_bot que usamos para preparar los tests.

    Evidenciaba que el uso de las associations estaban provocando que se estuvieran creando en cascada muchos elementos en la base de datos y que había unas pocas factorías en las que se iba notablemente más tiempo. Ya con esos KPIs es mucho más fácil tomar decisiones de por dónde empezar.

    En la primera sentada decidí no tocar el código de producción y reducir el número de elementos que se creaban en cascada, ya que en muchos casos no eran necesarios. Así que los cambios fueron básicamente dos:

    • Minimizar el número de elementos en algunos usos de create_list para que esos tests cubrieran sólo casos límite.
    • Cambiar un buen número de tests para que en la propia preparación se crearan elementos de las associations usando el método create de forma explícita, de ese modo al pasar sus referencias a las factorías se evitaban generar muchas de las creaciones en cascada.

    Eso supuso un reducción de entre 15-20% del tiempo. Aún siendo una mejora sustancial había mucho margen de mejora porque el mayor cuello de botella seguía siendo los tiempos de una factoría en concreto.

    Tal y como me indicó Iván, estaba claro el principal sospechoso, ya que teníamos anotado un concern de deuda técnica desde hacía tiempo: una dependencia en un callback de ActiveRecord que había que quitar y mover a un sitio más apropiado. Así que en la segunda sentada trabajé en ese refactor y una vez hecho redujo significativamente los tiempos de ejecución, pasando de lanzar la suite en casi 2 minutos a algo menos de 30 segundos en mi máquina.

    Conclusiones

    Trata de buscar herramientas que te ayuden a medir antes de hacer cambios, es difícil ser consciente de si un cambio provoca mejoras sin ninguna referencia. De hecho, en algunos contextos incluso podría valer la pena instrumentalizar el efecto que tienen este tipo de mejoras en la productividad de los equipos.

    A más rápido siempre es mejor pero, igual que con el porcentaje de cobertura, tampoco creo que haya que andar obsesionándose con ello. Así que preocúpate (de nuevo igual que con la baja cobertura) cuando la lentitud de tu suite te esté suponiendo un obstáculo para el flujo en tu trabajo.

    Y recuerda que la suite de tests que vamos dejando tiene que aportar valor como red de seguridad para introducir cambios en el código. Así que ante todo debería ayudarnos a documentar lo que hace (y cómo está diseñado) el software y ser expresiva cuando falla.


    Más referencias


    Coding Potions: Vue directives - Guía completa de uso

    $
    0
    0

    Introducción ¿qué son las directivas?

    Las directivas son unos atributos que puedes añadir en el HTML de los componentes que permiten controlar el elemento DOM sobre el que se colocan.

    Si ya has usando Vue estoy seguro ya hayas usado directivas, aunque no lo sabías. Cuando usas el v-model y el v-show realmente lo que estás usando son directivas, auque estas vienen ya creadas con Vue.

    Como dentro de una directiva tienes acceso al elemento DOM sobre el que se aplica, las posibilidades son casi infinitas. Puedes ejecutar código javascript directamente al elemento del DOM y además puedes personalizar cada directiva porque admiten el paso de parámetros.

    En el artículo anterior (Cómo se usan los filters de Vue) hablamos de los filters y de cómo se usan insertándolos en los datos de la vista, pero las directivas son totalmente distintas, auque también se usan dentro de la vista de los componentes, las directivas se ponen como atributo del HTML y no dentro de los datos que se pintan. Ahora veremos esto con más detalle.

    ¿Cómo se crean las directivas?

    Lo primero que tienes que saber, es que las directivas siempre se usan poniendo dentro de la etiqueta HTML v- seguido del nombre de la directiva.

    Primero vamos a crear la directiva más básica, una que no recibe nada ni hace nada. Yo lo que suelo hacer es crear un fichero llamado direcives.js para tener todas las directivas localizadas, ya que no aconstumbro a tener muchas.

    Por ejemplo, vamos a crear la directiva esta que no hace nada, la voy a llamar void:

    // src/directives.jsimportVuefrom'vue'Vue.directive('void',function(el){console.log("Directiva");})

    Para usar este fichero de directivas lo tienes que importar en el main.js:

    // /src/main.jsimportVuefrom'vue'importAppfrom'./App'import"./filters"newVue({el:'#app',template:'<App/>',components:{App},})

    Para usar esta directiva que hemos creado tan solo tienes que poner dentro de la etiqueta HTML del componente:

    <template><divclass="component"><divv-void></div></div></template>

    Recuerda que siempre tienes que poner delante del nombre de la directiva el v-.

    Como ves, la directiva es una función que recibe un parámetro el. Este parámetro es el elemento DOM sobre el que se apica la directiva.

    Hooks de las directivas

    Las directivas tienen sus propios hooks, es decir, tienen una serie de funciones para controlar su ciclo de vida, veamos cuáles son:

    • bind: Se ejecuta en el momento en el que la directiva se añade al elemento del DOM. Sería como el created() de los componentes
    • inserted: Cuando se ha insertado en el DOM el elemento sobre el que se aplica esta directiva. Como el mounted() de los componentes.
    • updated: Cuando cambia el elemento del DOM sobre el que está aplicada la directiva.
    • componentUpdated: Cuando todo el componente y el hijo han terminado de ser actualizados.
    • unbind: Cuando la directiva es eliminada de ese elemento del DOM.

    En el anterior ejemplo la directiva era una función. Para usar los hooks es necesario que la directiva sea un objeto con los hooks que quieres usar.

    Veamos un ejemplo de directiva llamada focus que lo que hace es poner el foco en el navegador sobre el elemento en el que se inserta la directiva. Para ello uso el hook bind para porder hacer el focus al crearse el elmento en el DOM:

    Vue.directive('focus',{// Cuando el elemento enlazado se inserta en el DOM...inserted:function(el){// Enfoca el elementoel.focus()}})

    Parámetros de los hooks

    Cada uno de los hooks antes mencionados pueden recibir como parámetro de la función lo siguiente:

    • el: El elemento del DOM sobre el que se aplica la directiva
    • binding: Objeto solo de lectura que contiene información que puede recibir la directiva
    • vnode: Objeto solo de lectura que contiene el nodo del virtual dom asociado a la directiva
    Vue.directive('hooks',{bind:function(el,binding,vnode){console.log(el);console.log(binding);console.log(vnode);}})

    Pasar información a las directivas

    Como hemos dicho antes, v-show y v-if son directivas por lo que el paso de infotmación a las directivas es igual a estas dos. Todo lo que pongas dentro al llamar a la directiva es código javascript, métodos o variables y propiedades computadas. Por ejemplo

    <divv-color="'red'"></div><divv-color="getColor()"></div>

    Ahora bien, para usar este valor que pasas dentro de la directiva, tienes que usar el parámetro binding visto anteriormente. Dentro de este parámetro puedes acceder a su propiedad value para recoger el valor que pasas desde la directiva. Por ejemplo:

    Vue.directive("color",{inserted:function(el,binding,vnode){el.style.color=binding.value;}})

    Argumentos

    Las directivas de Vue también aceptan argumentos. Los argumentos en vue se usan poniendo dos puntos después del nombre de la directiva. Por ejemplo:

    <app-navigationv-sticky:bottom></app-navigation>

    Para usar este valor dentro de la directiva tenemos que usar la propiedad arg que viene dentro del objeto binding:

    Vue.directive("sticky",function(el,binding,vnode){console.log(binding.arg);});

    Modificadores

    También puedes usar modificadores en las directivas. Para usarlos es simplemente poniendo un punto después del nombre de la directiva y a continuación el nombre del modificador:

    <divv-test.foo></div>

    Dentro de la directiva, puedes recoger los modificadores así:

    Vue.directive("sticky",function(el,binding,vnode){constmodifiers=binding.modifiers;console.log(modifiers);});

    Los modificadores normalmente se usan para eventos o directivas nativas, por ejemplo: click.stop.

    Directivas interesantes

    v-click-outside

    Sirve para detectar cuando se hace click fuera de un elemento del HTML. Esta directiva suele ser especialmente útiles en popups. Cuando el usuario hace click fuera del popup puedes ejecutar una función para ocultarlo:

    <divv-click-outside="onClickOutside"></div>

    https://github.com/ndelvalle/v-click-outside

    v-hotkey

    Sirve para poder hacer bindeos de teclas. Por ejemplo puedes hacer que al pulsar Esc se cierre un popup o que con cierta combinación de teclas ocurra algo:

    <template><spanv-hotkey="keymap"v-show="show"> Press `ctrl + esc` to toggle me! Hold `enter` to hide me! </span></template><script>
    export default {data(){return{show:true}},methods:{toggle(){this.show=!this.show},show(){this.show=true},hide(){this.show=false}},computed:{keymap(){return{// 'esc+ctrl' is OK.'ctrl+esc':this.toggle,'enter':{keydown:this.hide,keyup:this.show}}}}}</script>

    https://github.com/Dafrok/v-hotkey

    v-clipboard

    Simplemente añade la funcionalidad de copiar al portapapeles cuando se pulsa el botón:

    <buttonv-clipboard="value">Copytoclipboard</button>
    

    v-lazyload

    Para cargar perezosamente imágenes, es decir, no se cargan hasta que no aparecen en pantalla.

    <imgv-lazy="https://www.domain.com/image.jpg">

    Conclusiones

    Las directivas pueden llegar a ser muy potentes y te pueden salvar más de una vez de tener que añadir más código. Lo bueno como siempre, es que son reutilizables y vas a hacer que el código sea más mantenible.

    Por cierto, que se me olvidaba, puedes declarar directivas de forma local también dentro de un solo componente, de tal forma que solo sea accesible para ese componente en particular. Para ello tienes que crear la directiva o importarla dentro de la propiedad directives del componente.

    Bitácora de Javier Gutiérrez Chamorro (Guti): PSeInt

    $
    0
    0


    Después del lenguaje Scratch, seguimos hablando un poco sobre programación, hoy con un software hispano escrito por el argentino Pablo Novara. Su nombre es PSeInt, la abreviatura de PSeudo Intérprete, y su cometido es precisamente ese, un intérprete de lenguaje pseudocódigo, que además cuenta con algunas ayudas para los estudiantes. Personalmente nunca me ha gustado …

    PSeInt Leer más »



    Artículo publicado originalmente en Bitácora de Javier Gutiérrez Chamorro (Guti)

    Blog Bitix: El patrón de diseño Specification, ejemplo de implementación y uso en JPA con Spring Data

    $
    0
    0

    Los métodos de búsqueda y consulta permiten recuperar objetos de las bases de datos según los criterios deseados. Dependiendo del tamaño de la aplicación y sus casos de uso el número de consultas será más o menos grande. Con un número de consultas grande estas se vuelven complejas de mantener y generan duplicación de lógica de negocio. Para simplificar el mantenimiento de un número grande de consultas y evitar duplicidad de lógica de negocio una solución es implementar el patrón de diseño Specification.

    Java

    Spring

    Dado un objeto suele ser necesario comprobar si cumple una o más condiciones. Estas condiciones pueden implementarse tanto en un método del objeto como en la lógica de persistencia en la base de datos.

    Esta aproximación tiene dos inconvenientes, el número de métodos de consulta crece significativamente en las aplicaciones grandes y las consultas son conjunto fijo sin posibilidad de extensión salvo añadir nuevos métodos, las consultas no son fáciles de externalizar y reutilizar.

    En estos casos implementar el patrón de diseño Specification ayuda a hacer el código más mantenible, extensible, simple y de más fácil lectura.

    Los siguientes ejemplos implementan el patrón Specification para comprobar si un objeto cumple una serie de condiciones de negocio y como Spring Data hace uso del patrón para construir las condiciones de las consultas de JPA. Los ejemplos incluyen teses que usan la herramienta TestConainers para hacer pruebas de integración en Java con la base de datos PostgreSQL en un contenedor Docker.

    El problema en las consultas

    Suponiendo que se tiene la siguiente entidad del dominio con una serie de campos la idea primera y más directa para implementar si un producto cumple una serie de condiciones es añadir métodos en las clases, un método por cada condición. Por ejemplo, para buscar los productos que que son baratos, tienen un tiempo largo de existencia o un sobrestock.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product;...@Entity@Table(name="Product")publicclassProductimplementsSpecificable<Product>{    ...    @Id    @GeneratedValue    privateLongid;    privateStringname;    privateLocalDatedate;    privateBigDecimalprice;    privateIntegerunits;    publicProduct(){    }    publicProduct(Stringname,LocalDatedate,BigDecimalprice,Integerunits){        this.name=name;        this.date=date;        this.price=price;        this.units=units;    }    ...    publicbooleanisCheap(){        return...;    }    publicbooleanisLongTerm(){        return...;    }    publicbooleanisOverstock(){        return...;    }}
    Product-1.java

    Esta aproximación sencilla de implementar y suficiente en aplicaciones pequeñas tiene dos inconvenientes. El número de métodos a escribir crece significativamente para aplicaciones grandes o complejas y los criterios de los métodos de consulta son fijos, no son extensibles. Para solventar estos dos problemas se opta por crear métodos con los criterios individuales y se combinan entre ellos dinámicamente para obtener la consulta deseada.

    Aquí es donde el patrón de diseño Specification es de utilidad. Este patrón también es aplicable a las consultas presentes en las clases repositorio de acceso a la base de datos donde seguramente es más probable repetir la misma lógica de condiciones en varias consultas hardcodeado en las SQLs. Con los mismos problemas, condiciones repetidas en varios métodos y proliferación de métodos de consulta. Esta es la razón de que Spring Data implemente el patrón Specification.

    Qué es y ventajas del patrón de diseño Specification

    El patrón de diseño Specification permite encapsular una pieza del conocimiento del dominio y rehusarla en diferentes partes de la aplicación. Utilizándolo se mueven estas reglas de negocio a clases llamadas specifications.

    El patrón de diseño Specification parte de una interfaz con un método a implementar para encapsular la lógica de negocio que comprueba si la condición se cumple.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    packageio.github.picodotdev.blogbitix.patronspecification.specification;publicinterfaceSpecification<T>{    booleanisSatisfied(Tobject);    defaultSpecification<T>and(Specification<T>...specifications){        returnnewAndSpecification(specifications);    }    defaultSpecification<T>or(Specification<T>...specifications){        returnnewOrSpecification(specifications);    }    defaultSpecification<T>not(Specification<T>specification){        returnnewNotSpecification(specification);    }}
    Specification.java

    Por cada condición hay una implementación de la interfaz.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specifications;importjavax.persistence.criteria.CriteriaBuilder;importjavax.persistence.criteria.CriteriaQuery;importjavax.persistence.criteria.Predicate;importjavax.persistence.criteria.Root;publicclassIsCheapSpecificationimplementsSpecification<Product>,org.springframework.data.jpa.domain.Specification<Product>{    privateStringpriceAttributeName;    publicIsCheapSpecification(){        this(null);    }    publicIsCheapSpecification(Stringpath){        this.priceAttributeName=Specifications.getAttributeName(path,"price");    }    @Override    publicbooleanisSatisfied(Productproduct){       returnProduct.CHEAP_PRICE.compareTo(product.getPrice())==1;    }    @Override    publicPredicatetoPredicate(Root<Product>root,CriteriaQuery<?>query,CriteriaBuildercriteriaBuilder){        returncriteriaBuilder.lt(root.get(priceAttributeName),Product.CHEAP_PRICE);    }}
    IsCheapSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specifications;importjavax.persistence.criteria.CriteriaBuilder;importjavax.persistence.criteria.CriteriaQuery;importjavax.persistence.criteria.Predicate;importjavax.persistence.criteria.Root;importjava.time.LocalDate;importjava.time.Period;publicclassIsLongTermSpecificationimplementsSpecification<Product>,org.springframework.data.jpa.domain.Specification<Product>{    privateStringdateAttributeName;    publicIsLongTermSpecification(){        this(null);    }    publicIsLongTermSpecification(Stringpath){        this.dateAttributeName=Specifications.getAttributeName(path,"date");    }    @Override    publicbooleanisSatisfied(Productproduct){       return!Period.between(product.getDate(),LocalDate.now()).minus(Product.LONG_TERM_PERIOD).isNegative();    }    @Override    publicPredicatetoPredicate(Root<Product>root,CriteriaQuery<?>query,CriteriaBuildercriteriaBuilder){        LocalDatelongTermDate=LocalDate.now().minus(Product.LONG_TERM_PERIOD);        returncriteriaBuilder.lessThan(root.get(dateAttributeName),longTermDate);    }}
    IsLongTermSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specifications;importjavax.persistence.criteria.CriteriaBuilder;importjavax.persistence.criteria.CriteriaQuery;importjavax.persistence.criteria.Predicate;importjavax.persistence.criteria.Root;publicclassIsOverstockSpecificationimplementsSpecification<Product>,org.springframework.data.jpa.domain.Specification<Product>{    privateStringpriceAttributeName;    publicIsOverstockSpecification(){        this(null);    }    publicIsOverstockSpecification(Stringpath){        this.priceAttributeName=Specifications.getAttributeName(path,"units");    }    @Override    publicbooleanisSatisfied(Productproduct){       returnproduct.getUnits()>Product.OVERSTOCK_UNITS;    }    @Override    publicPredicatetoPredicate(Root<Product>root,CriteriaQuery<?>query,CriteriaBuildercriteriaBuilder){        returncriteriaBuilder.gt(root.get(priceAttributeName),Product.OVERSTOCK_UNITS);    }}
    IsOverstockSpecification.java

    En el objeto Product se implementa el patrón Visitor con la interfaz Specificable donde cada implementación de la clase Specification trata la lógica y la clase Product solo tiene el método satisfies que invoca a la instancia de specification recibida como parámetro.

    1
    2
    3
    4
    5
    6
    
    packageio.github.picodotdev.blogbitix.patronspecification.specification;publicinterfaceSpecificable<T>{    booleansatisfies(Specification<T>object);}
    Specificable.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specificable;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;...@Entity@Table(name="Product")publicclassProductimplementsSpecificable<Product>{    ...    @Override    publicbooleansatisfies(Specification<Product>specification){        returnspecification.isSatisfied(this);    }    ...}
    Product.java

    Para realizar combinaciones con operaciones lógicas and, or o not se utiliza el patrón Composite. De entre las operaciones básicas solo se muestra la operación equals, sería neesario implementar otro tipo de operaciones como lessThan, greaterThan, contains u otras si es necesario.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specifications;importjavax.persistence.criteria.CriteriaBuilder;importjavax.persistence.criteria.CriteriaQuery;importjavax.persistence.criteria.Predicate;importjavax.persistence.criteria.Root;importjava.beans.PropertyDescriptor;publicclassEqualsSpecification<T>implementsSpecification<T>,org.springframework.data.jpa.domain.Specification<T>{    privateStringproperty;    privateObjectvalue;    privateStringpropertyAttributeName;    publicEqualsSpecification(Stringproperty,Objectvalue){        this(property,value,null);    }    publicEqualsSpecification(Stringproperty,Objectvalue,Stringpath){        this.property=property;        this.value=value;        this.propertyAttributeName=Specifications.getAttributeName(path,property);    }    @Override    publicbooleanisSatisfied(Tproduct){        try{            PropertyDescriptordescriptor=newPropertyDescriptor(property,Product.class);            Objectv=descriptor.getReadMethod().invoke(product);            if(v==value){                returntrue;            }            if(v!=null&&value==null||v==null&&value!=null){                returnfalse;            }            returnvalue.equals(v);        }catch(Exceptione){            thrownewRuntimeException(e);        }    }    @Override    publicPredicatetoPredicate(Root<T>root,CriteriaQuery<?>query,CriteriaBuildercriteriaBuilder){        returncriteriaBuilder.equal(root.get(propertyAttributeName),value);    }}
    EqualsSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    packageio.github.picodotdev.blogbitix.patronspecification.specification;importjava.util.Arrays;importjava.util.List;publicclassAndSpecification<T>implementsSpecification<T>{    privateList<Specification<T>>specifications;    publicAndSpecification(Specification<T>...specifications){        this.specifications=Arrays.asList(specifications);    }    publicbooleanisSatisfied(Tobject){        returnspecifications.stream().allMatch(s->{returns.isSatisfied(object);});    }}
    AndSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    packageio.github.picodotdev.blogbitix.patronspecification.specification;importjava.util.Arrays;importjava.util.List;publicclassOrSpecification<T>implementsSpecification<T>{    privateList<Specification<T>>specifications;    publicOrSpecification(Specification<T>...specifications){        this.specifications=Arrays.asList(specifications);    }    publicbooleanisSatisfied(Tobject){        returnspecifications.stream().anyMatch(s->{returns.isSatisfied(object);});    }}
    OrSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    packageio.github.picodotdev.blogbitix.patronspecification.specification;publicclassNotSpecification<T>implementsSpecification<T>{    privateSpecification<T>specification;    publicNotSpecification(Specification<T>specification){        this.specification=specification;    }    publicbooleanisSatisfied(Tobject){        return!specification.isSatisfied(object);    }}
    NotSpecification.java

    Al implementar el patrón Specification se hace uso de varios patrones:

    • El patrón de diseño Visitor, en el método satisfies de la clase Product realmente se llama al método isSatisfied de la interfaz Specification.
    • El patrón de diseño Composite en las operaciones lógicas and, or y not. Estas condiciones lógicas de agrupación se componente de otras independientemente de contengan una o varias.
    • El patrón de diseño Comamnd, se construye la instancia de Specification a ejecutar y el método satisfies lo ejecuta.
    • El patrón de diseño Builder se utiliza para facilitar la construcción de las condiciones con una API fluida y ocultar las clases concretas que implementan la interfaz Specification. Spring Data lo implementa.

    La siguiente prueba unitaria muestra con código el uso del patrón Specification.

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importjava.math.BigDecimal;importjava.time.LocalDate;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.AndSpecification;importio.github.picodotdev.blogbitix.patronspecification.specification.NotSpecification;importio.github.picodotdev.blogbitix.patronspecification.specification.OrSpecification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importorg.junit.jupiter.api.Test;import staticorg.junit.jupiter.api.Assertions.*;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.util.TestPropertyValues;importorg.springframework.context.ApplicationContextInitializer;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.test.context.ContextConfiguration;@SpringBootTest@ContextConfiguration(initializers={ProductSpecificationTest.Initializer.class})publicclassProductSpecificationTest{    @Test    voidtestEqualsSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now(),newBigDecimal("80.0"),10);        // and
            Specification<Product>specification=newEqualsSpecification("name","Raspberry Pi");        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestIsCheapSpecification(){        // given
            Productproduct=newProduct("Pin",LocalDate.now(),newBigDecimal("1.0"),50);        // and
            Specification<Product>specification=newIsCheapSpecification();        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestIsLongTermSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now().minus(Product.LONG_TERM_PERIOD).minusDays(1),newBigDecimal("80.0"),10);        // and
            Specification<Product>specification=newIsLongTermSpecification();        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestIsOverstockSpecification(){        // given
            Productproduct=newProduct("Pin",LocalDate.now(),newBigDecimal("5.0"),50);        // and
            Specification<Product>specification=newIsOverstockSpecification();        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestIsCheapAndIsLongTermSpecification(){        // given
            Productproduct=newProduct("Pin",LocalDate.now().minus(Product.LONG_TERM_PERIOD).minusDays(1),newBigDecimal("1.0"),50);        // and
            Specification<Product>cheapSpecification=newIsCheapSpecification();        Specification<Product>longTermSpecification=newIsLongTermSpecification();        Specification<Product>specification=newAndSpecification<>(cheapSpecification,longTermSpecification);        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestAndSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now(),newBigDecimal("80.0"),10);        // and
            Specification<Product>specification=newAndSpecification<>(newEqualsSpecification("name","Raspberry Pi"),newEqualsSpecification("price",newBigDecimal("80.0")),newEqualsSpecification("units",10));        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestOrSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now(),newBigDecimal("80.0"),50);        // and
            Specification<Product>specificationA=newOrSpecification<>(newEqualsSpecification("name","Raspberry Pi"),newEqualsSpecification("price",newBigDecimal("1.0")),newEqualsSpecification("units",0));        Specification<Product>specificationB=newOrSpecification<>(newEqualsSpecification("name",""),newEqualsSpecification("price",newBigDecimal("80.0")),newEqualsSpecification("units",0));        Specification<Product>specificationC=newOrSpecification<>(newEqualsSpecification("name",""),newEqualsSpecification("price",newBigDecimal("0.0")),newEqualsSpecification("units",50));        Specification<Product>specificationZ=newOrSpecification<>(newEqualsSpecification("name",""),newEqualsSpecification("price",newBigDecimal("0.0")),newEqualsSpecification("units",0));        // then
            assertTrue(product.satisfies(specificationA));        assertTrue(product.satisfies(specificationB));        assertTrue(product.satisfies(specificationC));        assertFalse(product.satisfies(specificationZ));    }    @Test    voidtestNotSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now(),newBigDecimal("80.0"),10);        // and
            Specification<Product>specification=newNotSpecification<>(newAndSpecification<Product>(newEqualsSpecification("name","Raspberry Pi"),newEqualsSpecification("price",newBigDecimal("80.0")),newEqualsSpecification("units",10)));        // then
            assertFalse(product.satisfies(specification));    }    publicstaticclassInitializerimplementsApplicationContextInitializer<ConfigurableApplicationContext>{        publicvoidinitialize(ConfigurableApplicationContextconfigurableApplicationContext){            TestPropertyValues.of("spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration")                    .applyTo(configurableApplicationContext.getEnvironment());        }    }}
    ProductSpecificationTest.java

    Implementación para JPA con Spring Data

    El proyecto Spring Data para el acceso a bases de datos con JPA implementa el patrón de diseño Specification, la interfaz JpaSpecificationExecutor añade a los repositorios métodos de búsqueda que reciben un argumento de tipo Specification.

    Esta clase Specification transforma las condiciones en un objeto Predicate que es el que JPA usa para las condiciones de la consulta SQL que se genera. La interfaz JpaSpecificationExecutor también añade métodos para hacer búsquedas paginadas y con ordenación.

    Si en el proyecto se utiliza Spring y JPA esta es la opción recomendada, si no se utiliza Spring o se utiliza otra librería de persistencia distinta a JPA se puede realizar una implementación siguiendo los principios del patrón Specification.

    Las clases EqualsSpecification, IsCheapSpecification, IsLongTermSpecification, y IsOverstockSpecification anteriores también implementan la interfaz Specification de Spring Data. Estas clases implmentan dos interfaces distintas para diferentes cosas, para hacer comprobaciones sobre un objeto en memoria y para generar clases Predicate con las condiciones equivalentes de JPA, son símplemente ejemplos y para separar conceptos no estaría mal dividir cada clase en dos para que implementen las interfaces de forma individual.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product;importorg.springframework.data.jpa.repository.JpaSpecificationExecutor;importorg.springframework.data.jpa.repository.Modifying;importorg.springframework.data.jpa.repository.Query;importorg.springframework.data.repository.PagingAndSortingRepository;publicinterfaceProductRepositoryextendsPagingAndSortingRepository<Product,Long>,JpaSpecificationExecutor<Product>{    @Override    @Modifying    @Query("delete from Product")    voiddeleteAll();}
    ProductRepository.java

    La siguiente prueba de integración con Testcontainers, PostgresSQL y Docker prueba el repositorio con las implementaciones de las clases del patrón Specification para JPA de Spring Data.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.domain.product.ProductRepository;importliquibase.pro.packaged.T;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.data.jpa.domain.Specification;importorg.springframework.test.context.ContextConfiguration;importorg.springframework.test.context.jdbc.Sql;importjava.math.BigDecimal;import staticorg.junit.jupiter.api.Assertions.assertEquals;@SpringBootTest@ContextConfiguration(initializers={io.github.picodotdev.blogbitix.testcontainers.DefaultPostgresContainer.Initializer.class})publicclassProductJpaSpecificationTest{    @Autowired    privateProductRepositoryproductRepository;    @Test    @Sql("/sql/products.sql")    voidtestIsCheapSpecification(){        // given
            Specification<Product>specification=newIsCheapSpecification();        // then
            assertEquals(1,productRepository.findAll(specification).size());    }    @Test    @Sql("/sql/products.sql")    voidtestIsLongTermSpecification(){        // given
            Specification<Product>specification=newIsLongTermSpecification();        // then
            assertEquals(1,productRepository.findAll(specification).size());    }    @Test    @Sql("/sql/products.sql")    voidtestIsOverstockSpecification(){        // given
            Specification<Product>specification=newIsOverstockSpecification();        // then
            assertEquals(1,productRepository.findAll(specification).size());    }    @Test    @Sql("/sql/products.sql")    voidtestAndSpecification(){        // given
            Specification<Product>specification=newEqualsSpecification("name","Raspberry Pi").and(newEqualsSpecification("price",newBigDecimal("80.0"))).and(newEqualsSpecification("units",10));        // then
            assertEquals(1,productRepository.findAll(specification).size());    }    @Test    @Sql("/sql/products.sql")    voidtestOrSpecification(){        // given
            Specification<Product>specificationA=newEqualsSpecification("name","Raspberry Pi").or(newEqualsSpecification("price",newBigDecimal("0.0"))).or(newEqualsSpecification("units",0));        Specification<Product>specificationB=newEqualsSpecification("name","").or(newEqualsSpecification("price",newBigDecimal("80.0"))).or(newEqualsSpecification("units",0));        Specification<Product>specificationC=newEqualsSpecification("name","").or(newEqualsSpecification("price",newBigDecimal("0.0"))).or(newEqualsSpecification("units",50));        Specification<Product>specificationZ=newEqualsSpecification("name","").or(newEqualsSpecification("price",newBigDecimal("0.0"))).or(newEqualsSpecification("units",0));        // then
            assertEquals(1,productRepository.findAll(specificationA).size());        assertEquals(1,productRepository.findAll(specificationB).size());        assertEquals(1,productRepository.findAll(specificationC).size());        assertEquals(0,productRepository.findAll(specificationZ).size());    }    @Test    @Sql("/sql/products.sql")    voidtestNotSpecification(){        // given
            Specification<Product>specification=Specification.not(newEqualsSpecification("name","Raspberry Pi").and(newEqualsSpecification("price",newBigDecimal("80.0"))).and(newEqualsSpecification("units",50)));        // then
            assertEquals(7,productRepository.findAll(specification).size());    }    private<T>Specification<T>equalsSpecification(Stringproperty,Objectvalue){        return(root,query,criteriaBuilder)->{            returncriteriaBuilder.equal(root.get("name"),"Raspberry Pi");        };    }}
    ProductJpaSpecificationTest.java
    1
    2
    3
    4
    5
    6
    7
    8
    
    deletefromProduct;insertintoProduct(id,name,date,price,units)values(1,'Pin',to_date('2010/01/01','YYYY/MM/DD'),1.00,50);insertintoProduct(id,name,date,price,units)values(2,'Raspberry Pi',CURRENT_DATE,80.00,10);insertintoProduct(id,name,date,price,units)values(3,'Intel NUC',CURRENT_DATE,400.00,10);insertintoProduct(id,name,date,price,units)values(4,'PlayStation 4',CURRENT_DATE,350.00,10);insertintoProduct(id,name,date,price,units)values(5,'BenQ',CURRENT_DATE,330.00,10);insertintoProduct(id,name,date,price,units)values(6,'Amazon Kindle',CURRENT_DATE,130.00,10);insertintoProduct(id,name,date,price,units)values(7,'Fleck Duo 7 50',CURRENT_DATE,330.00,10);
    products.sql

    En la salida de los teses se muestran la traducción de los objetos specification a las condiciones de las consultas.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=10 and product0_.price=80.0 and product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.price<5.00
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.price<5.00
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=0 or product0_.price=0.0 or product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=0 or product0_.price=80.0 or product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=50 or product0_.price=0.0 or product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=0 or product0_.price=0.0 or product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.price<5.00
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units<>10 or product0_.price<>80.0 or product0_.name<>?
    System.out

    Otra de las funcionalidades proporcionadas por Spring Data es hacer consultas basadas en un objeto ejemplo o query by example.

    Terminal

    El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
    ./gradew test

    Referencia:

    Blog Bitix: El patrón Specification, ejemplo de implementación y uso en JPA con Spring Data

    $
    0
    0

    Los métodos de búsqueda y consulta permiten recuperar objetos de las bases de datos según los criterios deseados. Dependiendo del tamaño de la aplicación y sus casos de uso el número de consultas será más o menos grande. Con un número de consultas grande estas se vuelven complejas de mantener y generan duplicación de lógica de negocio. Para simplificar el mantenimiento de un número grande de consultas y evitar duplicidad de lógica de negocio una solución es implementar el patrón Specification.

    Java

    Spring

    Dado un objeto suele ser necesario comprobar si cumple una o más condiciones. Estas condiciones pueden implementarse tanto en un método del objeto como en la lógica de persistencia en la base de datos.

    Esta aproximación tiene dos inconvenientes, el número de métodos de consulta crece significativamente en las aplicaciones grandes y las consultas son conjunto fijo sin posibilidad de extensión salvo añadir nuevos métodos, las consultas no son fáciles de externalizar y reutilizar.

    En estos casos implementar el patrón Specification ayuda a hacer el código más mantenible, extensible, simple y de más fácil lectura.

    Los siguientes ejemplos implementan el patrón Specification para comprobar si un objeto cumple una serie de condiciones de negocio y como Spring Data hace uso del patrón para construir las condiciones de las consultas de JPA. Los ejemplos incluyen teses que usan la herramienta TestConainers para hacer pruebas de integración en Java con la base de datos PostgreSQL en un contenedor Docker.

    El problema en las consultas

    Suponiendo que se tiene la siguiente entidad del dominio con una serie de campos la idea primera y más directa para implementar si un producto cumple una serie de condiciones es añadir métodos en las clases, un método por cada condición. Por ejemplo, para buscar los productos que que son baratos, tienen un tiempo largo de existencia o un sobrestock.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product;...@Entity@Table(name="Product")publicclassProductimplementsSpecificable<Product>{    ...    @Id    @GeneratedValue    privateLongid;    privateStringname;    privateLocalDatedate;    privateBigDecimalprice;    privateIntegerunits;    publicProduct(){    }    publicProduct(Stringname,LocalDatedate,BigDecimalprice,Integerunits){        this.name=name;        this.date=date;        this.price=price;        this.units=units;    }    ...    publicbooleanisCheap(){        return...;    }    publicbooleanisLongTerm(){        return...;    }    publicbooleanisOverstock(){        return...;    }}
    Product-1.java

    Esta aproximación sencilla de implementar y suficiente en aplicaciones pequeñas tiene dos inconvenientes. El número de métodos a escribir crece significativamente para aplicaciones grandes o complejas y los criterios de los métodos de consulta son fijos, no son extensibles. Para solventar estos dos problemas se opta por crear métodos con los criterios individuales y se combinan entre ellos dinámicamente para obtener la consulta deseada.

    Aquí es donde el patrón Specification es de utilidad. El patrón Specificaion también es aplicable a las consultas presentes en las clases repositorio de acceso a la base de datos donde seguramente es más probable repetir la misma lógica de condiciones en varias consultas hardcodeado en las SQLs. Con los mismos problemas, condiciones repetidas en varios métodos y proliferación de métodos de consulta. Esta es la razón de que Spring Data implemente el patrón Specification.

    Qué es y ventajas del patrón Specification

    El patrón Specification permite encapsular una pieza del conocimiento del dominio y rehusarla en diferentes partes de la aplicación. Usando el patrón Specificaction se mueven estas reglas de negocio a clases llamadas specifications.

    El patrón Specification parte de una interfaz con un método a implementar para encapsular la lógica de negocio que comprueba si la condición se cumple.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    packageio.github.picodotdev.blogbitix.patronspecification.specification;publicinterfaceSpecification<T>{    booleanisSatisfied(Tobject);    defaultSpecification<T>and(Specification<T>...specifications){        returnnewAndSpecification(specifications);    }    defaultSpecification<T>or(Specification<T>...specifications){        returnnewOrSpecification(specifications);    }    defaultSpecification<T>not(Specification<T>specification){        returnnewNotSpecification(specification);    }}
    Specification.java

    Por cada condición hay una implementación de la interfaz.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specifications;importjavax.persistence.criteria.CriteriaBuilder;importjavax.persistence.criteria.CriteriaQuery;importjavax.persistence.criteria.Predicate;importjavax.persistence.criteria.Root;publicclassIsCheapSpecificationimplementsSpecification<Product>,org.springframework.data.jpa.domain.Specification<Product>{    privateStringpriceAttributeName;    publicIsCheapSpecification(){        this(null);    }    publicIsCheapSpecification(Stringpath){        this.priceAttributeName=Specifications.getAttributeName(path,"price");    }    @Override    publicbooleanisSatisfied(Productproduct){       returnProduct.CHEAP_PRICE.compareTo(product.getPrice())==1;    }    @Override    publicPredicatetoPredicate(Root<Product>root,CriteriaQuery<?>query,CriteriaBuildercriteriaBuilder){        returncriteriaBuilder.lt(root.get(priceAttributeName),Product.CHEAP_PRICE);    }}
    IsCheapSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specifications;importjavax.persistence.criteria.CriteriaBuilder;importjavax.persistence.criteria.CriteriaQuery;importjavax.persistence.criteria.Predicate;importjavax.persistence.criteria.Root;importjava.time.LocalDate;importjava.time.Period;publicclassIsLongTermSpecificationimplementsSpecification<Product>,org.springframework.data.jpa.domain.Specification<Product>{    privateStringdateAttributeName;    publicIsLongTermSpecification(){        this(null);    }    publicIsLongTermSpecification(Stringpath){        this.dateAttributeName=Specifications.getAttributeName(path,"date");    }    @Override    publicbooleanisSatisfied(Productproduct){       return!Period.between(product.getDate(),LocalDate.now()).minus(Product.LONG_TERM_PERIOD).isNegative();    }    @Override    publicPredicatetoPredicate(Root<Product>root,CriteriaQuery<?>query,CriteriaBuildercriteriaBuilder){        LocalDatelongTermDate=LocalDate.now().plus(Product.LONG_TERM_PERIOD);        returncriteriaBuilder.lessThan(root.get(dateAttributeName),longTermDate);    }}
    IsLongTermSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specifications;importjavax.persistence.criteria.CriteriaBuilder;importjavax.persistence.criteria.CriteriaQuery;importjavax.persistence.criteria.Predicate;importjavax.persistence.criteria.Root;publicclassIsOverstockSpecificationimplementsSpecification<Product>,org.springframework.data.jpa.domain.Specification<Product>{    privateStringpriceAttributeName;    publicIsOverstockSpecification(){        this(null);    }    publicIsOverstockSpecification(Stringpath){        this.priceAttributeName=Specifications.getAttributeName(path,"units");    }    @Override    publicbooleanisSatisfied(Productproduct){       returnproduct.getUnits()>Product.OVERSTOCK_UNITS;    }    @Override    publicPredicatetoPredicate(Root<Product>root,CriteriaQuery<?>query,CriteriaBuildercriteriaBuilder){        returncriteriaBuilder.gt(root.get(priceAttributeName),Product.OVERSTOCK_UNITS);    }}
    IsOverstockSpecification.java

    En el objeto Product se implementa el patrón Visitor con la interfaz Specificable donde cada implementación de la clase Specification trata la lógica y la clase Product solo tiene el método satisfies que invoca a la instancia de specification recibida como parámetro.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specificable;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;...@Entity@Table(name="Product")publicclassProductimplementsSpecificable<Product>{    ...    @Override    publicbooleansatisfies(Specification<Product>specification){        returnspecification.isSatisfied(this);    }    ...}
    Product.java

    Para realizar combinaciones con operaciones lógicas and, or o not se utiliza el patrón Composite. De entre las operaciones básicas solo se muestra la operación equals, sería neesario implementar otro tipo de operaciones como lessThan, greaterThan, contains u otras si es necesario.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specifications;importjavax.persistence.criteria.CriteriaBuilder;importjavax.persistence.criteria.CriteriaQuery;importjavax.persistence.criteria.Predicate;importjavax.persistence.criteria.Root;importjava.beans.PropertyDescriptor;publicclassEqualsSpecification<T>implementsSpecification<T>,org.springframework.data.jpa.domain.Specification<T>{    privateStringproperty;    privateObjectvalue;    privateStringpropertyAttributeName;    publicEqualsSpecification(Stringproperty,Objectvalue){        this(property,value,null);    }    publicEqualsSpecification(Stringproperty,Objectvalue,Stringpath){        this.property=property;        this.value=value;        this.propertyAttributeName=Specifications.getAttributeName(path,property);    }    @Override    publicbooleanisSatisfied(Tproduct){        try{            PropertyDescriptordescriptor=newPropertyDescriptor(property,Product.class);            Objectv=descriptor.getReadMethod().invoke(product);            if(v==value){                returntrue;            }            if(v!=null&&value==null||v==null&&value!=null){                returnfalse;            }            returnvalue.equals(v);        }catch(Exceptione){            thrownewRuntimeException(e);        }    }    @Override    publicPredicatetoPredicate(Root<T>root,CriteriaQuery<?>query,CriteriaBuildercriteriaBuilder){        returncriteriaBuilder.equal(root.get(propertyAttributeName),value);    }}
    EqualsSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    packageio.github.picodotdev.blogbitix.patronspecification.specification;importjava.util.Arrays;importjava.util.List;publicclassAndSpecification<T>implementsSpecification<T>{    privateList<Specification<T>>specifications;    publicAndSpecification(Specification<T>...specifications){        this.specifications=Arrays.asList(specifications);    }    publicbooleanisSatisfied(Tobject){        returnspecifications.stream().allMatch(s->{returns.isSatisfied(object);});    }}
    AndSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    packageio.github.picodotdev.blogbitix.patronspecification.specification;importjava.util.Arrays;importjava.util.List;publicclassOrSpecification<T>implementsSpecification<T>{    privateList<Specification<T>>specifications;    publicOrSpecification(Specification<T>...specifications){        this.specifications=Arrays.asList(specifications);    }    publicbooleanisSatisfied(Tobject){        returnspecifications.stream().anyMatch(s->{returns.isSatisfied(object);});    }}
    OrSpecification.java
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    packageio.github.picodotdev.blogbitix.patronspecification.specification;publicclassNotSpecification<T>implementsSpecification<T>{    privateSpecification<T>specification;    publicNotSpecification(Specification<T>specification){        this.specification=specification;    }    publicbooleanisSatisfied(Tobject){        return!specification.isSatisfied(object);    }}
    NotSpecification.java

    Al implementar el patrón Specification se hace uso de varios patrones:

    • El patrón de diseño Visitor, en el método satisfies de la clase Product realmente se llama al método isSatisfied de la interfaz Specification.
    • El patrón de diseño Composite en las operaciones lógicas and, or y not. Estas condiciones lógicas de agrupación se componente de otras independientemente de contengan una o varias.
    • El patrón de diseño Comamnd, se construye la instancia de Specification a ejecutar y el método satisfies lo ejecuta.
    • El patrón de diseño Builder se utiliza para facilitar la construcción de las condiciones con una API fluida y ocultar las clases concretas que implementan la interfaz Specification. Spring Data lo implementa.

    La siguiente prueba unitaria muestra con código el uso del patrón Specification.

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importjava.math.BigDecimal;importjava.time.LocalDate;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.specification.AndSpecification;importio.github.picodotdev.blogbitix.patronspecification.specification.NotSpecification;importio.github.picodotdev.blogbitix.patronspecification.specification.OrSpecification;importio.github.picodotdev.blogbitix.patronspecification.specification.Specification;importorg.junit.jupiter.api.Test;import staticorg.junit.jupiter.api.Assertions.*;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.boot.test.util.TestPropertyValues;importorg.springframework.context.ApplicationContextInitializer;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.test.context.ContextConfiguration;@SpringBootTest@ContextConfiguration(initializers={ProductSpecificationTest.Initializer.class})publicclassProductSpecificationTest{    @Test    voidtestEqualsSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now(),newBigDecimal("80.0"),10);        // and
            Specification<Product>specification=newEqualsSpecification("name","Raspberry Pi");        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestIsCheapSpecification(){        // given
            Productproduct=newProduct("Pin",LocalDate.now(),newBigDecimal("1.0"),50);        // and
            Specification<Product>specification=newIsCheapSpecification();        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestIsLongTermSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now().minus(Product.LONG_TERM_PERIOD).minusDays(1),newBigDecimal("80.0"),10);        // and
            Specification<Product>specification=newIsLongTermSpecification();        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestIsOverstockSpecification(){        // given
            Productproduct=newProduct("Pin",LocalDate.now(),newBigDecimal("5.0"),50);        // and
            Specification<Product>specification=newIsOverstockSpecification();        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestIsCheapAndIsLongTermSpecification(){        // given
            Productproduct=newProduct("Pin",LocalDate.now().minus(Product.LONG_TERM_PERIOD).minusDays(1),newBigDecimal("1.0"),50);        // and
            Specification<Product>cheapSpecification=newIsCheapSpecification();        Specification<Product>longTermSpecification=newIsLongTermSpecification();        Specification<Product>specification=newAndSpecification<>(cheapSpecification,longTermSpecification);        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestAndSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now(),newBigDecimal("80.0"),10);        // and
            Specification<Product>specification=newAndSpecification<>(newEqualsSpecification("name","Raspberry Pi"),newEqualsSpecification("price",newBigDecimal("80.0")),newEqualsSpecification("units",10));        // then
            assertTrue(product.satisfies(specification));    }    @Test    voidtestOrSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now(),newBigDecimal("80.0"),50);        // and
            Specification<Product>specificationA=newOrSpecification<>(newEqualsSpecification("name","Raspberry Pi"),newEqualsSpecification("price",newBigDecimal("1.0")),newEqualsSpecification("units",0));        Specification<Product>specificationB=newOrSpecification<>(newEqualsSpecification("name",""),newEqualsSpecification("price",newBigDecimal("80.0")),newEqualsSpecification("units",0));        Specification<Product>specificationC=newOrSpecification<>(newEqualsSpecification("name",""),newEqualsSpecification("price",newBigDecimal("0.0")),newEqualsSpecification("units",50));        Specification<Product>specificationZ=newOrSpecification<>(newEqualsSpecification("name",""),newEqualsSpecification("price",newBigDecimal("0.0")),newEqualsSpecification("units",0));        // then
            assertTrue(product.satisfies(specificationA));        assertTrue(product.satisfies(specificationB));        assertTrue(product.satisfies(specificationC));        assertFalse(product.satisfies(specificationZ));    }    @Test    voidtestNotSpecification(){        // given
            Productproduct=newProduct("Raspberry Pi",LocalDate.now(),newBigDecimal("80.0"),10);        // and
            Specification<Product>specification=newNotSpecification<>(newAndSpecification<Product>(newEqualsSpecification("name","Raspberry Pi"),newEqualsSpecification("price",newBigDecimal("80.0")),newEqualsSpecification("units",10)));        // then
            assertFalse(product.satisfies(specification));    }    publicstaticclassInitializerimplementsApplicationContextInitializer<ConfigurableApplicationContext>{        publicvoidinitialize(ConfigurableApplicationContextconfigurableApplicationContext){            TestPropertyValues.of("spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration")                    .applyTo(configurableApplicationContext.getEnvironment());        }    }}
    ProductSpecificationTest.java

    Implementación para JPA con Spring Data

    El proyecto Spring Data para el acceso a bases de datos con JPA hace uso del patrón Specification, la interfaz JpaSpecificationExecutor añade a los repositorios métodos de búsqueda que reciben un argumento de tipo Specification.

    Esta clase Specification transforma las condiciones en un objeto Predicate que es el que JPA usa para las condiciones de la consulta SQL que se genera. La interfaz JpaSpecificationExecutor también añade métodos para hacer búsquedas paginadas y con ordenación.

    Si en el proyecto se utiliza Spring y JPA esta es la opción recomendada, si no se utiliza Spring o se utiliza otra librería de persistencia distinta a JPA se puede realizar una implementación siguiendo los principios del patrón Specification.

    Las clases EqualsSpecification, IsCheapSpecification, IsLongTermSpecification, y IsOverstockSpecification anteriores también implementan la interfaz Specification de Spring Data. Estas clases implmentan dos interfaces distintas para diferentes cosas, para hacer comprobaciones sobre un objeto en memoria y para generar clases Predicate con las condiciones equivalentes de JPA, son símplemente ejemplos y para separar conceptos no estaría mal dividir cada clase en dos para que implementen las interfaces de forma individual.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product;importorg.springframework.data.jpa.repository.JpaSpecificationExecutor;importorg.springframework.data.jpa.repository.Modifying;importorg.springframework.data.jpa.repository.Query;importorg.springframework.data.repository.PagingAndSortingRepository;publicinterfaceProductRepositoryextendsPagingAndSortingRepository<Product,Long>,JpaSpecificationExecutor<Product>{    @Override    @Modifying    @Query("delete from Product")    voiddeleteAll();}
    ProductRepository.java

    La siguiente prueba de integración con Testcontainers, PostgresSQL y Docker prueba el repositorio con las implementaciones de las clases del patrón Specification para JPA de Spring Data.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    
    packageio.github.picodotdev.blogbitix.patronspecification.domain.product.specification;importio.github.picodotdev.blogbitix.patronspecification.domain.product.Product;importio.github.picodotdev.blogbitix.patronspecification.domain.product.ProductRepository;importliquibase.pro.packaged.T;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.data.jpa.domain.Specification;importorg.springframework.test.context.ContextConfiguration;importorg.springframework.test.context.jdbc.Sql;importjava.math.BigDecimal;import staticorg.junit.jupiter.api.Assertions.assertEquals;@SpringBootTest@ContextConfiguration(initializers={io.github.picodotdev.blogbitix.testcontainers.DefaultPostgresContainer.Initializer.class})publicclassProductJpaSpecificationTest{    @Autowired    privateProductRepositoryproductRepository;    @Test    @Sql("/sql/products.sql")    voidtestIsCheapSpecification(){        // given
            Specification<Product>specification=newIsCheapSpecification();        // then
            assertEquals(1,productRepository.findAll(specification).size());    }    @Test    @Sql("/sql/products.sql")    voidtestIsLongTermSpecification(){        // given
            Specification<Product>specification=newIsCheapSpecification();        // then
            assertEquals(1,productRepository.findAll(specification).size());    }    @Test    @Sql("/sql/products.sql")    voidtestIsOverstockSpecification(){        // given
            Specification<Product>specification=newIsCheapSpecification();        // then
            assertEquals(1,productRepository.findAll(specification).size());    }    @Test    @Sql("/sql/products.sql")    voidtestAndSpecification(){        // given
            Specification<Product>specification=newEqualsSpecification("name","Raspberry Pi").and(newEqualsSpecification("price",newBigDecimal("80.0"))).and(newEqualsSpecification("units",10));        // then
            assertEquals(1,productRepository.findAll(specification).size());    }    @Test    @Sql("/sql/products.sql")    voidtestOrSpecification(){        // given
            Specification<Product>specificationA=newEqualsSpecification("name","Raspberry Pi").or(newEqualsSpecification("price",newBigDecimal("0.0"))).or(newEqualsSpecification("units",0));        Specification<Product>specificationB=newEqualsSpecification("name","").or(newEqualsSpecification("price",newBigDecimal("80.0"))).or(newEqualsSpecification("units",0));        Specification<Product>specificationC=newEqualsSpecification("name","").or(newEqualsSpecification("price",newBigDecimal("0.0"))).or(newEqualsSpecification("units",50));        Specification<Product>specificationZ=newEqualsSpecification("name","").or(newEqualsSpecification("price",newBigDecimal("0.0"))).or(newEqualsSpecification("units",0));        // then
            assertEquals(1,productRepository.findAll(specificationA).size());        assertEquals(1,productRepository.findAll(specificationB).size());        assertEquals(1,productRepository.findAll(specificationC).size());        assertEquals(0,productRepository.findAll(specificationZ).size());    }    @Test    @Sql("/sql/products.sql")    voidtestNotSpecification(){        // given
            Specification<Product>specification=Specification.not(newEqualsSpecification("name","Raspberry Pi").and(newEqualsSpecification("price",newBigDecimal("80.0"))).and(newEqualsSpecification("units",50)));        // then
            assertEquals(7,productRepository.findAll(specification).size());    }    private<T>Specification<T>equalsSpecification(Stringproperty,Objectvalue){        return(root,query,criteriaBuilder)->{            returncriteriaBuilder.equal(root.get("name"),"Raspberry Pi");        };    }}
    ProductJpaSpecificationTest.java
    1
    2
    3
    4
    5
    6
    7
    8
    
    deletefromProduct;insertintoProduct(id,name,date,price,units)values(1,'Pin',to_date('2010/01/01','YYYY/MM/DD'),1.00,50);insertintoProduct(id,name,date,price,units)values(2,'Raspberry Pi',CURRENT_DATE,80.00,10);insertintoProduct(id,name,date,price,units)values(3,'Intel NUC',CURRENT_DATE,400.00,10);insertintoProduct(id,name,date,price,units)values(4,'PlayStation 4',CURRENT_DATE,350.00,10);insertintoProduct(id,name,date,price,units)values(5,'BenQ',CURRENT_DATE,330.00,10);insertintoProduct(id,name,date,price,units)values(6,'Amazon Kindle',CURRENT_DATE,130.00,10);insertintoProduct(id,name,date,price,units)values(7,'Fleck Duo 7 50',CURRENT_DATE,330.00,10);
    products.sql

    En la salida de los teses se muestran la traducción de los objetos specification a las condiciones de las consultas.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=10 and product0_.price=80.0 and product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.price<5.00
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.price<5.00
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=0 or product0_.price=0.0 or product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=0 or product0_.price=80.0 or product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=50 or product0_.price=0.0 or product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units=0 or product0_.price=0.0 or product0_.name=?
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.price<5.00
    Hibernate: select product0_.id as id1_0_, product0_.date as date2_0_, product0_.name as name3_0_, product0_.price as price4_0_, product0_.units as units5_0_ from product product0_ where product0_.units<>10 or product0_.price<>80.0 or product0_.name<>?
    System.out

    Otra de las funcionalidades proporcionadas por Spring Data es hacer consultas basadas en un objeto ejemplo o query by example.

    Terminal

    El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
    ./gradew test

    Referencia:

    Blog Bitix: Comando para convertir una imagen SVG a PNG y JPG con diferentes tamaños, formatos y color de fondo con Inkskape

    $
    0
    0

    El formato de imagen SVG tiene la ventaja de permitir el escalado de la imagen sin pérdida de calidad y suele tener menor tamaño que la imagen equivalente en formato PNG y JPG. Si es necesario la imagen SVG es exportable a formato de imagen PNG y JPG con el tamaño deseado o color de fondo. El editor de imágenes Inkscape permite con su utilidad de línea de comandos automatizar y exportar archivos SVG a PNG y JPG y ocultar y mostrar las capas deseadas del archivo original para obtener el resultado deseado en la exportación.

    Inkscape

    GNU

    El formato de imagen SVG es un formato de imagen vectorial donde las líneas, formas, posición y colores se describen en formato texto, tiene la ventaja de ser un formato escalable que no pierde resolución independiente del tamaño de imagen en la que se represente, es decir, la imagen tiene la misma calidad al tamaño full hd 1920x1080 que a 4K 3840x2560 que en 800x600 píxeles.

    Con las imágenes de fotos en formato JPG y sin pérdida de calidad PNG la resolución adecuada para mostrar estas imágenes es la original del archivo a otra resolución hay que hacer un escalado con un algoritmo para añadir o quitar píxeles, el escalado es una operación imprecisa que resta algo de calidad a la imagen. Escalar el tamaño de una imagen JPG o PNG es necesario para obtener la imagen en otros tamaños, dependiendo del número de píxeles a añadir si se hace más grande que la original o píxeles a quitar si se reduce el tamaño la pérdida de calidad se nota más o menos.

    Imagen en formato JPG originalImagen en formato JPG escalada a 300x200 píxeles

    Imagen en formato JPG original y escalada a 300x200 píxeles

    Los navegadores y dispositivos móviles ya soportan como formato de imagen el SVG, en la web y los dispositivos móviles es especialmente adecuado este formato ya que además de adaptarse a la variedad de tamaños de los dispositivos de escritorio o móviles suelen tener un menor tamaño de archivo lo que hace que se descarguen más rápido al requerir menos ancho de banda.

    Aún con los beneficios que posee el formato SVG algunas aplicaciones no soportan el formato SVG y en este caso es necesario hacer una conversión de SVG a los formatos binarios rasterizados PNG o JPG. El formato SVG permite obtener estas imágenes PNG y JPG en diferentes tamaños sin pérdida de calidad.

    Inkscape es un editor de imágenes vectoriales con una utilidad de línea de comandos que permite convertir y exportar imágenes en formato SVG a PNG y JPG en el tamaño y con el color de fondo deseado. El siguiente comando convierte todos los archivos SVG a PNG de una carpeta. En el comando se indican varios parámetros como la anchura deseada de la imagen, el color de fondo, los identificativos de las capas a exportar, el formato de salida y el nombre del archivo creado. Posteriormente con un segundo comando hay que convertir las imágenes de formato PNG a JPG, dependiendo del tipo de imagen, los colores y degradados de la imagen el tamaño en formato PNG será mayor o menor que en formato JPG.

    1
    2
    
    $ for f in *.svg;do inkscape -w 750"$f" --export-background white --export-background-opacity 1 --export-type png --export-filename "$(basename ${f%.*})-750.png";done;
    $ for f in *.png;do convert "$f""${f%.*}.jpg";done;
    inkscape-convert-svg-png.sh

    Imagen original en formato SVG

    Imagen original en formato SVG

    El editor Inkscape permite definir capas con diferentes elementos de la imagen, la linea de comandos permite exportar únicamente capas deseadas de la imagen para obtener el resultado deseado en la exportación. El SVG anterior contiene en el mismo archivo diferentes capas con diferentes versiones de la imagen adecuadas para un fondo claro y oscuro.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    BASENAME="apache-tapestry"ARTWORK_FILE="$BASENAME-artwork.svg"
    
    $ inkscape "$ARTWORK_FILE" --export-id-only --export-id lightObject --export-background white -w 800 --export-type png --export-filename "$BASENAME-icontext-800-light.png"
    $ inkscape "$ARTWORK_FILE" --export-id-only --export-id darkObject --export-background black -w 800 --export-type png --export-filename "$BASENAME-icontext-800-dark.png"
    $ inkscape "$ARTWORK_FILE" --export-id-only --export-id lightIconObject --export-background white -w 800 --export-type png --export-filename "$BASENAME-icon-800-light.png"
    $ inkscape "$ARTWORK_FILE" --export-id-only --export-id darkIconObject --export-background black -w 800 --export-type png --export-filename "$BASENAME-icon-800-dark.png"
    $ inkscape "$ARTWORK_FILE" --export-id-only --export-id lightTextObject --export-background white -w 800 --export-type png --export-filename "$BASENAME-text-800-light.png"
    $ inkscape "$ARTWORK_FILE" --export-id-only --export-id darkTextObject --export-background black -w 800 --export-type png --export-filename "$BASENAME-text-800-dark.png"
    inkscape-png-versions.sh

    Imagen en formato PNGImagen en formato PNG

    Imagen en formato PNGImagen en formato PNG

    Imagen en formato PNGImagen en formato PNG

    Diferentes versiones de la imagen SVG en formato PNG

    También es posible modificar el SVG original para mostrar y ocultar las capas visibles del archivo. Los siguientes comandos permiten exportar a PNG la imagen en diferentes versiones (icono y texto, solo icono o solo texto), con diferente color de fondo (transparente, blanco y negro) y en diferente tamaño. Esto permite automatizar y hacerlo mucho más rápido que el repetitivo proceso que sería realizar manualmente la exportación usando la interfaz gráfica de Inkscape.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    BASENAME="apache-tapestry"ARTWORK_FILE="$BASENAME-artwork.svg"
    
    $ (cp "$ARTWORK_FILE""$BASENAME-icon-light.svg"&& inkscape "$BASENAME-icon-light.svg" --verb LayerHideAll --verb DialogLayers --verb LayerToggleHide --verb FitCanvasToDrawing --verb FileSave --verb FileQuit)
    $ (cp "$ARTWORK_FILE""$BASENAME-icon-dark.svg"&& inkscape "$BASENAME-icon-dark.svg" --verb LayerHideAll --verb DialogLayers --verb LayerPrev --verb LayerToggleHide --verb FitCanvasToDrawing --verb FileSave --verb FileQuit)
    $ (cp "$ARTWORK_FILE""$BASENAME-text-light.svg"&& inkscape "$BASENAME-text-light.svg" --verb LayerHideAll --verb DialogLayers --verb LayerPrev --verb LayerPrev --verb LayerToggleHide --verb EditDeselect --verb FitCanvasToDrawing --verb FileSave --verb FileQuit)
    $ (cp "$ARTWORK_FILE""$BASENAME-text-dark.svg"&& inkscape "$BASENAME-text-dark.svg" --verb LayerHideAll --verb DialogLayers --verb LayerPrev --verb LayerPrev --verb LayerPrev --verb LayerToggleHide --verb FitCanvasToDrawing --verb FileSave --verb FileQuit)
    $ (cp "$ARTWORK_FILE""$BASENAME-icontext-light.svg"&& inkscape "$BASENAME-icontext-light.svg" --verb LayerHideAll --verb DialogLayers --verb LayerPrev --verb LayerPrev --verb LayerPrev --verb LayerPrev --verb LayerToggleHide --verb FitCanvasToDrawing --verb FileSave --verb FileQuit)
    $ (cp "$ARTWORK_FILE""$BASENAME-icontext-dark.svg"&& inkscape "$BASENAME-icontext-dark.svg" --verb LayerHideAll --verb DialogLayers --verb LayerPrev --verb LayerPrev --verb LayerPrev --verb LayerPrev --verb LayerPrev --verb LayerToggleHide --verb FitCanvasToDrawing --verb FileSave --verb FileQuit)
    inkscape-svg-versions.sh

    Imagen en formato SVGImagen en formato SVG

    Imagen en formato SVGImagen en formato SVG

    Imagen en formato SVGImagen en formato SVG

    Diferentes versiones de la misma imagen SVG
    Viewing all 2713 articles
    Browse latest View live