Skip to content

mongodb

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:

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:

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:

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:

Í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:

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:

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:

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.

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.