Skip to content

dartis (y 2)

En este artículo se continúa la revisión de las características ofrecidas por dartis, un cliente para Redis escrito en Dart que publiqué hace unos días.

Pub/Sub

Redis permite que los clientes entren en un modo de funcionamiento especial en el que se implementa el patrón Publish/Subscribe. En este modo los clientes pueden suscribirse a canales y recibir mensajes a través de ellos. Es decir, que Redis se comporta como un sistema de mensajería. En este modo los clientes sólo pueden ejecutar los comandos SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PING y QUIT.

El servidor envía, tanto las respuestas a las suscripciones a canales, como los propios mensajes publicados en dichos canales, en un stream de eventos con una estructura de mensajes distinta a la que se recibe cuando el cliente se encuentra operando en el modo normal, por lo que se debe conocer en todo momento en que modo se encuentra trabajando el cliente para poder procesar correctamente las respuestas recibidas del servidor.

La conexión a este modo de funcionamiento se realiza de forma explícita en dartis a través de una factoría estática. Y los eventos recibidos se publican a través de un Stream.

final pubsub = await PubSub.connect<String, String>('redis://localhost:6379');

pubsub.subscribe(channel: 'dev.dart');

pubsub.stream.listen(print, onError: print);

Tener una clase especializada para implementar este modo permite reducir la complejidad del código. Máxime cuando Redis sólo permite a los clientes en este modo ejecutar un conjunto de comandos muy concretos y salir terminando la conexión con el servidor. Aunque impide que el cliente ejecute otros comandos antes de entrar en este modo, como por ejemplo validarse contra el servidor utilizando una clave.

Monitor

Redis permite también que los clientes trabajen en modo monitor. En este modo no pueden ejecutar ningún comando, se limitan a recibir mensajes del servidor. Los mensajes recibidos contienen un texto con detalles acerca de todos los comandos ejecutados por todos los clientes contra el servidor. Es decir, que cada vez que un cliente ejecuta un comando, el servidor envía un mensaje informando de tal acción a todos los clientes que se encuentran en modo monitor. Esto resulta de utilidad para depurar aplicaciones de forma no intrusiva.

La conexión a este modo de funcionamiento se realiza de forma explícita en dartis a través de una factoría estática. Y los eventos recibidos se publican a través de un Stream.

final monitor = await Monitor.connect('redis://localhost:6379');

monitor.start();

monitor.stream.listen(print);

De igual forma que en el modo del apartado anterior, disponer de una clase especializada reduce la complejidad de la implementación, pero no permite ejecutar ningún comando previo a la entrada en este modo.

Inline Commands

Redis permite la ejecución de comandos sin tener que implementar el protocolo RESP. En este modo los comandos se envían como cadenas de texto plano al servidor. Es similar a como se haría a través de una sesión Telnet, escribiendo directamente en una consola de texto.

final terminal = await Terminal.connect('redis://localhost:6379');

terminal.run('PING\r\n'.codeUnits);

terminal.stream.listen(print);

Las respuestas recibidas desde el servidor en este modo están formateadas siguiendo el protocolo RESP, por lo que este modo es adecuado para comprobar la respuesta exacta del servidor ante un determinado comando, evitando cualquier tipo de agente intermedio que la procese o altere de alguna forma.

Transacciones

Redis permite ejecutar varios comandos dentro del contexto de una transacción. Aunque no es una acción de “todo o nada” como habitualmente se sobreentiende que funciona una transacción con la mayoría de software existente.

Una transacción empieza en Redis ejecutando el comando MULTI y termina ejecutando el comando EXEC. Todos los comandos que se ejecutan después de MULTI se encolan y se ejecutan cuando se llama a EXEC. Si se produce un error encolando un comando la transacción se aborta en el momento que se llama a EXEC, no en el momento que se produce el error encolando el comando. Y si se produce un error durante la ejecución de un comando encolado se continúa con el siguiente comando encolado, no se produce un rollback de lo ejecutado hasta el momento.

EXEC retorna el resultado de la ejecución de todos los comandos ejecutados en el contexto de la transacción, tanto los terminados con éxito como los terminados con error, para dar la oportunidad al cliente de actuar en función del resultado individual de cada comando.

await commands.multi();

commands.set(key, 1).then(print);
commands.incr(key).then(print);

await commands.exec();

Con este patrón no se puede utilizar await  sobre cada comando, ya que el Future de cada comando no se completa hasta que se obtiene la respuesta del servidor, algo que no ocurre hasta ejecutar EXEC.

Una transacción iniciada con MULTI puede abortarse ejecutando el command DISCARD antes de llamar a EXEC. El comando DISCARD termina la transacción en curso descartando todos los comandos encolados hasta el momento.

Por último, comentar que en el contexto de una transacción no se debe utilizar el comando CLIENT REPLY, ya que en determinados casos el cliente puede perder la sincronía con el servidor. Es por ello que las transacciones en Redis están documentadas como una funcionalidad a ser deprecada, prefiriéndose el uso de scripts en Lua en el servidor.

Scripts Lua

Redis permite ejecutar scripts en Lua en el servidor. Esta característica es ofrecida a través de comandos, por lo que no requiere ninguna implementación especial por parte del cliente.

await commands.eval<void>(
  'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}',
  keys: [key1, key2],
  args: ['first', 'second']);

La única característica particular a tener en cuenta con esta funcionalidad es que la salida de la ejecución de un script puede ser de cualquier tipo. Lo mismo puede resultar en una primitiva, como una cadena de texto o un entero, o un array de valores heterogéneos. Para lidiar con esta situación, dartis admite un parámetro que permite mapear la respuesta del servidor en cualquier tipo que se quiera.

class CustomMapper implements Mapper<List<String>> {
  @override
  List<String> map(Reply reply, RedisCodec codec) =>
    codec.decode<List<String>>(reply);
  }
...
final results = await commands.eval<List<String>>(
  'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}',
  keys: [key1, key2],
  args: ['first', 'second'],
  mapper: CustomMapper());

print(results); // ['key1', 'key2', 'first', 'second']

En el ejemplo del código anterior, el mapper convierte la respuesta del servidor en una lista de cadenas de texto utilizando las facilidades que ofrece dartis para crear serializadores y deserializadores personalizados.

Serializadores/Deserializadores

dartis tiene un sistema de conversores dinámico que permite añadir conversores nuevos o sobreescribir los existentes. Un conversor es una clase que convierte una primitiva en una lista de bytes de cara a ser enviado al servidor, y convierte una lista de respuestas del servidor o bytes en una primitiva de cara a que un cliente pueda utilizarla en una aplicación.

Por defecto hay registrados conversores para los tipos de uso más habitual como enteros, decimales, cadenas de texto y listas. Utilizando UTF-8 por defecto para las cadenas de texto.

Los conversores que transforman primitivas en listas de bytes se denominan codificadores, y son clases que se crean extendiendo de Converter o Encoder. Por ejemplo, el siguiente código muestra un conversor que transforma instancias del tipo DateTime de Dart en listas de bytes.

class DateTimeEncoder extends Encoder<DateTime> {
  @override
    List<int> convert(DateTime value, [RedisCodec codec]) =>
    utf8.encode(value.toString());
  }
}

Por su parte, los conversores que transforman listas de respuestas del servidor o bytes en una primitiva se denominan decodificadores, y son clases que se crean extendiendo de Converter o Decoder. Por ejemplo, el siguiente código muestra un conversor que transforma una respuesta del servidor en una instancia del tipo DateTime de Dart.

class DateTimeDecoder extends Decoder<SingleReply, DateTime> {
@override
DateTime convert(SingleReply value, RedisCodec codec) =>
value.bytes == null ? null : DateTime.parse(utf8.decode(value.bytes));
}

Todos los codificadores se registran utilizando el atributo codec del cliente.

client.codec.register(
  encoder: DateTimeEncoder(),
  decoder: DateTimeDecoder());

Permitir definir tipos personalizados facilita trabajar de forma sencilla con los comandos propios que pueda definir cualquier módulo para Redis. Siendo un módulo una extensión que se puede añadir a un servidor Redis, a modo de plugin, y que dartis soporta de manera natural.

Y estas son básicamente las principales características de la librería. En el propio repositorio del proyecto puede encontrarse más documentación y ejemplos de uso.

Para terminar, comentar que el proyecto está desarrollado utilizando Visual Studio Code como IDE con tan sólo un par de extensiones. EditorConfig para garantizar la uniformidad del formato de los ficheros, y Dart Code para la integración de las herramientas del SDK, en particular el formateador de código y el analizador de código estático. Travis como servidor de integración continua y Coveralls para el análisis de la cobertura del código. El proyecto tiene implementadas unos trescientos casos de prueba, incluyendo tanto pruebas unitarias como de integración, con un noventa por ciento de cobertura.

dartis (1)

Hace un par de días liberé dartis, un cliente para Redis escrito en Dart. Empecé a desarrollarlo para probar algunas de las novedades de la versión 2 de Dart y al final he acabado con un cliente sencillo pero bastante completo.

RESP

REdis Serialization Protocol (RESP) es el protocolo que implementa Redis para comunicar los clientes con el servidor. Es un protocolo muy básico sobre TCP, con tan sólo cinco tipos de paquetes distintos. Los mensajes son todos de tipo texto, con un primer carácter identificador del tipo de paquete, seguido de una longitud en algunos casos, a continuación el contenido del mensaje en sí mismo, y un par de caracteres de terminación.

El hecho de que sea un protocolo de tipo texto tiene la ventaja de que resulta fácil de implementar y depurar, pero tiene el inconveniente de que hay que convertir entre binario y texto continuamente, en particular las longitudes de los mensajes. En principio he optado por utilizar las facilidades del propio lenguaje para las conversiones, sin realizar ninguna medición del rendimiento, pero podría estudiarse la posibilidad de hacer una implementación más específica.

Por otra parte, debido a que los paquetes no contienen ningún tipo de identificador, es necesario guardar la secuencia de cada comando enviado de forma ordenada y casarla contra las respuestas recibidas en el mismo orden. Con el hándicap de que es posible deshabilitar las respuestas del servidor, por lo que en algunos casos los paquetes han de enviarse sin esperar respuesta. En determinadas circunstancias muy concretas el protocolo no impide que se produzcan pérdidas de sincronía entre cliente y servidor.

Conexión

La conexión al servidor se realiza siguiendo el típico patrón consistente en utilizar un método estático en la clase del cliente a modo de factoría.

final client = await Client.connect('redis://localhost:6379');
…
await client.disconnect();

Como parámetro se admite una cadena de conexión siguiendo el formato clásico de protocolo, host y puerto. Lo que posibilita una configuración muy sencilla y ampliable.
Redis permite trabajar con varias bases de datos dentro de una misma instancia, pero esta forma de trabajo no está totalmente soportada cuando se utiliza una instalación de Redis en cluster, por lo que es mejor no utilizar esta característica si no es absolutamente necesaria. Resulta más sencillo levantar más instancias.

Redis permite proteger de forma opcional con una clave los accesos al servidor, pero normalmente las instancias de Redis no se encuentran abiertas al mundo exterior, sino como dentro de una DMZ, por lo que no suele utilizarse, aunque ello depende en gran medida de las políticas de seguridad de cada cual.

Algunos puntos abiertos en la implementación de las conexiones con el servidor son la posibilidad de indicar un timeout, realizar reconexiones automáticas en caso de pérdida de conexión, y un pool de conexiones.

Comandos

Un cliente le comunica a un servidor Redis lo que quiere hacer a través de comandos. Y existen más de 200 comandos distintos. dartis implementa una interface genérica que permite ejecutar cualquier tipo de comando, a la vez que ofrece una interface más específica que expone todos los comandos ofrecidos por Redis de forma fuertemente tipada.

dartis funciona de forma totalmente asíncrona, por lo que todos los comandos retornan un Future que se completa cuando el servidor retorna el resultado.

Además, el conjunto de comandos está expuesto utilizando Generics, lo que permite que cada aplicación que utilice la librería pueda decidir el tipo concreto de datos que quiere utilizar, en vez de limitarse a utilizar String como hacen otras librerías.

final commands = client.asCommands<String, String>();

await commands.set(‘key, ‘value’);
final value = await commands.get(‘key’);

print(value);

Redis almacena secuencias de bytes, no cadenas de textos, por lo que puede utilizarse para gestionar cualquier tipo de información.

final commands = client.asCommands<String, List<int>();

await commands.set(‘key’, <int>[1, 2, 3]);

Los comandos están expuestos en forma de vista sobre el cliente, es decir, es la misma instancia pero exponiendo sólo una interface sobre la misma, por lo que pueden obtenerse distintas vistas del mismo cliente para trabajar con distintos tipos de datos.

final strings = client.asCommands<String, String>();
final bytes = client.asCommands<String, List<int>>();

String title = await strings.get('book:24902:title');
List<int> cover = await bytes.get('book:24902:cover');

Es seguro trabajar con varias vistas a un mismo tiempo, ya que todas utilizan el mismo cliente.

Pipelining

Redis permite enviar una al servidor una serie de comandos a un mismo tiempo, en vez de uno a uno. Aunque en la práctica esta es más una característica propia de las comunicaciones que de Redis. El cliente puede enviar más de un comando dentro de un mismo paquete TCP. Y el servidor puede procesar paquetes TCP que contengan más de un comando.
Por defecto dartis sólo envía un comando en cada paquete TCP, deshabilitando de forma explícita el algoritmo de Nagle sobre el socket, para garantizar que cada comando se envía de forma inmediata al servidor, pero permite activar el pipelining llamando al método pipeline:

client.pipeline();

commands.incr('product:9238:views').then(print);
commands.incr('product:1725:views').then(print);
commands.incr('product:4560:views').then(print);

client.flush();

En buena lógica, con este patrón no se puede utilizar await sobre cada comando, ya que el Future de cada comando no se completa hasta que se obtiene respuesta del servidor, y los comandos no se envían al servidor hasta que se vacía el pipeline con la llamada al método flush.

Otra opción es utilizar la lista de Futures retornada por el método flush para esperar hasta que se completen todos los comandos.

client.pipeline();

commands
..incr('product:9238:views')
..incr('product:1725:views')
..incr('product:4560:views');

final futures = client.flush();

await Future.wait<Object>(futures).then(print);

Esta forma de trabajo se utiliza sobre todo para realizar cargas masivas de información o clientes con casos de uso concretos que requieren un mayor rendimiento.

Fire And Forget

Redis permite que los clientes indiquen al servidor que no quieren recibir respuestas del servidor. Es decir, se pueden enviar comandos para su ejecución y desentenderse del resultado (fire and forget).

Esta es una característica que Redis ofrece a través del comando CLIENT REPLY. El comando necesita un parámetro que admite el valor OFF para deshabilitar todas las respuestas, SKIP para deshabilitar sólo la respuesta del comando siguiente, y ON para habilitar todas las respuestas.

En este modo los comandos se completan de forma inmediata con el valor null cuando las respuestas del servidor están deshabilitadas.

await commands.clientReply(ReplyMode.off);

await commands.ping().then(print); // null
await commands.ping().then(print); // null
await commands.ping().then(print); // null

La implementación de esta característica no es compleja, pero requiere que el cliente detecte de manera explícita que se está ejecutando el comando CLIENT REPLY, y llevar la cuenta de para que comandos se debe esperar respuesta y para cuáles no. Si el propio comando CLIENT REPLY es el que falla entonces puede llegar a perderse la sincronía entre servidor y cliente, aunque se presupone una casuística con una probabilidad muy baja de que suceda.

MongoDB: Hands on!

MongoDB es una base de datos NoSQL distribuida y escalable que gestiona objetos almacenados en un formato llamado BSON. Un formato inspirado en el formato JSON, pero que almacena la información de forma binaria. Es habitual pensar en los objetos almacenados en MongoDB, llamados documentos, como objetos en formato JSON.

Una de las características más significativas de MongoDB es que es una base de datos de esquema dinámico, es decir, que no fuerza un esquema fijo predefinido como las base de datos relacionales. Cada documento (objeto JSON) puede tener los campos y tipos que se quieran. Aunque si realmente se necesita, es posible forzar unas reglas de validación siguiendo la especificación JSON Schema.

MongoDB permite almacenar los documentos agrupados en colecciones, equivalente a las tablas del modelo relacional, y realizar consultas sobre estas colecciones. Estas consultas pueden realizarse sobre cualquier campo de los documentos de una manera eficiente, ya que es posible crear índices, tanto sobre campos de primer nivel, como campos de objetos JSON anidados, e incluso arrays. Además, es posible realizar consultas agregadas sobre cualquier campo, a la manera de una cláusula GROUP BY en SQL. MongoDB da una gran importancia a las agregaciones, permitiendo incluso ejecutar varias agregaciones en paralelo con una única consulta.

Instalación

La instalación de MongoDB puede realizarse en local descargando un fichero comprimido, o alternativamente usando una imagen de Docker.

En Windows el ejecutable del servidor es mongod.exe, y para arrancarlo hay que pasarle el parámetro –dbpath con el nombre de un directorio existente. El cliente de línea de comandos es mongo.exe, y al arrancarlo se conectará automáticamente al servidor local por defecto.

El cliente GUI oficial de MongoDB se llama Compass, disponible a través de suscripción o de una versión Community que requiere registro. Como alternativa pueden encontrarse distintos clientes no oficiales, desarrollados tanto por empresas como particulares en régimen de código abierto. Uno de los clientes más populares es Robomongo, que fue adquirido por una empresa llamada 3T Software Labs, y le ha cambiado el nombre a la herramienta por el de Robo 3T. Su versión gratuita portable para Windows no requiere instalación, basta con descomprimir el fichero descargado en un directorio y lanzar el ejecutable.

En lo que sigue se entenderá que los ejemplos se ejecutan directamente sobre el cliente de línea de comandos mongo.exe contra una base de datos local. En la práctica se puede utilizar cualquier cliente contra cualquier servidor, ya sea local o proporcionado por un tercero. La propia MongoDB proporciona instancias en la nube a través de su servicio Atlas.

Comandos

MongoDB ofrece una extensa lista de comandos organizados en los siguientes grupos:

  • Conexiones
  • Bases de Datos
  • Gestión de Usuarios
  • Gestión de Roles
  • Colecciones
  • Cursores
  • Construcción de Objetos
  • Actualización Masiva de Datos
  • Planes de Ejecución de Consultas
  • Réplica
  • Balanceado de Carga
  • Monitorización
  • Invocación de Comandos Nativos

La lista de comandos es demasiado extensa como para examinarlos todos, por lo que es imprescindible revisar la documentación oficial que constituye la referencia más actualizada que se puede encontrar al respecto.

Como es habitual, los comandos que más se usan son los de acceso de datos, es decir, los de lectura y escritura sobre colecciones. En cualquier caso, la secuencia de creación de una base de datos y una colección es tan sencilla como la ejecución de los siguientes comandos:

> use tienda
switched to db tienda

> db.createCollection('clientes')
{ "ok" : 1 }

La sintaxis del shell por defecto es JavaScript y resulta sencilla de entender. En las dos líneas anteriores lo único que resulta conveniente aclarar es que el objeto global db del segundo comando referencia automáticamente a la base de datos seleccionada en el primer comando. Si la base de datos no existe se instancia en el momento que se crea una colección.

CRUD

Los documentos (objetos JSON) se insertan y recuperan en la colecciones siguiendo la sintaxis de JSON:

> db.clientes.insertOne({nombre: 'Alberto', edad: 29})
{
"acknowledged" : true,
"insertedId" : ObjectId("5b55eda9251e77b7ec9bb363")
}

> db.clientes.find()
{ "_id" : ObjectId("5b55eda9251e77b7ec9bb363"), "nombre" : "Alberto", "edad" : 29 }

> db.clientes.find().pretty()
{
  "_id" : ObjectId("5b55eda9251e77b7ec9bb363"),
  "nombre" : "Alberto",
  "edad" : 29
}

> db.clientes.findOne({edad: 29})
{
  "_id" : ObjectId("5b55eda9251e77b7ec9bb363"),
  "nombre" : "Alberto",
  "edad" : 29
}

Como se observa, MongoDB añade automáticamente a los documentos el atributo _id  con un UUID autogenerado a modo de clave primaria. Opcionalmente se puede suministrar este valor desde el cliente, pero en la mayoría de casos esta opción no se utiliza.

Donde MongoDB se sale un poco del tiesto con la nomenclatura es en las modificaciones. Por defecto la base de datos trabaja con documentos completos, no parciales, por lo que para modificar o eliminar un atributo es necesario reescribir el documento completo. Para evitar dicho trasiego de información, MongoDB permite indicar lo que denomina operadores de actualización (update operators) en los comandos de modificación de documentos:

> db.clientes.update(
... {_id: ObjectId('5b55eda9251e77b7ec9bb363')},
... {$set: {genero: 'masculino'},
... $inc: {edad: 5}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

> db.clientes.find().pretty()
{
  "_id" : ObjectId("5b55eda9251e77b7ec9bb363"),
  "nombre" : "Alberto",
  "edad" : 34,
  "genero" : "masculino"
}

Como se observa, los operadores empiezan por el símbolo del dolar, e indican la operación concreta a realizar sobre los atributos. $set añade un nuevo atributo al documento, $incr incrementa el valor de un atributo, y existen otros como $unset que elimina un atributo. El mismo concepto se utiliza para realizar filtros con condiciones lógicas, aplicando operadores con nombres bastantes descriptivos como $and y $or por citar un par de ellos. Leer la documentación para hacerse una idea de las opciones disponibles es imprescindible.

El borrado de documentos es trivial utilizando un filtro:

> db.clientes.remove({_id: ObjectId('5b55eda9251e77b7ec9bb363')})
WriteResult({ "nRemoved" : 1 })

> db.clientes.count()
0

Índices

MongoDB crea automáticamente un índice por el campo _id de todas las colecciones, y permite además definir índices sobre otros campos para mejorar el rendimiento de las consultas más habituales:

> db.clientes.ensureIndex({nombre:1})
{
  "createdCollectionAutomatically" : false,
  "numIndexesBefore" : 1,
  "numIndexesAfter" : 2,
  "ok" : 1
}

El valor 1 pasado como parámetro en la operación indica que se quiere que el orden del índice sea ascendente. Un valor -1 indicaría que se quiere que sea descendente.

En la práctica MongoDB permite crear una gran variedad de índices en función de los elementos sobre los que se definan:

  • Sobre un único campo
  • Sobre múltiples campos
  • Sobre los elementos de un campo de tipo array
  • Sobre campos de coordenadas geoespaciales
  • Sobre campos de tipo string para búsquedas de texto
  • Sobre colecciones distribuidas por varios servidores
  • Sobre un conjunto filtrado de documentos de una colección
  • Sobre documentos que tengan un determinado campo
  • Sobre documentos que expiran transcurrido un tiempo

Interesante comentar en este punto que MongoDB permite obtener el plan de ejecución de una consulta, incluyendo los índices utilizados:

> db.clientes.find({nombre:'Alberto'})
... .explain('executionStats')
{
  "queryPlanner" : {
...
    "winningPlan" : {
...
      "indexName" : "nombre_1",
...

Joins

Para recuperar dos documentos relacionados, que se encuentren en dos colecciones distintas, es necesario indicar el nombre de las dos colecciones y el nombre de los dos campos que las relacionan:

> db.createCollection('paises')
{ "ok" : 1 }

> db.createCollection('capitales')
{ "ok" : 1 }

> db.paises.insertOne({nombre: 'Italia'})
{
  "acknowledged" : true,
  "insertedId" : ObjectId("5b576be4251e77b7ec9bb364")
}

> db.captitales.insertOne(
... {nombre: 'Roma',
... pais_id: ObjectId("5b576be4251e77b7ec9bb364")})
{
  "acknowledged" : true,
  "insertedId" : ObjectId("5b576c6f251e77b7ec9bb365")
}

> db.capitales.aggregate([
... {$lookup: {
... localField: 'pais_id',
... from: 'paises',
... foreignField: '_id',
... as: 'pais'}}]).pretty()
{
  "_id" : ObjectId("5b576c6f251e77b7ec9bb365"),
  "nombre" : "Roma",
  "pais_id" : ObjectId("5b576be4251e77b7ec9bb364"),
  "pais" : [
    {
      "_id" : ObjectId("5b576be4251e77b7ec9bb364"),
      "nombre" : "Italia"
    }
  ]
}

Como se observa, MongoDB trata esta operación como una agregación en vez de una consulta simple, y es en el parámetro $lookup donde se indican los nombres y colecciones sobre los que realizar el join, así como el nombre del objeto sobre el que proyectar el resultado.

MongoDB trata todos los joins como «left outer joins«, por lo que las consultas siempre retornan resultado incluso cuando no puede satisfacerse el join.

Agregaciones

Las agregaciones en MongoDB son un tipo de consultas que van más allá de la simple lectura de uno o varios documentos de una colección. De hecho, MongoDB las considera un framework en si mismo, una alternativa al patrón map-reduce.

Las agregaciones se construyen como una secuencia de operaciones, denominada pipeline, que se ejecuta sobre una colección. Cada una de las operaciones, denominadas stages, generan un resultado o modifican el resultado de la operación anterior.

> db.createCollection('nevera')
{ "ok" : 1 }

> db.nevera.insert([
... {nombre: 'ternera', tipo: 'carne', cantidad: 1},
... {nombre: 'pollo', tipo: 'carne', cantidad: 1},
... {nombre: 'queso', tipo: 'lacteos', cantidad: 4},
... {nombre: 'lomo', tipo: 'carne', cantidad: 2},
... {nombre: 'huevos', tipo: 'huevos', cantidad: 6}])
BulkWriteResult({
  "writeErrors" : [ ],
  "writeConcernErrors" : [ ],
  "nInserted" : 5,
  "nUpserted" : 0,
  "nMatched" : 0,
  "nModified" : 0,
  "nRemoved" : 0,
  "upserted" : [ ]
})

> db.nevera.aggregate([
... {
...   $group: {
...     _id: null,
...     count: {$sum: 1}
...   }
... }
... ])
{ "_id" : null, "count" : 5 }

> db.nevera.aggregate([
... {
...   $group: {
...     _id: '$tipo',
...     count: {$sum: 1}
...   }
... }
... ])
{ "_id" : "huevos", "count" : 1 }
{ "_id" : "lacteos", "count" : 1 }
{ "_id" : "carne", "count" : 3 }

> db.nevera.aggregate([
... {
...   $group: {
...     _id: '$tipo',
...     count: {$sum: '$cantidad'}
...   }
... }
... ])
{ "_id" : "huevos", "count" : 6 }
{ "_id" : "lacteos", "count" : 4 }
{ "_id" : "carne", "count" : 4 }

> db.nevera.aggregate([
... {
...   $group: {
...     _id: '$tipo',
...     count: {$sum: '$cantidad'}
...   }
... },
... {
...   $sort: {
...     _id: 1
...   }
... }
... ])
{ "_id" : "carne", "count" : 4 }
{ "_id" : "huevos", "count" : 6 }
{ "_id" : "lacteos", "count" : 4 }

> db.nevera.aggregate([
... {
...   $match: {
...     tipo: 'carne'
...   }
... },
... {
...   $group: {
...     _id: '$nombre',
...     count: {$sum: '$cantidad'}
...   }
... },
... {
...   $sort: {
...     count: -1
...   }
... }
... ])
{ "_id" : "lomo", "count" : 2 }
{ "_id" : "pollo", "count" : 1 }
{ "_id" : "ternera", "count" : 1 }

Como se observa, el parámetro de entrada es un array de objetos (pipeline). Cada objeto es una operación (stage). Y MongoDB las ejecuta de forma secuencial, por lo que el orden es importante. Se puede empezar con una operación e ir añadiendo operaciones hasta que se obtenga el resultado deseado. El tipo de operaciones que se puede realizar es bastante extenso, por lo que es recomendable consultar la documentación de referencia para hacerse una idea de todas las opciones disponibles.

Las operaciones más comunes son $match para filtrar, $sort para ordenar, $group para agrupar, $count para contar ocurrencias, $skip para ignorar elementos, y $limit  para limitar el tamaño del resultado. Otras operaciones interesantes son por ejemplo $unwind para extraer elementos de los arrays embebidos en los documentos, $facet para calcular varias agregaciones en paralelo, y $graphLookup para hacer búsquedas recursivas.

Otros

Para terminar, comentar que MongoDB permite definir colecciones con un tamaño máximo prefijado que se caracterizan por ofrecer un rendimiento muy eficiente. Cuando el número de documentos insertados en dichas colecciones supera dicho máximo los documentos más antiguos se sobreescriben por los nuevos. Si se realiza una consulta sin especificar ningún orden se garantiza que los documentos se retornarán en el mismo orden en que se insertaron, pudiéndose indicar también que se retornen en el orden inverso. Este tipo de colecciones permiten además crear cursores sobre los últimos N elementos de los mismos, como el comando tail de UNIX, por lo que pueden utilizarse para almacenar documentos como si fueran entradas en un log.

Además, ofrece la posiblidad de definir un tiempo de expiración para los documentos, de forma que cuando se supere dicho tiempo el documento será automáticamente eliminado de la base de datos. Una característica muy popular en otra base de datos NoSQL como es Redis.

Y por supuesto, es una base de datos altamente escalable que posibilita implantarla utilizando distintas configuraciones de réplica y alta disponibilidad, incluyendo la posibilidad de distribuir colecciones sobre diversos servidores.

En definitiva, el servidor de base de datos orientado a documentos más popular hoy en día por el tiempo que lleva en el mercado y las posibilidades que ofrece.

MongoDB

Estos últimos meses están siendo muy prolíficos en cuanto a major releases se refiere. MongoDB recientemente publicó la versión 4 de su conocida base de datos NoSQL. La mayor novedad es que permite realizar operaciones sobre varios documentos dentro de una misma transacción de forma atómica. Una característica que al parecer llamó más la atención cuando se anunció que ahora que se ha liberado. No obstante, el cambio es importante, posiblemente el de mayor impacto desde que en la versión 3.2 se introdujo la posibilidad de realizar «left outer joins» entre documentos de colecciones distintas.

Decidir si MongoDB es la herramienta adecuada para un proyecto es un tema recurrente cuando se decide la arquitectura de una aplicación. A veces la tecnología viene impuesta por contrato, la mayoría de veces simplemente se utiliza lo ya que se conoce, y en algunas ocasiones se utiliza sólo para poder añadirla al portfolio de la empresa.

MongoDB es una base de datos, por lo que decidir utilizarla dependerá en buena medida del modelo de datos y los casos de uso fundamentales que se necesitan cubrir, ya que un modelo de datos no tiene porque ser visto sólo como un mero almacén de información. Las operaciones que se van a realizar sobre él son tan importantes como sus entidades y atributos. Y junto a ellos, las estimaciones de los volúmenes de información que se espera manejar de cara a decidir que gestor de base de datos utilizar.

Modelo Dinámico

MongoDB dice de si mismo que es adecuado cuando los modelos de datos no sean estáticos, cuando instancias de una misma entidad puedan tener un conjunto distinto de atributos. Un ejemplo: los productos de una gran superficie comercial. No es lo mismo una barra de pan que un calentador de agua, pero no por ello dejan de ser «productos». El calentador tendrá marca, modelo, altura, anchura, profundidad, capacidad, y otra serie de atributos propios de acuerdo a sus especificaciones técnicas. Una barra de pan tendrá peso, tamaño, variedad, y otros atributos de acuerdo a su composición química. Es bastante probable que sus datos provengan de fuentes muy distintas y se realice un tratamiento distinto para  cada uno de ellos, pero no por eso dejan de ser productos de un mismo catálogo que sería deseable tratar de una forma conjunta.

En la práctica la mayoría de este tipo de problemas se resuelven en tiempo de diseño adoptando soluciones fáciles de implementar. Siguiendo con el ejemplo del catálogo de productos, es bastante habitual que todos los atributos específicos de un producto acaben en un campo de texto de gran tamaño a modo de descripción. Lógicamente, el mayor inconveniente de esta solución es que la información no se puede explotar de una manera sencilla al no estar estructurada. Otra solución es utilizar MongoDB.

En MongoDB las entidades se llaman documentos y se guardan en colecciones mediante transacciones ACID. Cada documento dentro de una misma colección puede tener el número de campos que se quiera, y cada campo puede ser del tipo que se quiera. Lógicamente se tiende a guardar documentos del mismo tipo en una misma colección. Así, una colección de «productos» podría contener un documento con campos que describan una barra de pan y otro que describan un calentador de agua. Es un caso de uso que MongoDB cubre de una manera natural gracias a que trabaja con un modelo de datos dinámico, frente a una base de datos relacional que trabaja con un modelo de datos predefinido.

JSON

MongoDB capturó la atención de muchos desarrolladores presentando los documentos en formato JSON. Debido a que el uso de este formato estaba universalmente extendido con el auge de JavaScript, pensar que las colecciones de documentos de MongoDB eran arrays de objetos en formato JSON facilitaba trabajar con ellas. No obstante, también llevaba a equívocos. Es un caso similar a lo que ocurría años atrás con el formato XML. En algunos modelos de datos se almacenaban datos en formato XML en columnas de tipo texto plano. El inconveniente es que todo el procesamiento de dichas columnas había que hacerlo manualmente, ya fuese en el servidor de aplicaciones tras recuperarlas de base de datos, o en procedimientos PL/SQL en el propio servidor de base datos. Algunas bases de datos relacionales reaccionaron llevando a cabo desarrollos específicos para que las columnas de las tablas pudieran almacenar y explotar información directamente en formato XML, pero al no ser parte de ningún estándar implicaba ligarse a un determinado proveedor. Con el formato JSON ocurre algo similar, algunas base de datos permiten almacenar información en formato JSON, pero en muchos casos no dejan de ser columnas en formato de texto plano optimizadas para eliminar espacios en blancos y claves duplicadas de los objetos JSON almacenados en ellas.

MongoDB no almacena los objetos en formato JSON, lo hace en un formato binario llamado BSON. Y gracias a esto MongoDB puede realizar operaciones de una forma eficiente sobre cualquier campo, de igual forma que una base de datos relacional opera con el valor de una columna de una tabla.

Joins

El gran caballo de batalla.

Tener la información totalmente estructurada pasa por tenerla totalmente normalizada. Cada cosa en su sitio, y un sitio para cada cosa. Por ejemplo, el registro de los clientes en una tienda web puede requerir una serie de campos básicos como nombre y apellidos, pero también formas de contacto como teléfonos y direcciones, que pueden ser de varios tipos, como dirección de envío o dirección de facturación, y a su vez las direcciones pueden hacer referencia a países, o localidades más concretas dentro de un país. Es decir, es posible identificar un conjunto de entidades perfectamente delimitadas con los atributos que le son propios, así como sus relaciones con otras entidades.

Sin embargo, cuanto más estructurada esté la  información, más esparcida estará por el modelo. Insertar, recuperar, actualizar o borrar una entidad completa puede suponer tener que acceder a una gran cantidad de entidades relacionadas con ella. Lógicamente, hacer una consulta por cada entidad sería muy ineficiente, por lo que el procedimiento habitual es realizar una única consulta que recupere todas las entidades de una sola vez a través de sus relaciones. Es decir, mediante lo que normalmente se denomina «hacer un join». Cuantas más relaciones existan, más joins será necesario realizar, más compleja y costosa será la consulta, y todo ello sin tener en cuenta la relaciones opcionales, es decir, lo que normalmente se denomina «hacer un left outer join». Otra solución es utilizar MongoDB.

En MongoDB las entidades se pueden almacenar de forma anidada unas dentro de otras. Es decir, un documento puede tener dentro otro documento, o conjunto de ellos. O dicho de otra forma, un objeto JSON puede tener un campo que a su vez sea otro objeto JSON, o un array de objetos JSON. Volviendo al ejemplo anterior, un documento de la colección «clientes» puede tener un atributo «direcciones» que sea un array. De esta forma, en una única operación, ya sea de lectura o escritura, se tiene acceso a la entidad completa. Esta es la diferencia fundamental entre una base de datos relacional y una base de datos orientada a documentos como es MongoDB.

No obstante, llegados a este punto, surgen de manera natural muchas dudas acerca de las entidades embebidas. Continuando con el ejemplo de los clientes, ¿hay que repetir el nombre de un mismo país en todas las direcciones de todos los clientes de dicho país?, ¿no se puede tener una colección de países y que las direcciones hagan referencia a ella?, ¿cómo se garantiza la integridad referencial?, … Resumiendo: ¿cómo se crean «foreign keys» y se hacen «joins» en MongoDB?

Vayamos por partes.

Cada documento en MongoDB tiene un campo de nombre «_id» que contiene un valor que identifica dicho documento de manera unívoca. Es decir, es equivalente a una «primary key» en una base de datos relacional. Por tanto, para crear una «foreign key» entre un documento A (dirección) y un documento B (país) hay que añadir un campo en el documento A (dirección) que contenga el valor del identificador de B (país). Pero teniendo siempre presente que MongoDB no garantiza la integridad referencial. Es responsabilidad de los desarrolladores. MongoDB no es una base de datos relacional, es orientada a documentos.

Por otra parte, comentar que inicialmente MongoDB no permitía recuperar varios documentos relacionados de una sola vez. Si todos los documentos tenían todos sus atributos entonces no era necesario realizar más consultas, dicha opción no tenía sentido. Si se quería obtener un documento relacionado había que hacer una segunda consulta. No obstante, con el tiempo, MongoDB añadió esta característica. Es decir, a día de hoy, cuando se hace una consulta en MongoDB, se puede hacer un «join» entre dos colecciones indicando en una cláusula el nombre de las colecciones y campos sobre los que realizar el «join».

Pero a pesar de contar con todas estas opciones, los creadores de MongoDB defienden un modelo mixto. No ven mal el uso de «foreign keys» y «joins», pero recomiendan copiar los atributos más habitualmente usados de los documentos referenciados en los documentos que contienen la referencia. Lo que aplicado al ejemplo de los clientes de una tienda web quiere decir que una dirección debería tener un campo con una referencia al país, pero también debería tener un campo con el nombre del país si el caso de uso más habitual es imprimir el nombre del país junto a la dirección. La redundancia de datos la justifican aduciendo que hoy en día el almacenamiento es económico y los campos habitualmente candidatos a ser redundados no suelen cambiar nunca con el tiempo.

Índices

Hubo un tiempo en que toda la información se presentaba en registros en un formato tabular. Cada valor en un columna distinta. Cada columna con un nombre. Y junto a cada nombre un botón para ordenar ascendente o descendentemente. Era un desastre.

Las base de datos son buenas almacenando información y recuperándola por clave primaria, pero penalizan mucho el rendimiento cuando se realizan consultas por cualquier campo de manera indiscriminada, sobre todo cuando el número de registros es elevado. El problema es que se les obliga a recorrer todos los registros para comprobar cuales cumplen los criterios de filtrado u ordenación de la consulta. Por ello dejar que cualquier tabla se ordenase por cualquier columna era una receta para el desastre.

La solución a este tipo de problema es obviamente cambiar la forma de presentar la información, restringir las opciones, y crear índices cuando sea preciso. Cuando se diseña un modelo de datos es importante identificar las consultas que se realizarán y planificar los índices adecuados sobre las entidades correspondientes.

MongoDB en este apartado no se diferencia del resto de base de datos. Permite crear índices sobre los campos de los documentos para agilizar las consultas. De hecho, hacer un «join» en MongoDB es inviable sin definir un índice en la colección a la que se referencia. Y penaliza de igual forma que cualquier otra base datos, con una mayor necesidad de espacio y con un mayor tiempo al insertar o actualizar.

Fragmentación

Los gestores de base de datos relacionales reservan un espacio determinado para las filas de una tabla. Espacio que normalmente se llama bloque y coincide en tamaño con el número de bytes que pueden recuperar de disco en un sólo acceso de forma óptima. Si se inserta un registro con muchas columnas a nulo queda mucho espacio que pueden aprovechar otras filas. Si posteriormente se actualiza la fila cambiando sus nulos por valores no nulos entonces la fila reclama su espacio dentro del bloque. Si no hay espacio disponible dentro del bloque la fila se reparte entre dos bloques. Acceder a un bloque implica leer de disco duro, por lo que cuanto más bloques se requiera para una fila más lento será el acceso.

MongoDB gestiona el espacio de una forma similar. Insertar un documento dentro de una colección requiere tanto espacio como campos con valores no nulos tenga. Si posteriormente se actualiza el documento con nuevos campos no nulos reclamará su espacio. Si no hay espacio disponible el documento quedará fragmentado.

Por mi experiencia, este detalle del funcionamiento interno de los gestores de base de datos no se suele tener en cuenta, pero puede afectar al rendimiento de una aplicación. La situación ideal es tener entidades que no contengan valores nulos y que una vez insertadas no se modifican. Parece un escenario utópico, pero debería ser considerado parte de los objetivos de diseño más a menudo.

Si se da la circunstancia, es bueno saber que MongoDB ofrece comandos para desfragmentar las colecciones de igual forma que las base de datos relacionales, ya que puede afectar significativamente al rendimiento según el caso de uso.

Agregaciones

Hay una tendencia a evaluar una base de datos pensado en su rendimiento de cara a proporcionar información en tiempo real a una interface de usuario. No obstante, hay casos de uso que pueden resolverse mediante ejecuciones de procesos desatendidos donde el tiempo de respuesta no es un factor crítico. En esos casos la facilidad para manipular la información debería ser el factor más importante a tener en cuenta.

Utilizar sentencias SQL para generar informes puede que no sea la mejor solución, ya que si bien es cierto que permiten extraer registros de una forma sencilla, también lo es que carecen de capacidades avanzadas para agregar y manipular dicha información de una forma sencilla. Otra solución es utilizar MongoDB.

MongoDB proporciona un API exclusivo para realizar agregaciones. Es decir, para recolectar información existente en las colecciones y generar nueva información a partir de ellas. El API es bastante versátil y permite definir una secuencia de operaciones a realizar. Las operaciones generan resultados o modifican el resultado de las operaciones anteriores. En esencia es como programar pequeños fragmentos de código, añadirlos a una lista y pedir al servidor que los ejecute sobre una colección.

Conclusiones

Desde el punto de vista de un desarrollador puro y duro, hoy en día no hay muchas diferencias entre MongoDB y una base de datos relacional. Hay «primary keys», «foreign keys», «joins», índices, agrupaciones, …

Si el modelo de dominio del negocio se adecua a las características de un modelo orientado a documentos entonces se puede sacar partido del hecho de poder almacenar y recuperar toda una entidad de una sola vez. Y si no se adecua a dicho modelo entonces siempre se puede realizar un modelado de forma similar al que se haría en una base de datos relacional.

No obstante, si la única ventaja de MongoDB fuera que permite trabajar con las entidades de forma completa como objetos en formato JSON entonces habría que considerar otras soluciones. Por ejemplo, la exitosa versión 10 de PostgreSQL permite definir columnas de tipo JSONB, que es una representación de JSON en formato binario, para poder operar sobre los campos de un JSON de forma eficiente, e incluso definir índices sobre los mismos para agilizar las consultas como MongoDB.

Por su parte, la falta de integridad referencial no debería ser un factor crítico a tener en cuenta. No recuerdo haber tenido que resolver nunca un problema en producción por culpa de una «foreign key». Ese tipo de error se detecta normalmente de forma muy temprana durante el propio desarrollo.

Otro punto importante es el rendimiento, pero los benchmarks son muy difíciles de hacer y muy fáciles de malinterpretar. Una batería de pruebas de una base de datos no puede servir para predecir como se comportarán todas las aplicaciones en producción que utilicen dicha base de datos. No importa que la base de datos sea extremadamente rápida realizando escrituras cuando el caso de uso típico de una aplicación son las lecturas. Y de igual forma, no importa que la base de datos sea capaz de realizar miles de lecturas por segundo cuando el caso de uso típico de la aplicación es de decenas de lecturas por minuto.

Hace unos pocos años hubo una presentación de un caso de éxito de MongoDB. El rendimiento era espectacular. Hasta que dijeron las especificaciones del CPD. Era tan impresionantes que es bastante posible que el sistema hubiera rendido igual de bien con una base de datos relacional.

¿Entonces?

En mi opinión, el factor clave no es el conjunto de características que MongoDB ofrece a los desarrolladores, ni que el setup sea simple, ni que existan drivers para los principales lenguajes de programación e integración con los frameworks más populares. El factor clave es la escalabilidad horizontal. MongoDB fue diseñada desde el principio con ese objetivo específico en mente. Hoy funciona para cien, mañana funciona para un millón. Las bases de datos relaciones tradicionales no escalan de igual forma que MongoDB, ni que la mayoría de las bases de datos NoSQL en general. Estas últimas han sido creadas en un momento muy distinto, con unas coyunturas y necesidades totalmente distintas.

MongoDB es una alternativa a considerar cuando se quiere un desarrollo más dinámico, donde se pueda ir construyendo y ampliando. Y sobre todo cuando se prevea un crecimiento exponencial. Las bases de datos relaciones tradicionales no suelen tener mecanismos de escalado de esas dimensiones, y en caso de ofertar algo similar es normalmente mediante soluciones parciales, como el particionado de tablas, o con sofware adicional con licencias propietarias o productos de terceros.

Dart 2.0.0 (y 2)

Después de comentar en el artículo anterior algunos de los hitos importantes en la historia de Dart, junto con las características y herramientas básicas del lenguaje, ahora toca repasar algunas de las novedades específicas más relevantes de la recién liberada versión 2.0.0.

  • El sistema de tipos ahora es más estricto que en la versión anterior. Utilizando una combinación de comprobaciones realizadas tanto en tiempo de compilación como en tiempo de ejecución se garantiza que el valor asignado a una variable o parámetro es del tipo declarado para los mismos, elevándose un error en caso contrario. Con este cambio Dart se aleja de su sistema de tipos inicial, más parecido al de JavaScript, que realiza conversiones de los valores de un tipo a otro de manera automática y sin indicación explícita por parte de los desarrolladores. El nuevo sistema de tipos permite desarrollar sistemas con comportamientos más predecibles y facilita la detección temprana de errores.
  • El tipo entero int tiene ahora un tamaño fijo predefinido de 64 bits. En la versión anterior el tipo no tenía un tamaño definido y las variables enteras podían tener cualquier valor, Dart reservaba más memoria para ellas a medida que su valor crecía. Ahora los enteros son de tamaño fijo y la funcionalidad anterior está en la clase BigInt. Tipo este último que se ha añadido de forma nativa a JavaScript hace relativamente poco tiempo para representar enteros con una precisión de tamaño arbitrario.
  • Se ha eliminado la necesidad de utilizar new y const para instanciar objetos. Su uso está marcado como deprecado, pero se sigue soportando. Este cambio equipara los constructores con las factorías, haciéndolos patrones intercambiables e indistinguibles a la hora de invocarlos. Además tiene la ventaja de que no es necesario utilizar const de forma repetitiva cuando en una sentencia se construye un objeto constante por composición de otros objetos constantes.
  • Las funciones marcadas con async se ejecutan ahora de forma síncrona hasta alcanzar la primera llamada a await dentro del cuerpo de dichas funciones. En la versión anterior la ejecución de las funciones asíncronas se realizaba en el siguiente ciclo de ejecución del event loop de Dart empezando desde el principio del cuerpo de dichas funciones. Con este cambio el comportamiento es más acorde a lo que la intuición sugiere y no se presupone la necesidad de conocer el detalle de funcionamiento interno de la máquina virtual para programar correctamente una función de manera asíncrona.
  • Las directivas part of ahora pueden utilizarse con una cadena de texto con la ruta de un fichero en vez del nombre de una librería. Esto permite crear librerías anónimas y facilita organizar los proyectos de forma interna con una mayor granularidad. La sintaxis original ha sido marcada como deprecada.
  • El tipo void se ha promocionado y ahora puede utilizarse como tipo parametrizado dentro de expresiones genéricas. Es decir, ahora por ejemplo puede escribirse Future<void>. Sin embargo, no puede utilizarse dentro de todas las expresiones, por lo que T is void, T == void o identical(T, void) no son expresiones válidas en tiempo de compilación. Mi impresión general es que este cambio se ha publicado sin estar completamente definido, y en consecuencia implementado y probado. Y si ese es el comportamiento definido por diseño entonces el cambio se me antoja un tanto extraño.
  • Cuando se invoca un método que no se encuentra implementado, como puede llegar a ocurrir en el caso de métodos de clases abstractas, Dart invoca a un método con un nombre prefijado llamado noSuchMethod. En la versión anterior de Dart el método no recibía ningún parámetro opcional de la función original no implementada. En la nueva versión recibe los parámetros opcionales con sus valores por defecto.
  • Se han dejado de soportar los paquetes dart:isolate  y dart:mirrors para la parte cliente web, aunque aún se siguen soportando para la parte servidora ejecutándose sobre la máquina virtual en línea de comandos. Con respecto a dart:isolate, se recomienda el uso de webworkers para ejecutar tareas en segundo plano en el navegador. Y con respecto a dart:mirrors, el mecanismo de reflexión siempre ha sido una patata caliente en Dart, y parece que con esta versión va a seguir siéndolo. Habrá que esperar a la siguiente.
  • Los nombres de constantes de todas las librerías se han renombrado para utilizar minúsculas en vez de mayúsculas. Escribir las constantes utilizando mayúsculas es una vieja práctica que se sigue utilizando dentro las guías de estilo de la mayoría de lenguajes de programación. El cambio es una propuesta de hace un tiempo y que se ha materializado en esta nueva versión de Dart. Su intención es que si una variable deja de ser una constante no debería implicar reescribir todo el código que hace referencia a ella.
  • Las librerías internas del core del SDK han sufrido muchos cambios. Se han añadido y modificado clases y métodos. Enumero a continuación algunos de estos cambios más representativos. En el paquete dart:async, a la clase Stream se le han añadido los métodos cast y castFrom para realizar comprobaciones en tiempo de ejecución de conversiones de objetos de un tipo a otro. En el paquete dart:collection, a la mayoría de las clases se les ha añadido el constructor of para facilitar la instanciación de colecciones a partir de iterables. En el paquete dart:convert, la mayoría de conversores retorna ahora Uint8List en vez de List<int>. En el paquete dart:core, a los iterables se les ha añadido el método whereType para facilitar el filtrado de secuencias heterogéneas de objetos. Y en el paquete dart:typed_data, se ha añadido una clase de vista inmutable para todos los tipos de listas existentes.
  • El cambio más relevante del formateador de código es que ahora formatea las expresiones interpoladas dentro de las cadenas de texto. Y que se le ha incorporado el parámetro –fix en línea de comandos para ayudar con la migración del código existente a la nueva versión de Dart. El nuevo parámetro hace que el formateador elimine automáticamente new y const al instanciar objetos, y sustituye : por = al asignar valores por defecto a los parámetros opcionales.
  • El analizador de código ahora sólo funciona en modo estricto y trata el directorio packages como cualquier otro directorio ordinario. Estas dos modificaciones reflejan dos cambios importantes realizados en la nueva versión de Dart. El cambio referido al modo estricto se refiere al nuevo sistema de tipos, y el cambio referido al directorio packages se refiere a que en la versión anterior el directorio era en realidad un enlace simbólico que se utilizaba internamente para la resolución de paquetes y requería un tratamiento especial.
  • El gestor de dependencias se ha reescrito y ahora dispone de un sistema de construcción y publicación más flexible que los antiguos transformers, que han dejado de ser soportados. Además, ahora al publicar un paquete se obliga a declarar la versión máxima de SDK soportada. A los paquetes existentes que no tienen declarada ninguna versión máxima se les aplica el valor <2.0.0 por defecto.
  • El compilador cruzado de Dart a JavaScript ahora genera código para la versión 2 de Dart, lo que implica que el código generado tiene un tamaño mayor que en la versión anterior debido al mayor número de comprobaciones que se realizan en el sistema de tipos. Para tratar de aliviar este efecto se le ha añadido el parámetro –omit-implicit-checks al compilador con objeto de eliminar dichas comprobaciones del código generado para los casos en que no consideren realmente necesarias en tiempo de ejecución. Otro cambio significativo es que la clase Promise de JavaScript ahora se expone con un mecanismo compatible con la clase Future de Dart.
  • Dartium, la versión de Chrome que incluía la máquina virtual de Dart embebida, ha dejado de desarrollarse y dársele soporte.

Además de todos estos puntos comentados, se han corregido muchos casos de uso poco habituales del sistema de tipos y de la ejecución asíncrona de métodos. La release notes correspondiente contiene un listado muy detallado y su lectura es obligada para obtener una visión más completa de todas las modificaciones introducidas.