MEAN: MongoDB

Continuando con la serie de artículos relacionados con el stack MEAN (MongoDB, ExpressJS, AngularJS y NodeJS), y tras haber explicado lo que es JSON, paso a explicar los por menores del sistema gestor de bases de datos MongoDB, no sin antes introducir el concepto de NoSQL.

Tabla de Contenidos

NoSQL

En el lado del almacenamiento se han utilizado tradicionalmente bases de datos relacionales. Sin embargo, actualmente, los tipos de información que suelen requerir las aplicaciones web demandan mayor flexibilidad, menos coherencia y sobre todo mayor capacidad de escalar. Para dar respuesta a todo esto surge la tendencia tecnológica en almacenes de datos que se denomina NoSQL. Aunque el nombre pueda llevar a confusiones, su significado real es “no solo SQL” (Not Only SQL), pues su característica principal es que no utilizan SQL como lenguaje principal para las consultas.

En una base de datos NoSQL los datos almacenados no requieren de estructuras fijas, por lo general tampoco soportan operaciones JOIN y no cumplen completamente las formas normales que garantizan atomicidad, consistencia, aislamiento y durabilidad.

Esta ruptura con las formas tradicionales nace de las nuevas necesidades en el tratamiento de datos. Con el crecimiento de la red y con ello el crecimiento exponencial de la cantidad de datos, se hacía necesario una nueva forma de procesar grandes volúmenes de datos con un mayor rendimiento y que permitiera un procesamiento en tiempo real más eficiente. Estos requerimientos se hicieron mucho más importantes que la coherencia de datos, que eran la principal característica de los sistemas de base de datos clásicos. De esta forma, los sistemas NoSQL basan su optimización en las operaciones de agregar y recuperar datos más allá de la de mantener la consistencia de la información almacenada.

A pesar de que los sistemas NoSQL ofrecen grandes ventajas para algunas estructuras de datos no hay que tomar esta nueva tecnología como un estándar para todos los sistemas que se vayan a desarrollar en el futuro. Los sistemas de base de datos relacionales siguen cumpliendo su papel, y lo seguirán haciendo en la mayoría de los sistemas en los que se utiliza. Es por ello que antes de tomar la decisión de si utilizar un sistema tradicional o un NoSQL habrá que realizar una tarea de análisis para determinar cuál es el mejor gestor de base de datos a utilizar dependiendo de las características del sistema que vayamos a desarrollar.

Almacenamiento de datos NoSQL

Los almacenes de datos NoSQL pueden ser de diversos tipos. Dependiendo de cómo almacenen la información y manejen los datos se clasificarán dentro de uno de los siguientes grupos:

  • Documentales
  • De Grafo
  • Clave-Valor
  • Multivalor
  • Orientados a Objetos
  • Tabulares

MongoDB

De entre todos los almacenes de datos no-relacionales hay uno que destaca especialmente: MongoDB, que es la «M» del stack MEAN.

En la clasificación de bases de datos NoSQL podríamos clasificar a MongoDB entre las bases de datos documentales y de clave valor, ya que en el tratamiento de los datos realiza una mezcla de ambos sistemas. Puesto que está orientado a documentos, lo que se almacena en una base de datos de tipo MongoDB es, valga la redundancia, documentos en BSON, que no es más que una implementación binaria del formato JSON, por lo que todos los datos almacenados en la base de datos se podrían tratar de una forma similar a como se hacen en JavaScript, que será el lenguaje que utilizará estos datos.

Los documentos se almacenan en colecciones, que es lo más parecido a las tablas de los sistemas de bases de datos tradicionales. Sin embargo, a diferencia de las tablas, los elementos de una colección no tienen porqué tener los mismos campos. En el Fragmento de Código 1 podemos ver un ejemplo de colección válida almacenada en BSON con mongoDB. Hay que tener en cuenta que mongoDB añade un campo adicional a todos los documentos de una colección. Este campo se denomina “_id” y es un valor numérico autoincremental de carácter único que sirve de identificador.

[
    { 
        "Nombre": "Miguel", 
        "Apellidos": "S. Mendoza", 
        "Correo":"miguel@smendoza.net"
    },
    {
        "Nombre": "Antonio", 
        "Teléfono": "987998877", 
        "DNI": "47089987M"
    }
    
]

Cómo puede verse, no existe un esquema definido, por lo que habrá que plantear la forma de almacenar los datos de una manera diferente a cómo se hace con las bases de datos relacionales, pues, como puede deducirse, no solo carecemos de esquemas definidos, sino también de relaciones.

Almacenamiento de Datos

Si quisiéramos almacenar los datos de una tienda online en la que existen clientes y pedidos podríamos diseñarlo de las siguientes formas, cada una con sus ventajas y desventajas:

Una única colección

Podemos almacenar en una misma colección los datos de los clientes, e incluirles un campo pedidos en el que almacenaríamos una lista con todos los pedidos del cliente. En el Fragmento de Código 2 podemos ver cómo quedaría en la base de datos.

[
  { 
    "_id" : 123, 
    "Nombre" : "Antonio Córcoles", 
    "Dirección" : "Calle Falsa 123", 
    "Pedidos" : [ 
      { 
        "id_pedido" : 1, 
        "Productos" : [
          { 
            "id_producto" : 1, 
            "Nombre" : "Piruleta", 
            "Cantidad":1 
          }, 
          { 
            "id_producto" : 2, 
            "Nombre" : "Chicle", 
            "Cantidad" : 1 
          } 
        ]
      }, 
      { 
        "id_pedido" : 2, 
        "Productos" : [
          { 
            "id_producto" : 77, 
            "Nombre" : "Gominola", 
            "Cantidad" : 3 
          }
        ] 
      }
    ] 
  } 
]

Almacenando los datos de esta forma conseguiremos optimizar las consultas de pedidos de cada cliente, sin embargo, habrá que tener en cuenta, que cuantos más pedidos haga un cliente, mayor crecerá el tamaño de éste, y cada vez que hagamos una consulta de un cliente todos esos datos serán manejados. A largo plazo no parece una buena solución dependiendo del sistema que se vaya a desarrollar.

Dos colecciones

Podemos optar por un diseño de base de datos similar al que utilizaríamos en una base de datos relacional, esto es, utilizando una colección para clientes, y otra para pedidos. En el Fragmento de Código 3 se muestra un ejemplo de cómo se almacenarían los datos en este caso.

Colección Clientes:
[
  { 
    "_id" : 123, 
    "Nombre" : "Antonio Córcoles", 
    "Dirección" : "Calle Falsa 123"
  }
]
Colección Pedidos:
[ 
  { 
    "_id " : 1,
    "id_cliente" : 123,
    "Productos" : [
      { 
        "id_producto" : 1, 
        "Nombre" : "Piruleta", 
        "Cantidad":1 
      }
    ]
  }, 
  { 
    "_id " : 2, 
    "id_cliente" : 123,
    "Productos" : [
      { 
        "id_producto" : 77, 
        "Nombre" : "Gominola", 
        "Cantidad" : 3 
      }
    ] 
  }
]

Aunque el uso de esta estructura nos pueda parecer más familiar y coherente, debemos tener en cuenta que al no tratarse de un sistema de base de datos relacional, para obtener el conjunto de pedidos de un cliente, anteriormente hemos de obtener el cliente para poder conocer el campo de identificación que lo relaciona con cada pedido, por lo que deberíamos realizar dos peticiones en lugar de una.

Dos colecciones con datos duplicados

Dependiendo del sistema podríamos optar por un diseño híbrido de las dos alternativas anteriores. Supongamos que necesitáramos consultar de forma recurrente el último pedido de un cliente. En este caso sería conveniente almacenar ese pedido en el mismo documento del cliente, además de en la tabla general de pedidos. De esa forma ahorraríamos un gran número de peticiones habiendo tenido en cuenta esa consulta recurrente, sacrificando por otro lado el inconveniente de duplicar la información. En el Fragmento de Código 4 podemos ver un ejemplo de cómo se almacenarían los datos en este caso.

Colección Clientes:
[
  { 
    "_id" : 123, 
    "Nombre" : "Antonio Córcoles", 
    "Dirección" : "Calle Falsa 123",
    "UltimoPedido" : { 
      "id_pedido" : 1,
      "id_cliente" : 123,
      "Productos" : [
        { 
          "id_producto" : 1, 
          "Nombre" : "Piruleta", 
          "Cantidad":1 
        }, 
        { 
          "id_producto" : 2, 
          "Nombre" : "Chicle", 
          "Cantidad" : 1 
        } 
      ]
    }
  }
]
Colección Pedidos:
Similar al esquema “Dos Colecciones”

En definitiva, como podemos observar el sistema es muy flexible, y nos permite diseñar siempre la opción más óptima dependiendo del sistema, haciendo en este caso que el sistema de base de datos se adapte a las necesidades del mismo, en lugar de ser el sistema el que se tenga que adaptar a las características de un sistema gestor de base de datos concreto.

Es importante destacar, que escojamos la opción de diseño que escojamos, podremos cambiarla en cualquier momento sin que esto afecte a los datos ya almacenados, lo que supone una gran ventaja a la hora de escoger este sistema.

Operaciones de Consulta

Mientras que en los sistemas tradicionales podíamos utilizar SQL como lenguaje de consulta, en MongoDB no tenemos ese privilegio. En este sistema todas las consultas se realizan mediante JavaScript. Por tanto, en MongoDB no existen cadenas de consulta, si no que existen funciones que aceptan parámetros específicos para realizar las consultas. Las funciones de consulta más utilizadas son find, findOne y findAndModify. En los siguientes apartados veremos un ejemplo de uso de las mismas. Para poder entenderlos debemos definir algunos objetos JavaScript que se utilizarán:

  • db: Objeto de conexión de base de datos. Dependiendo del sistema a utilizar (Express, C#, etc.) se creará de una forma u otra. Todas las peticiones parten de este objeto. Dependiendo del sistema accederemos a las colecciones de una forma u otra, pero en la mayoría de los casos el esquema es muy similar al que veremos a continuación.
  • Colección usuarios: Definimos una colección a la que llamaremos “usuarios” con la estructura del Fragmento de Código 5.
[
  {
    "_id" : 1,
    "name" : "Miguel S. Mendoza", 
    "mail" : "miguel@smendoza.net",
    "age" : 32,
    "token" : "8f1334b2e6f14169a6e690aeba47ca21"
  },
  {
    "_id" : 2,
    "name" : "Catalina Sánchez Román", 
    "mail" : "catalina@smendoza.net",
    "age" : 32,
    "token" : "413fb29052e649de94b6808923fdfd0a"
  }
]

find

Este método es el que nos permite buscar elementos en una colección. Si no le pasamos ningún parámetro nos devolverá todos los elementos de la misma.

db.usuarios.find();

Sin embargo, hay que tener en cuenta que este método no nos devolverá de forma automática todos los elementos de una colección, si no que nos devolverá una especie de puntero a los datos solicitados que podremos recorrer de forma sencilla. A este objeto se le denomina cursor.

Cursor

En mongoDB un cursor es una conexión directa con la base de datos que permanece abierta mientras la necesitamos, permitiéndonos iterar sobre los resultados de una consulta concreta. Como ejemplo, para recorrer todos los objetos de una colección podríamos hacer algo parecido a lo que nos encontramos en el Fragmento de Código 6.

var usuariosCursor = db.usuarios.find();

while (usuariosCursor.hasNext()) {
	var usuario = usuariosCursor.next();
	var token = usuario.token;
}

Los cursores cuentan con diferentes métodos adicionales que nos pueden servir para diferentes finalidades. Entre ellos destaca la función limit, que acepta como parámetro un número y nos permite limitar el número de resultados, y sort, que nos permite ordenar los resultados conforme a parámetros específicos. La función sort recibe como parámetro un objeto JSON con los campos por los que se deberá ordenar, cada uno con dos posibles valores: 1 en caso de que queramos que se ordene de forma ascendente por ese campo, o -1 en caso contrario. En el Fragmento de Código 7 podemos ver un ejemplo completo de los métodos limit y sort.

db.usuarios.find().limit(2).sort({ "age" : 1, "name": -1 });

El resultado serán los dos primeros objetos de la colección ordenados de forma ascendente por el campo age, y de forma descendente por el campo name.

Filtros

Para poder filtrar los resultados de una consulta debemos pasar como parámetro al método find un documento JSON con las características que deben cumplir los objetos a encontrar. Este procedimiento sería similar a la cláusula WHERE de las sentencias SQL. En el Fragmento de Código 8 podemos ver un ejemplo de uso de un filtro simple que nos devolvería un cursor con el usuario con edad 32 y nombre Catalina Sánchez Román.

db.usuarios.find({ "age" : 32, "name" : "Catalina Sánchez Román" });

Existen diferentes operadores que nos permiten modificar estos filtros para optimizar las búsquedas de forma sencilla.

Proyecciones

Hasta ahora la función find está devolviendo documentos completos de una colección. En SQL sería el equivalente a realizar consultas comenzando con SELECT *. Sin embargo, al igual que con SQL, podemos filtrar los resultados para obtener solo los campos que necesitemos. Para ello haremos uso de las proyecciones, que se pasarán en el segundo parámetro de la función find. Una proyección es un objeto JSON que contiene como pares clave/valor los nombres de los campos a obtener con valores 0 o 1. Utilizaremos el valor 1 en caso de que queramos que ese valor se obtenga, o un 0 en caso contrario. Por defecto, en caso de que pasemos una proyección como parámetro, todos los valores que no definamos de forma explícita tendrán el valor 0, excepto el campo _id, el cual, si no queremos que se encuentre entre los campos devueltos habrá que especificar claramente que debe ser así, como podemos observar en el Fragmento de Código 9, el cual, nos devolverá tan solo los campos name de la colección.

db.usuarios.find({ "age" : 32 }, { "name": 1, "_id" : 0});

findOne

Este método es parecido al método find, pero en lugar de obtener un cursor, obtendremos un único documento en forma de objeto JavaScript. Si la consulta de búsqueda contiene más de un objeto que cumpla con las condiciones que hayamos establecido, la función findOne nos devolverá el primero. En el Fragmento de Código 10 tenemos un ejemplo de uso con filtros y proyecciones.

var usuario = db.usuarios.findOne({ "_id" : 2 }, { "mail" : 1 });
var mail = usuario.mail;

findAndModify

En ocasiones se hace necesario modificar un documento y obtener el resultado de la modificación de inmediato. De esta forma certificamos que la modificación se ha hecho correctamente y podemos seguir operando con el objeto en cuestión manteniendo de esa forma cierta consistencia. En este caso, los parámetros se pasarán como un objeto JSON con los siguientes campos:

  • query: Un objeto JSON con los criterios de búsqueda, similar a los filtros de los métodos find y findOne.
  • sort: Un objeto JSON con los criterios de ordenación similar a los parámetros del método sort de los cursores.
  • remove: Campo booleano. Si el valor es true, se borrará el documento encontrado. No es necesario incluirlo si se incluye el campo update.
  • update: Un objeto JSON con los campos a modificar y sus valores. Este campo no es necesario en caso de utilizar el campo remove.
  • new: Campo booleano. Si es true, el método findAndModify devuelve el objeto modificado, en caso contrario devuelve el original antes de ser modificado.
  • fields: Un objeto JSON con la proyección de los datos que queremos obtener.
  • upsert: Campo booleano. Si es true y la consulta no devuelve ningún documento creará uno nuevo con los campos del parámetro update. Si es false no ser hará nada.

En el Fragmento de Código 11 podemos ver una forma de modificar el primer documento con el campo age igual a 32 ordenado de forma descendente por el name cambiándole el name y obteniendo el objeto ya modificado. Como puede deducirse, findAndModify es el método que nos sirve tanto para actualizar datos, como para eliminarlos.

var usuario = db.usuarios.findAndModify({
	query: { "age" : 32 },
	sort: { "name" : -1 },
	update: { "mail" : "misanmen@gmail.com" },
	new: true,
	fields: { "mail" : 1 }, 
	upsert: false
});
var mail = usuario.mail; // misanmen@gmail.com

Operaciones de Actualización de Datos

Todas las bases de datos son capaces de realizar operaciones CRUD (Create, Read, Update y Delete). En el apartado anterior hemos visto diferentes formas de realizar opciones de lectura, por lo que nos quedaría ver cómo se insertan y modifican datos en MongoDB.

Inserción

La inserción es uno de los comandos más simples de MongoDB, ya que tan solo hay que utilizar el método insert y pasarle como parámetro un objeto JSON. En el Fragmento de Código 12 podemos ver un ejemplo de inserción de un nuevo usuario en la colección usuarios.

db.usuarios.insert(
{
	"name" : "Pepe Pérez", 
	"mail" : "pepe@perez.net",
	"age" : 44,
	"token" : "46ac1443b4b7471cb737bc6cb6b9f9f1"
});

Como mencionamos anteriormente, MongoDB añade automáticamente un campo identificador denominado _id a todos los objetos que se insertan. Este campo se puede añadir en un objeto de inserción sin ningún problema, teniendo en cuenta que en caso de introducir un valor que coincida con alguno ya almacenado en la colección, MongoDB nos devolverá una excepción.

Existe otra función que nos permite introducir objetos en una base de datos pero que, en caso de añadir un campo _id que ya exista en la colección, en lugar de devolver una excepción, modificaría el objeto que coincidiera con esa misma _id introducida. Esta función se denomina save y se utiliza de la misma forma que se hace con insert.

Para añadir varios objetos a la vez debemos pasar como parámetro un array JSON con los objetos a añadir, como podemos observar en el Fragmento de Código 13.

db.usuarios.insert(
[
	{
		"name" : "Pepe Pérez", 
		"mail" : "pepe@perez.net",
		"age" : 44,
		"token" : "46ac1443b4b7471cb737bc6cb6b9f9f1"
	},
	{
		"_id" : 3,
		"name" : "Antonio Andújar", 
		"mail" : "antonio@andujar.net",
		"age" : 23,
		"token" : "32586fd5c3f74aca92ee98c183989d30"
	}
]);

Borrado

Para eliminar documentos de una colección utilizaremos el método remove, que recibe como parámetro un objeto JSON con los criterios de selección de los elementos a borrar como podemos ver en el Fragmento de Código 14, dónde se eliminarían todos los objetos de la colección cuyo campo age sea igual a 32 y cuyo campo mail sea “miguel@smendoza.net”. En caso de no pasarle ningún parámetro se borraría la colección completa.

db.usuarios.remove({ age:32, mail:'miguel@smendoza.net'});

Actualización

En el apartado anterior sobre Operaciones de Consulta, vimos un ejemplo de cómo actualizar un documento haciendo uso de la función findAndModify. Sin embargo, existe otra forma más directa de actualizar un documento, y es haciendo uso de la función update. Esta función recibe como parámetros un objeto con los criterios de selección del elemento o elementos a modificar, y un objeto JSON con el nuevo objeto a sustituir. El Fragmento de Código 15 muestra un ejemplo de cómo sustituir todos los documentos cuyo campo age sea 32 por un objeto diferente, es decir, no se realizaría una actualización de los campos propiamente dichos dentro del objeto que coincida con los criterios, si no que el objeto sería sustituido por completo por el nuevo pasado por parámetro, sea como sea su estructura.

db.usuarios.update(
	{
		age:32
	}, 
	{
		"Nombre" : "Miguel",
		"Apellidos" : "S. Mendoza"
	});

Como puede observarse éste no es el comportamiento habitual de una operación de actualización, ya que estamos acostumbrados a utilizar este tipo de funciones para actualizar campos concretos de un objeto en una base de datos, en lugar de sustituirlos por completo. Para lograr un comportamiento similar al de las sentencias UPDATE en SQL deberemos utilizar un operador de MongoDB denominado $set. Éste nos permitirá actualizar campos concretos de un objeto que coincida con los criterios de búsqueda introducidos en el primer parámetro. En el Fragmento de Código 32 podemos ver como modificar el campo mail del documento con valor del campo _id igual a 1.

db.usuarios.update(
	{
		_id:1
	}, 
	{
		$set: {
			"mail" : "misanmen@gmail.com"
		}
	}
);

Instalación

Para instalar una base de datos mongoDB tan solo deberemos seguir los pasos que podremos encontrar en el siguiente enlace:

https://docs.mongodb.com/manual/installation/

La instalación es un proceso bastante sencillo, por lo que no nos detendremos en este momento a detallar los por menores de la misma, pero resulta de vital importancia entender que una vez instalado en nuestro sistema, podremos acceder a la base de datos mediante línea de comandos y ejecutar todos los comandos que veremos a continuación. En la mayoría de sistemas esto se consigue ejecutando el comando «mongo» en un terminal.

Recomiendo utilizar una interfaz de usuario de administración (Admin UI) para manejar los datos de forma más eficiente. Entre todas las posibilidades yo destacaría Robomongo, ya que su facilidad de uso y posibilidades superan con creces a las demás alternativas. En cuanto a interfaz de usuario basada en web recomiendo utilizar mongo-express. La utilización y posibilidades de estas herramientas escapan al contexto del artículo, por lo que os animo a investigar una vez hayáis entendido bien los conceptos.

Conclusión

Como hemos podido ver, MongoDB es un sistema gestor de base de datos muy flexible y sencillo de utilizar. Todas las sentencias que hemos visto podríamos utilizarlas tanto en el terminal de comandos del propio sistema de Mongo, como a través de cualquier framework compatible.

En los siguientes artículos, veremos como utilizar MongoDB desde NodeJS y como integrarlo en un entorno real.

También te podría gustar...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

1 respuesta

  1. 8 febrero, 2018

    […] vimos en la descripción de MongoDB, existe una pequeña diferencia a la hora de utilizar este sistema gestor de base de datos entre un […]