Skip to content

Juan Mellado

Dart 2.0.0 (1)

Esta semana se publicó la versión 2.0.0 de Dart, un lenguaje de programación desarrollado por Google que a mí personalmente siempre me ha parecido muy productivo. Es un lenguaje sencillo pero potente que viene con “baterías incluidas”, a la manera de Java, es decir, con un conjunto de librerías fundamentales con las que empezar a trabajar de forma inmediata. ¿Necesitas una cola? Pues una línea para importar el paquete de colecciones y otra línea para instanciarla. ¿Necesitas un socket? Pues una línea para importar el paquete de entrada/salida y otra línea para instanciarlo. Y aunque lo mismo se puede decir de la mayoría de lenguajes de programación hoy en día, los cuales posiblemente dispongan de un ecosistema de librerías más rico, Dart tiene una facilidad de uso y rendimiento que hacen muy grata la experiencia de desarrollo.

La mayoría de características que se encuentran en versiones modernas de JavaScript, como async y await para la programación asíncrona, o yield para la construcción de generadores, por citar sólo un par de ejemplos, se incorporaron hace años de forma nativa a Dart. Para mí siempre ha sido un buen laboratorio donde probar los futuros añadidos que se harán a JavaScript en el futuro. Empecé a utilizarlo para mis proyectos personales hace ya siete años, desde las primeras versiones beta, tanto para hacer aplicaciones en la parte cliente como en la parte servidora, y nunca me he arrepentido del tiempo invertido.

Dart es un lenguaje de script que se ejecuta sobre una máquina virtual propia creada por Lars Bak y Kasper Lund, en base a su exitosa experiencia de desarrollo de la máquina virtual v8 de JavaScript, y que por cierto, hace poco dejaron Google para montar su propia startup. Los planes iniciales eran incorporar la máquina virtual de Dart a Chrome, pero finalmente esa idea se desestimó. Durante todo este tiempo Google ha mantenido el desarrollo de Dartium, una versión de Chrome que incorpora Dart de forma nativa, pero con la versión 2 han decidido abandonar totalmente dicho proyecto. Y esto es una cuestión importante, ya que el anuncio oficial en su día de que la máquina virtual de Dart no se incorporaría a Chrome fue un punto de inflexión fundamental para el lenguaje, muchos desarrolladores dejaron de ver Dart como un lenguaje con futuro y prefirieron seguir desarrollando en JavaScript o TypeScript. Dart entró entonces en una especie de limbo, pero Google decidió no dejar morir el lenguaje y utilizarlo en productos internos como AdSense y AdWords por ejemplo.

Dart ha vuelto a aparecer en los medios últimamente gracias a dos noticias que han supuesto un revulsivo para el lenguaje. Por una parte el anuncio de que Fuchsia, el nuevo sistema operativo que está desarrollando Google, utilizará Dart para el desarrollo de aplicaciones. Y por otra parte la popularidad de Flutter, un framework para desarrollar aplicaciones nativas en Android e iOS que utiliza Dart. La primera noticia todavía está por concretar, pero la segunda es un hecho. Aún recuerdo el vídeo de hace unos años donde se hizo la primera presentación del proyecto, donde un excitado Eric Seidel hablaba de un proyecto que entonces se llamaba Sky y más tarde se renombró como Flutter. Algunas de las características que más han llamado la atención de Flutter son que un mismo código base sirve tanto para Android como iOS, que las interfaces de usuario se desarrollan escribiéndolas mediante código en vez de plantillas, a la manera de Ext JS, que incorpora toda una galería de componentes utilizando los estilos de Material Design por defecto, y que sus herramientas junto con su filosofía de trabajo con Dart acortan significativamente el ciclo de desarrollo.

Dart se puede utilizar como lenguaje de script en el servidor o compilado a JavaScript en el cliente. Google ha anunciado que con la versión 2 del lenguaje se van a centrar en la parte cliente, objetivo que en realidad lleva intentando desde el principio, pero que no ha acabado de alcanzar. Incluso llegó a intentar cambiar el desarrollo de su popular framework Angular de JavaScript a Dart, pero al final lo dejó estar por la fría acogida que tuvo el anuncio y creó una línea de desarrollo en paralelo con AngularDart. Producto este último al que han vuelto a intentar dar más protagonismo de cara a conseguir que más equipos de desarrollo lo adopten y deje de ser sólo una herramienta de uso interno. Su demo de construcción de una PWA con un 100% de puntuación en los tests de Lighthouse es una buena declaración de intenciones.

Google creó junto con el lenguaje un IDE llamado Dart Editor basado en Eclipse y pensado específicamente para trabajar con Dart. Con los años dejaron de mantenerlo y hoy en día el IDE que recomiendan es WebStorm, aunque es posible trabajar con otros editores. Yo en particular utilizo Visual Studio Code con el plugin Dart Code.

En cuanto al lenguaje en sí mismo, Dart sigue el estándar ECMA-408. Similar en su sintaxis a los lenguajes imperativos clásicos como Java, pero incluyendo elementos de sus versiones más modernas, tomados en su mayoría del ámbito de la programación funcional, como por ejemplo las expresiones lambda, e incorporando algunos elementos de su propia cosecha, como por ejemplo los constructores con nombre o la invocación de métodos en cascada. Inicialmente débilmente tipado, Dart ha ido evolucionando hacia un sistema de tipado más fuerte, aunque conservando aspectos tales como la inferencia automática de tipos cuando estos se excluyen de las declaraciones. En Dart todos los objetos son instancias de clases, incluso los tipos primitivos ofrecidos de forma nativa, como los números enteros por ejemplo. Como todo lenguaje en continua evolución, aún tiene características que se encuentran en otros lenguajes que serían deseables incorporar al mismo, y algunas lagunas, como un sistema de reflexión, pero en general el balance es bastante positivo.

Dart incorpora de forma nativa las herramientas básicas necesarias para un desarrollo moderno. En particular un formateador de código, un analizador de código estático, y un gestor de dependencias.

El formateador de código se llama dartfmt y es algo particular, porque no se puede configurar, por lo que a veces el resultado no es exactamente el deseado, pero es algo con lo que se puede vivir. La justificación de esta decisión de diseño es que se pretende que todo el código escrito en Dart tenga el mismo aspecto independientemente de su origen. A escala mundial. Una visión que sólo puede tenerse en una organización global como Google. Hace algún tiempo leí un artículo del autor, Bob Nystrom, sobre la vasta cantidad de permutaciones que pueden generarse a partir de un mismo código, atendiendo sólo a razones de formateado del mismo, y el reto que supone elegir una de dichas permutaciones de forma programática.

El analizador de código estático se llama dartanalyzer y es la base sobre la que se ha construido un linter. Esta herramienta si es parametrizable y existen decenas de reglas que pueden activarse según las necesidades particulares de cada proyecto. No obstante, es importante tener en cuenta que algunas reglas se han definido en base a la guía de estilo específica de Flutter en vez de a la guía de estilo más general de Dart. Esto quiere decir que cuando salta una regla hay que comprobar si se trata de una violación del estilo general recomendado por Dart o del estilo más específico de Flutter. Por ejemplo, Flutter recomienda que se utilice siempre código fuertemente tipado, mientras que Dart recomienda que se utilice la inferencia de tipos para las variables locales. La regla del linter por defecto sigue las recomendaciones de Flutter, por lo que si se define una variable local sin indicar de manera explícita su tipo se produce una violación de la regla. Y si se desactiva la regla entonces el linter deja de realizar la validación para todo el código, incluso en las interfaces públicas, donde si es conveniente utilizar declaraciones fuertemente tipadas. Es de suponer que estas reglas se irán refinando con el tiempo.

El gestor de dependencias es una herramienta llamada pub que existe desde las primeras versiones de Dart. Su funcionamiento es similar a herramientas similares basadas en publicar y descargar paquetes en base a su versión, como Maven en Java o npm en node.js. El diseño de la web del gestor de paquetes ha ido evolucionando a lo largo de los años. En la actualidad no es sólo un mero listado de paquetes disponibles, sino que ofrece resúmenes acerca de cada paquete con indicadores acerca de su popularidad o volumen de actualizaciones. Para forzar la migración de los paquetes existentes a versiones más recientes de Dart la propia web realiza de forma automática comprobaciones compilando el código y verificando si cumplen con las reglas de estilo. El resultado de dichas comprobaciones se publica en la propia web, por lo que es sencillo hacerse una idea del estado de un paquete y la dificultad que puede entrañar realizar los cambios necesarios para actualizarlo a una nueva versión de Dart.

Para Dart 2.0.0 se ha creado además un sistema de herramientas para facilitar el desarrollo, construcción y empaquetado, a la manera de gulp y similares en JavaScript. Lo que ofrece este sistema son las facilidades habituales para la compilación, monitorización de cambios y ejecución de pruebas. Incluyendo el uso de servicios de integración continua, como Travis CI por ejemplo, y permitiendo la creación de plugins que pueden ejecutarse durante las distintas fases del proceso de construcción de una aplicación.

Resumiendo, Dart es un lenguaje por el que Google sigue apostando, a pesar de no ser muy utilizado o conocido por el gran público, y al que se le está dotando de las herramientas necesarias para que resulte más atractivo a los desarrolladores. Aún así, su supervivencia depende de la aceptación de Flutter, un framework que curiosamente está cobrando más relevancia que el propio lenguaje en sí mismo, y la incógnita en el horizonte del futuro Fuchsia. Con Kevin Moore como Product Manager, en vez de Seth Ladd, que siempre me ha parecido tener un perfil más político, y que de hecho se ha subido a la estela del exitoso Flutter, junto con la salida de Lars Bak y Kasper Lund, queda por ver el rumbo que seguirá tomando el lenguaje.

Redis (y 2)

Continuando con el repaso de las características básicas de Redis, en este artículo se revisan los tipos que representan estructuras agregadas de elementos.

Hash

Este tipo almacena pares claves-valor asociados a un nombre. Se comporta como los arrays asociativos presentes en la mayoría de lenguajes de programación.

El caso de uso más habitual que implementan es almacenar información relacionada con una entidad, a modo de atributos de un objeto.

Los comandos disponibles para trabajar con hashes siguen la notación característica de Redis para estructuras, donde cada comando empieza con una letra que hace referencia al tipo con el que operan. Por ejemplo, los comandos que empiezan por H operan con hashes.

Comandos específicos para el tipo hash:

  • HSET, establece el valor de un campo
  • HSETNX, establece el valor de un campo, pero sólo si no existía
  • HMSET, establece el valor de una serie de campos
  • HGET, obtiene el valor de un campo
  • HGETALL, obtiene todos los campos y valores
  • HMGET, obtiene los valores de los campos dados
  • HDEL, borra un campo
  • HINCRBY, incrementa el valor entero de un campo por un valor dado
  • HINCRBYFLOAT, incrementa el valor decimal de un campo por un valor dado
  • HSTRLEN, obtiene la longitud de un valor almacenado en un campo
  • HEXISTS, comprueba si un campo existe
  • HLEN, obtiene el número de campos
  • HKEYS, obtiene todos los nombres de campos
  • HVALS, obtiene todos los valores
  • HSCAN, itera por los campos

Los hashes de Redis de tamaño reducido realizan un uso muy eficiente de la memoria y se recomienda su uso cuando el número de claves, y sus valores, sean de reducido tamaño.

List

Las listas en Redis están implementadas como listas enlazadas, lo que quiere decir que la inserción y borrado son operaciones rápidas que se ejecutan en un tiempo constante, frente el acceso a los elementos por su índice que es una operación más costosa.

El caso de uso más habitual es almacenar las últimas N ocurrencias de un determinado evento o tipo de objeto. Por ejemplo, se pueden utilizar listas para almacenar los IDs de los últimos artículos consultados en la web de una tienda, de forma que puedan recuperarse muy rápidamente sin necesidad de realizar una consulta costosa sobre una base de datos relacional.

Comandos específicos para el tipo list:

  • LPUSH, inserta elementos por la cabecera
  • LPUSHX, inserta un elemento en la cabecera, pero sólo si la lista existe
  • RPUSH, inserta elementos por la cola
  • RPUSHX, inserta un elemento por la cola, pero sólo si la lista existe
  • LPOP, obtiene y borra el elemento cabecera
  • RPOP, obtiene y borra el elemento de la cola
  • RPOPLPUSH, mueve el elemento de la cola a otra lista
  • LINSERT, inserta un elemento en un índice dado
  • LSET, establece el valor de un elemento en un índice dado
  • LINDEX, obtiene el elemento de un índice dado
  • LRANGE, obtiene los elementos de un rango dado
  • LREM, elimina elementos
  • LTRIM, elimina elementos en un rango dado
  • LLEN, obtiene el número de elementos
  • BLPOP, obtiene y borra el elemento cabecera de forma bloqueante
  • BRPOP, obtiene y borra el elemento de la cola de forma bloqueante
  • BRPOPLPUSH, mueve el elemento de la cola a otra lista de forma bloqueante

Los comandos permiten tratar las listas como pilas y colas añadiendo y eliminando elementos por los dos extremos, operaciones ambas muy rápidas al estar implementadas como listas enlazadas. Los nombres de los comandos utilizan la letra L (left) para indicar que la operación afecta a la cabecera de la lista y R (right) para indicar que afecta a la cola.

Notar además que los comandos que acceden a un rango de elementos utilizan el cero como índice para hacer referencia al primer elemento, y un índice negativo para hacer referenciar a los elementos por el final.

Un caso de uso especial de las listas es su utilización como colas siguiendo un modelo de suscriptores/consumidores. Los comando BLPOP y BRPOP extraen un elemento de una lista, bloqueando al cliente hasta que haya un elemento disponible, si la lista está vacía, o hasta que se sobrepase un tiempo de espera indicado como parámetro de la operación. Usando 0 como tiempo de espera el cliente espera indefinidamente.

Para probar el funcionamiento de Redis como cola de mensajes bloqueante basta con abrir un segundo cliente desde línea de comandos y ejecutar el comando BLPOP o BRPOP sobre una lista creada desde el primer cliente. Si la lista tiene elementos el segundo cliente retornará inmediatamente, pero si no lo tiene se bloqueará hasta que el primer cliente añada un nuevo elemento o se supere el tiempo de espera. Es realmente sencillo e inmediato. Para casos de usos más sofisticados se puede utilizar el comando BRPOPLPUSH, que extrae un elemento de una lista y lo inserta en otra, lo que puede resultar de utilidad para evitar la pérdida de mensajes, almacenándolos en una lista de trabajo temporal.

Set

Los conjuntos en Redis son colecciones desordenadas de elementos. Un mismo elemento sólo puede añadirse una única vez a un mismo conjunto.

El caso de uso habitual para los conjuntos es su utilización como almacén de referencias de unas entidades relacionadas con otras. Como las foreign keys en una base de datos relacional. Por ejemplo, para almacenar los ids de las etiquetas (tags) de las entradas (posts) de un blog.

Comandos disponibles con el tipo list:

  • SADD, añade un elemento
  • SMEMBERS, obtiene todos los elementos
  • SISMEMBER, comprueba si un elemento pertenece a un conjunto
  • SRANDMEMBER, obtiene elementos de forma aleatoria
  • SPOP, obtiene y borra elementos de forma aleatoria
  • SMOVE, mueve un elemento a otro conjunto
  • SREM, elimina elementos
  • SUNION, obtiene la unión de una serie de conjuntos
  • SUNIONSTORE, obtiene la unión de una serie de conjuntos y la almacena en otro
  • SINTER, obtiene la intersección de una serie de conjuntos
  • SINTERSTORE, obtiene la intersección de una serie de conjuntos y la almacena en otro
  • SDIFF, obtiene la diferencia de un conjunto contra una serie de conjuntos
  • SDIFFSTORE, obtiene la diferencia de un conjunto contra una serie de conjuntos y la almacena en otro
  • SCARD, obtiene el número de elementos
  • SSCAN, itera por los elementos

Los casos de uso de los conjuntos a veces parecen un poco forzados, como si se hubieran implementado y luego buscado una utilidad para los mismos. En la práctica se pueden utilizar para realizar uniones, intersecciones y diferencias entre conjuntos. Así como para obtener elementos aleatorios, incluso con repetición.

Sorted Set

Los conjuntos ordenados en Redis son colecciones de elementos a los que se puede asociar un valor o puntuación (score). Dicho valor se utiliza para ordenar los elementos retornados por las operaciones de consulta sobre los mismos.

El caso de uso más habitual es la implementación de listas con ranking (leader boards). Por ejemplo, para implementar una lista de jugadores con las mayores puntuaciones, u obtener la posición en el ranking de un jugador dado.

Comandos específicos para el tipo sorted set:

  • ZADD, añade elementos con puntuación
  • ZINCRBY, incrementa la puntuación de un elemento
  • ZSCORE, obtiene la puntuación de un elemento
  • ZRANK, obtiene el ranking de un elemento
  • ZREVRANK, obtiene el ranking de un elemento de forma descendente
  • ZPOPMIN, obtiene y elimina elementos con la menor puntuación
  • ZPOPMAX, obtiene y elimina elementos con la mayor puntuación
  • ZREM, elimina elementos
  • ZREMRANGEBYSCORE, elimina elementos en un rango de puntuaciones dado
  • ZREMRANGEBYRANK, elimina elementos en un ranking dado
  • ZREMRANGEBYLEX, elimina elementos en un rango lexicográfico dado
  • ZRANGE, obtiene elementos en un rango de puntuaciones dado
  • ZREVRANGE, obtiene elementos en un rango de puntuaciones dado ordenados de forma descendente
  • ZRANGEBYSCORE, obtiene elementos en un rango de puntuaciones dado ordenados por puntuación
  • ZREVRANGEBYSCORE, obtiene elementos en un rango de puntuaciones dado ordenados por puntuación de forma descendente
  • ZRANGEBYLEX, obtiene elementos en un rango de puntuaciones dado ordenados lexicográficamente
  • ZREVRANGEBYLEX, obtiene elementos en un rango de puntuaciones dado ordenados lexicográficamente de forma descendente
  • ZUNIONSTORE, obtiene la unión de varios conjuntos ordenados y la almacena en otro
  • ZINTERSTORE, obtiene la intersección de varios conjuntos ordenados y la almacena en otro
  • ZCARD, obtiene el número de elementos
  • ZCOUNT, obtiene el número de elementos en un rango de puntuaciones dado
  • ZLEXCOUNT, obtiene el número de elementos en un rango lexicográfico dado
  • BZPOPMIN, obtiene y elimina elementos con la menor puntuación de forma bloqueante
  • BZPOPMAX, obtiene y elimina elementos con la mayor puntuación de forma bloqueante
  • ZSCAN, itera por los elementos

En la práctica las puntuaciones se tratan ordenadas de menor a mayor, por lo que la mayoría de los comandos que recuperan rangos de elementos tienen un comando similar pero que recupera en orden inverso. En caso de dos elementos con la misma puntuación se retorna el resultado ordenado en base al orden lexicográfico de los elementos, que en Redis son siempre cadenas de texto. El orden está garantizado en la medida que un conjunto no puede contener dos veces una misma cadena.

El tipo sorted set es utilizado para casos de uso más elaborados, como por ejemplo para obtener productos similares a los consultados por un cliente en la web de una tienda, normalmente en función de las compras realizadas por otros usuarios que también consultaron o compraron los mismos productos.

HyperLogLogs

Este tipo está orientado a obtener el número de elementos distintos de una colección. Su peculiaridad es que no almacena realmente los elementos, por lo que no requiere mucha memoria, y no retorna un valor exacto, sino una aproximación.

Su caso de uso habitual es obtener la cuenta de las distintas ocurrencias de una determinada entidad. Por ejemplo, para obtener el número de las distintas direcciones IP que visitan una página web.

Comandos específicos del tipo HyperLogLogs:

  • PFADD, añade un elemento
  • PFCOUNT, obtiene el número aproximado de elementos
  • PFMERGE, realiza la unión de varios HyperLogLogs y la almacena en otro

Para el caso de uso habitual comentado, contar las distintas IPs que visitan una web, lo ideal sería llevar una cuenta a base de guardar todas las direcciones, incrementando el contador en el caso de que la dirección no estuviera ya guardada. Lógicamente, si se hiciera así, los requerimientos de memoria y tiempo de proceso serían inviables. La solución de Redis es hacer un hash por cada elemento (IP) añadido al conjunto (HyperLogLogs) y contar los distintos hashes. Esto elimina la necesidad de guardar todos los elementos y da una aproximación bastante buena de la cardinalidad (contador) del conjunto. De hecho, según la documentación, con un error de menos del 1%.

Streams

El tipo stream estará disponible a partir de la versión 5 de Redis. Su caso de uso más natural será el de modelar series temporales. Es decir, registrar información asociada a un determinado momento en el tiempo.

Es equivalente a un fichero de log, donde cada línea tiene un timestamp. Y aunque aún es pronto para afirmarlo, puede convertirse en el sistema de referencia para almacenar de forma local las medidas generadas por los sensores de dispositivos IoT (Internet of Things) antes de ser enviadas a la nube para su procesamiento.

Comandos específicos del tipo stream:

  • XADD, añade un elemento
  • XRANGE, obtiene elementos en un rango dado
  • XDEVRANGE, obtiene elementos en un rango dado en orden descendente
  • XLEN, obtiene el número de elementos
  • XREAD, obtiene elementos en un rango dado, opcionalmente de forma bloqueante
  • XREADGROUP, obtiene elementos en un rango para un grupo dado, opcionalmente de forma bloqueante
  • XPENDING, obtiene los elementos pendientes de un grupo

Los streams guardan elementos de igual forma que otras estructuras, pero su implementación está optimizada para su caso de uso más natural. Cuando se añade un elemento se genera automáticamente un ID que se compone de dos partes. La primera parte es un timestamp y la segunda es un secuencial dentro de dicho timestamp. Opcionalmente se puede utilizar un ID propio obviando el generado automáticamente, pero no se espera que sea una opción muy utilizada.

Redis ofrece dos formas de consultar los datos almacenados en un stream. La primera es mediante una consulta por rango, y la segunda es mediante un mecanismo de publicación/subscripción. Esta segunda forma se suma a los mecanismos ya existentes dentro de Redis, como los comandos bloqueantes para extraer elementos de listas, y los comandos más específicos como PUB/SUB para operar con canales. La novedad es que los clientes podrán suscribirse para obtener las entradas registradas a partir de un momento concreto en el tiempo. Con los tipos existentes anteriormente esto no era posible, cuando un cliente se desconectaba y volvía a conectarse no podía acceder a la información generada durante el periodo de desconexión.

En definitiva, un nuevo tipo añadido a Redis que aún está por venir y tiene más características por explotar, como los grupos de consumidores, que garantiza que cada mensaje es enviado a un único consumidor distinto dentro del grupo.

Redis (1)

La introducción de un nuevo tipo de datos en la futura versión 5 de Redis supone una buena oportunidad para escribir un par de artículos sobre este veterano software.

Aunque tradicionalmente vista como una base de datos NoSQL de tipo clave-valor en memoria, útil sólo como cache, en realidad Redis ofrece soluciones para un mayor número de casos de usos a través de sus componentes básicos: tipos y comandos.

Los tipos soportados por Redis son las cadenas de textos, arrays de bits, hashes, listas, conjuntos, conjuntos con peso, e HyperLogLogs (una estructura creada específicamente para estimar el número de elementos distintos de una colección). La futura versión 5 de Redis añadirá además los streams, un tipo especializado para la gestión de series temporal.

Los comandos de Redis son las herramientas a través de la que se construyen estructuras con los tipos soportados y se manipulan sus elementos. La integridad de los resultados está garantizada porque los comandos se ejecutan de forma atómica, pudiendo incluso utilizarse transacciones para ejecutar de forma atómica un conjunto de comandos y hasta ejecutar scripts en Lua.

La documentación de Redis es bastante detallada y su lectura obligada. Una característica de Redis a este respecto es que junto a la definición de los comandos se describen sus casos de uso más habituales, así como los posibles usos que se le puede dar a la herramienta, como por ejemplo su utilización como broker de mensajes mediante colas.

Instalación

No existe una distribución oficial de Redis para Windows, pero se pueden encontrar distribuciones no oficiales, o más simplemente utilizar una imagen de Docker.

En el caso de las distribuciones no oficiales hay que descomprimir el fichero descargado en un directorio y ejecutar redis-server.exe. El fichero de configuración por defecto es redis.conf. Y la forma más sencilla de probar la instalación es usando el cliente de línea de comandos ejecutando redis-cli.exe.

Comentar que Redis soporta de forma nativa su despliegue en una configuración distribuida en cluster con dos modos de persistencia, el modo RDB (Redis Database File), que funciona creando snapshots cada N unidades de tiempo, siempre y cuando se hayan modificado M claves en dicho periodo, y el modo AOF (Append Only File), que funciona almacenando todas las operaciones a fichero de forma inmediata garantizando la integridad de la información a costa de un peor rendimiento.

Claves

Redis almacena todos los datos asociados a una clave única, como las claves primarias en las base de datos relacionales, sólo que las claves en Redis son siempre cadenas.

No hay una forma estándar de escribir claves, sólo unas recomendaciones generales basadas en el uso de namespaces separados por el carácter de dos puntos. Por ejemplo, user:1000:views podría utilizarse para almacenar el número de veces que se ha visto el perfil del usuario con id 1000, pero no deja de ser una recomendación, no una obligación seguir dicho formato.

Todas las claves se tratan siempre en función de su representación a nivel binario. Es decir, no tiene sentido hablar de ningún tipo de codificación, como UTF-8, o juego de caracteres, como ISO-8859-1. Las claves se comparan siempre en base a la secuencia de bytes que las representan.

Comandos específicos para claves:

  • KEYS, obtiene las claves que siguen un patrón
  • SCAN, itera por todas las claves de la base de datos actual seleccionada
  • RANDOMKEY, obtiene una clave de forma aleatoria
  • EXISTS, comprueba si una clave existe
  • RENAME, renombra una clave
  • RENAMENX, renombra una clave, pero sólo la nueva clave no existe
  • DEL, borra una clave
  • UNLINK, borra una clave de forma no bloqueante
  • TYPE, obtiene el tipo del valor almacenado bajo una clave
  • SORT, ordena los elementos de una lista, conjunto o conjunto con pesos
  • EXPIRE, establece el tiempo de vida de una clave en segundos
  • EXPIREAT, establece el tiempo de vida de una clave con un timestamp
  • TTL, obtiene el tiempo de vida de una clave
  • PEXPIRE, establece el tiempo de vida de una clave en milisegundos
  • PEXPIREAT, establece el tiempo de vida de una clave con un timestamp en milisegundos
  • PTTL, obtiene el tiempo de vida de una clave en milisegundos
  • PERSIST, elimina el tiempo de vida de una clave
  • DUMP, obtiene una cadena resultado de serializar una clave
  • RESTORE, restaura una clave a partir de una cadena obtenida con DUMP
  • OBJECT, obtiene un volcado de características internas de una clave
  • TOUCH, modifica la fecha de último acceso a una clave
  • MOVE, mueve una clave a otra base de datos
  • MIGRATE, mueve una clave a otra instancia de Redis

Una característica interesante de Redis es que se puede programar un tiempo de expiración para una clave de forma individual, de forma que pasado ese tiempo Redis la eliminará de memoria automáticamente.

String

Las cadenas de texto son el tipo más básico de Redis. Como ya se ha comentado, es el tipo utilizado para almacenar las claves.

Su caso de uso más habitual es almacenar información a modo de cache. Por ejemplo, para almacenar tokens, cookies, fragmentos estáticos de HTML, o una página completa, utilizando su URL como clave y el HTML como valor.

Comandos específicos para el tipo string:

  • SET, establece el valor de una clave
  • SETNX, establece el valor de una clave, pero sólo si no existe
  • SETEX, establece el valor y tiempo de vida de una clave en segundos
  • PSETEX, establece el valor y tiempo de vida de una clave en milisegundos
  • GET, obtiene el valor de una clave
  • GETSET, establece el valor de una clave y retorna el valor anterior
  • MSET, establece el valor de una serie de claves
  • MSETNX, estable el valor de una serie de claves, pero sólo si ninguna existe
  • MGET, obtiene el valor de una serie de claves
  • INCR, incrementa el valor entero de una clave
  • INCRBY, incrementa el valor entero de una clave por un valor dado
  • INCRBYFLOAT, incrementa el valor decimal de una clave por un valor dado
  • DECR, decrementa el valor entero de una clave
  • DECRBY, decrementa el valor entero de una clave por un valor dado
  • STRLEN, obtiene la longitud del valor de una clave
  • GETRANGE, obtiene una subcadena del valor de una clave
  • SETRANGE, establece una subcadena del valor de una clave
  • APPEND, añade un valor al valor de una clave
  • BITFIELD, ejecuta comandos a nivel de campos de bits sobre el valor de una clave

Comentar que los valores de tipo string en Redis pueden tener una longitud de hasta 512 MB.

Bitmap

Redis no considera bitmap como un tipo, sino como un conjunto de comandos que permiten realizar operaciones a nivel de bit sobre valores del tipo string. Lo que cobra sentido cuando se recuerda que Redis trata las cadenas como secuencias de bytes, no como secuencias de caracteres.

El caso de uso habitual es marcar con un bit a 1 si alguna condición se cumple. Por ejemplo para indicar que el usuario de una aplicación web ha leído y aceptado las condiciones de uso de la misma. O para indicar si quiere recibir correos electrónicos periódicos con las novedades de la misma.

Comandos específicos para el tipo bitmap:

  • SETBIT, establece el valor de un bit
  • GETBIT, obtiene el valor de un bit
  • BITOP, realiza operaciones lógicas a nivel de bit
  • BITCOUNT, cuenta el número de bits a 1
  • BITPOS, obtiene el primer bit a cero o uno

Los bitmaps son adecuados además para señalizar eventos que ocurren con una periodicidad determinada y obtener estadísticas sobre ellos. Por ejemplo, para almacenar si un usuario visita una web cada día, utilizando un bit por día. Siendo lo más habitual distribuir esta información entre varias claves. Por ejemplo, guardando un bitmap por cada año, mes, o periodos más pequeños en función de cada evento concreto.

Spring Boot 2 (y 5)

Para cerrar el ejemplo de uso básico de Spring Boot 2, en este artículo se revisan algunas formas de empaquetar una aplicación web de cara a su distribución. Decidir utilizar un formato u otro depende de cada caso de uso en concreto, no existe una opción mejor que las demás. En algunos entornos/empresas se sigue requiriendo un WAR/EAR para su despliegue en un servidor de aplicaciones, en otros se requiere un único fichero JAR que se pueda ejecutar con una determinada máquina virtual, y en otros se requiere una imagen de Docker.

War

Spring Boot permite empaquetar una aplicación en un fichero WAR, eliminando el servidor embebido del empaquetado, y que se puede desplegar sobre un servidor de aplicaciones. Lo que quiere decir que lo que se obtiene es un único fichero con la aplicación, con sus dependencias incluidas dentro del propio WAR, o proporcionadas por el servidor de aplicaciones con objeto de ser compartidas por todas las aplicaciones desplegadas en el mismo.

Para generar un WAR hay que cambiar el formato de empaquetado del proyecto en el fichero pom.xml

Para excluir el servidor Tomcat embebido por defecto por Spring Boot dentro de la aplicación hay que añadir una entrada en el fichero pom.xml indicando que las librerías de Tomcat son proporcionadas de forma externa:

Para hacer que la aplicación pueda reaccionar al proceso de inicialización del servidor de aplicaciones, ya que ahora será el servidor y no Spring Boot quien arranque la aplicación, se debe cambiar la clase con el punto de entrada de la aplicación para que extienda de SpringBootServletInitializer y sobreescribir su método configure:

Y por último, para generar el WAR, hay que ejecutar la tarea de empaquetado estándar de Maven:

Como resultado de la ejecución de la tarea se genera un fichero WAR en el directorio \target que puede desplegarse sobre un servidor de aplicaciones de la forma acostumbrada.

A pesar de considerarse un modelo obsoleto, esta forma de distribución se sigue utilizando en muchas empresas. Dejar un fichero en un directorio para que lo desplieguen en un servidor de aplicaciones sigue siendo bastante más habitual de lo que debería.

Nested JARs

Spring Boot permite crear un único fichero JAR que contenga todas las dependencias en un formato propio llamado Nested JARs. Lo que quiere decir que lo que se obtiene es un único fichero, con la aplicación y sus dependencias, y se necesita una máquina virtual para ejecutarlo.

Para generar este tipo de ficheros se debe añadir al fichero pom.xml un plugin de Spring Boot:

Y ejecutar la tarea estándar de empaquetado de Maven:

Como resultado de la ejecución de la tarea se genera un fichero JAR en el directorio \target que puede ejecutarse con la máquina virtual de Java de la forma acostumbrada:

El fichero JAR que genera el plugin de Spring Boot contiene tanto las clases de la aplicación como las librerías de terceros. Es un modelo de empaquetado similar al formato Uber JAR, en el que todas las clases de todas las librerías de terceros se incluyen dentro del JAR como si fueran parte de la aplicación. La diferencia es que Spring Boot incluye los JARs completos, en vez de sólo sus clases, y utiliza un loader propio para poder realizar la carga de dichos JARs.

Es conveniente abrir el JAR generado y examinarlo para entender como está construido. Las clases en el directorio raíz son el cargador de Spring Boot, y las librerías están en un directorio propio llamado BOOT-INF, de forma similar a como se encuentran en el directorio WEB-INF de una aplicación web.

En la práctica, sólo he visto utilizar este modelo de distribución en un proyecto, concretamente para una aplicación de escritorio que se incluía dentro de los terminales de punto de venta de una gran cadena comercial.

Docker

Las aplicaciones empaquetadas en un único fichero son adecuadas para su distribución en entornos cloud. La mayoría de plataformas de computación en la nube permiten configurar una capa por encima de las aplicaciones con la máquina virtual de Java o el servidor de aplicaciones que necesiten.

Si una aplicación se distribuye como un WAR necesita un servidor de aplicaciones y una máquina virtual de Java, pero puede haber problemas en el entorno de producción si hay diferencias entre el software proporcionado por la plataforma y el utilizado en el entorno de desarrollo. De igual forma, si una aplicación se distribuye como un JAR con un servidor embebido, sólo necesita una máquina virtual de Java, pero todavía puede haber problemas si las versiones en los entornos de producción y desarrollo son distintas. Por su parte, en una imagen de Docker se empaqueta tanto la aplicación como todo el software que necesita, consiguiendo que los entornos de desarrollo y producción sean lo más parecidos posibles.

La forma más directa de generar una imagen de Docker es escribir manualmente un fichero de texto plano con nombre Dockerfile, tal cual, sin extensión. En este fichero se indica la imagen base que se quiere utilizar y los pasos necesarios para construir la nueva imagen a partir de la base.

Para nuestro ejemplo necesitamos una imagen que tenga una máquina virtual de Java 10. La imagen oficial proporcionada por OpenJDK en su versión slim está basada en Debian y pesa casi 400Mb, pero para el ejemplo el tamaño no importa.

Spring Boot recomienda utilizar un fichero Dockerfile como el siguiente:

En el fichero se parte de la imagen del OpenJDK, se indica que se cree un directorio temporal, se copie el JAR de la aplicación, y que cuando se ejecute la imagen se levante la maquina virtual de Java para correr la aplicación desde el JAR copiado.

El directorio temporal no es estrictamente necesario, pero Spring Boot recomienda crearlo para garantizar que todas las aplicaciones de Java funcionen correctamente, ya que algunas lo necesitan. El parámetro java.security.egd se configura para hacer que Tomcat arranque más rápido, y lo que hace es definir un fuente de entropía no bloqueante para la generación de números aleatorios necesarios para las librerías de seguridad.

Una vez escrito el fichero Dockerfile lo siguiente es invocar a docker desde línea de comandos para construir la imagen de la forma acostumbrada:

La nueva imagen se listará junto con el resto:

Y se podrá ejecutar por su nombre (tag) haciendo público el puerto en el que se encuentra el servicio:

Si todo el proceso se ejecuta de forma correcta la aplicación arrancará y el servicio estará disponible en el puerto indicado.

Para terminar, comentar que existen otros modelos de distribución a parte de los tres vistos. Uno de ellos es empaquetar la aplicación como un autoejecutable, de forma que pueda ejecutarse sin necesidad de anteponer  java -jar, pero sólo funciona en determinados entornos UNIX.

Spring Boot 2 (4)

Una parte importante de todo desarrollo moderno es la elaboración y ejecución automatizada de pruebas. Y a este respecto Spring Boot hereda todas las capacidades que ofrece el framework de Spring. Tanto para pruebas de integración, es decir, aquellas que requieren el auxilio de recursos externos a la propia aplicación, como pruebas unitarias, que pueden realizarse de forma independiente.

Para nuestro caso de uso, la construcción de servicios REST con Spring Boot 2, tiene sentido hacer pruebas para comprobar que el servidor web se levanta, mapea las rutas, y retorna un JSON que cumple con el formato esperado.

Caso de Uso

Como ejemplo utilizaremos una versión modificada de uno de los servicios REST construidos con Spring MVC en artículos anteriores, el que retorna una lista de números desde el 1 a un número dado. Teniendo en cuenta siempre que lo que interesa es probar que se cumple el contrato del servicio, no la implementación del mismo.

El primer paso es incluir las dependencias de Spring MVC en el fichero pom.xml mediante el starter de Spring Boot:

El segundo paso es crear en /src/main/java/{package}  la clase DTO que retornará el servicio:

El tercer paso es crear en /src/main/java/{package} la clase que implementa el controlador del servicio:

Y el último paso es compilar y ejecutar la aplicación con Maven:

Invocando a http://localhost:9999/counter?count=3 desde un navegador debería retornarse un JSON con un campo que contenga el array con la secuencia de números:

Notar que el ejemplo está cambiado con respecto a artículos anteriores con objeto de que no falle si no se proporciona el parámetro de entrada, y para que retorne un JSON en vez de un array. Aún así, si se le pasa un valor que no represente un número entero, el servicio fallará con una excepción de tipo NumberFormatException, mostrándose la página de error por defecto de Spring Boot. En un sistema real explotado en producción, y expuesto de forma pública en Internet, los parámetros deben validarse siempre y este tipo de información no debería exponerse nunca por motivos de seguridad.

SpringBootTest

El primer paso para probar el servicio de ejemplo es añadir al fichero pom.xml las dependencias de Spring Test a través del starter de Spring Boot:

Esta dependencia añade alguna de las librerías más populares utilizadas para la elaboración de pruebas. En particular JUnit, Hamcrest, Mockito, AssertJ y JSONassert, entre otras. Esto permite implementar las pruebas directamente con JUnit. Lo que es una ventaja en la medida que la mayoría de IDEs se integran con este framework y permiten lanzar las pruebas sin necesidad de pasar por Maven.

El segundo paso es crear en /src/test/java/{package}  una clase con un caso de prueba:

La anotación @RunWith sirve para indicar el runner que se quiere utilizar con JUnit. La anotación @SpringBootTest sirve para indicar que la clase ejecuta casos de prueba de una aplicación de Spring Boot. Y el parámetro WebEnvironment.RANDOM_PORT indica que se quiere arrancar el servidor en un puerto aleatorio, para evitar conflictos en caso de lanzar varias pruebas en paralelo.

La instancia inyectada de la clase TestRestTemplate es registrada automáticamente como resultado de aplicar la anotación @SpringBootTest sobre la clase, y tiene la ventaja de que todas las llamadas HTTP que realice las hará contra el servidor web embebido arrancado para la prueba. Con la versión 5 de Spring se prefiere el uso de la clase WebTestClient en vez de TestRestTemplate, ya que su API sigue un estilo fluent para las aserciones, pero en la release actual sólo se registra automáticamente cuando se utiliza WebFlux y no Spring MVC.

Cuando se ejecute la prueba se arrancará el servidor web embebido, se invocará al servicio a través de restTemplate, y se comprobará que por defecto devuelve una cadena de texto que representa un JSON con un array vacío.

En un IDE moderno, como Eclipse Photon, las pruebas con JUnit se pueden ejecutar de varias formas de una manera extremadamente sencilla. Por ejemplo, pulsando el botón derecho sobre el método de la prueba y ejecutándolo a través del menú contextual.

WebMvcTest

En algunos casos tener que arrancar un servidor web puede ser un inconveniente, sobre todo cuando lo que se quiere probar es sólo un controlador de la forma más rápida posible. Para evitar esto es posible reescribir la prueba anterior utilizando las facilidades de prueba de Spring MVC, en vez de las de Spring Boot. Lo que quiere decir que se puede invocar al controlador, a la manera de Spring MCV, sin tener que levantar el servidor, que es la manera de Spring Boot.

La anotación @WebMvcTest permite especificar el controlador que se quiere probar y tiene el efecto añadido que registra algunos beans de Spring, en particular una instancia de la clase MockMvc, que se puede utilizar para invocar al controlador simulando la llamada HTTP sin tener que arrancar realmente ningún servidor web.

Si se ejecuta la prueba se puede comprobar en el log que el servidor embebido no arranca, pero que el servicio es invocado y retorna el objeto JSON con un array vacío de igual forma que con el método del apartado anterior.

JSON

El inconveniente de los métodos de prueba de los apartados anteriores es que comprueban el resultado como una cadena de texto, sin ninguna estructura. Un espacio en blanco, o un orden distinto en los campos retornados, provocaría que la prueba fallase.

Para evitar estos problemas es mejor tratar el resultado como JSON y reescribir el método para que no opere con cadenas de texto.

Una forma sencilla de hacerlo es cambiar el método string por el método json:

La diferencia entre ambos métodos es que el primero es estricto en la comprobación que realiza entre las dos cadenas de texto, mientras que el segundo comprueba la similitud entre las dos cadenas tratándolas como si fueran representaciones en formato texto de objetos JSON. Por eso la prueba sigue ejecutándose con éxito, a pesar de los espacios añadidos a la cadena de texto utilizada para la comprobación.

Evidentemente sigue sin ser una solución correcta, ya que implica seguir escribiendo cadenas de texto, e impide utilizar las facilidades de comprobación de nombres y tipos del IDE. Una mejor solución es trabajar directamente con clases Java mapeadas como objetos JSON.

La anotación @AutoConfigureJsonTesters registra automáticamente una serie de beans de Spring para trabajar con librerías como Jackson, Gson y Jsonb, existiendo además anotaciones específicas para cada una de estas librerías. La instancia inyectada de la clase JacksonTester trabaja con Jackson y se utiliza para la prueba porque es la librería que se utiliza por defecto en las aplicaciones de Spring Boot.

De esta forma se evita utilizar cadenas de texto y se pueden escribir pruebas más elaboradas.

Otra opción disponible es comparar el JSON devuelto por el servicio con un JSON almacenado en un fichero.

Resumiendo, Spring ofrece toda una variedad de configuraciones para la realización de pruebas, no sólo las básicas vistas en este artículo, sino otras más específicas para probar servicios que utilizan JPA, LDAP, Redis, MongoDB, junto con algunas otras que merece la pena explorar en la documentación oficial de referencia.