Saltar a contenido

13.- MongoDB

Propuesta didáctica

En la última unidad terminaremos de trabajar el siguiente resultado de aprendizaje (RA):

  • RA7: Gestiona la información almacenada en bases de datos no relacionales, evaluando y utilizando las posibilidades que proporciona el sistema gestor.

Criterios de evaluación

  • CE7c: Se han identificado los elementos utilizados en estas bases de datos.
  • CE7d: Se han identificado distintas formas de gestión de la información según el tipo de base de datos no relacionales.
  • CE7e: Se han utilizado las herramientas del sistema gestor para la gestión de la información almacenada.

Contenidos

Bases de datos no relacionales:

  • Elementos de las bases de datos no relacionales.
  • Sistemas gestores de bases de datos no relacionales.
  • Herramientas de los sistemas gestores de bases de datos no relacionales para la gestión de la información almacenada.

Cuestionario inicial

  1. ¿Qué es MongoDB y cuáles son sus principales características como base de datos NoSQL?
  2. ¿Cuáles son las ventajas y desventajas de utilizar MongoDB como sistema de almacenamiento en memoria frente a otros sistemas de almacenamiento persistente?
  3. ¿Cómo se diferencian las bases de datos documentales de las bases de datos relacionales tradicionales?
  4. ¿Qué formato utiliza MongoDB para almacenar los documentos internamente?
  5. ¿Qué comando se utiliza para insertar un documento en una colección en MongoDB?
  6. ¿Cómo se accede a una base de datos específica en el shell de MongoDB?
  7. ¿Qué operador se utiliza para seleccionar documentos que cumplen múltiples condiciones en una consulta?
  8. ¿Qué operador se emplea para realizar consultas sobre campos que son arrays?
  9. ¿Cómo se proyectan campos específicos en los resultados de una consulta?
  10. ¿Qué operador permite realizar comparaciones entre campos dentro de un mismo documento?
  11. ¿Qué función se utiliza para contar el número de documentos que cumplen una condición específica?
  12. ¿Qué operador se utiliza para actualizar múltiples documentos que cumplen una condición?
  13. ¿Cómo se realiza una búsqueda utilizando expresiones regulares en MongoDB?
  14. ¿Qué operador se utiliza para agregar un elemento a un array dentro de un documento?
  15. ¿Cómo se reemplaza completamente un documento en una colección?
  16. ¿Qué operador permite actualizar un elemento específico dentro de un array?
  17. ¿Cómo se eliminan documentos que cumplen una condición específica?

Programación de Aula (11h)

Esta unidad es la segunda del bloque de soluciones NoSQL, la cual se imparte a final de curso, con una duración estimada de 11 horas:

Sesión Contenidos Actividades Criterios trabajados
1 MongoDB AC1301 CE7c, CE7d, CE7e
2 Consultas sencillas AC1302 CE7c, CE7d, CE7e
3 Consultas avanzadas AC1303 CE7c, CE7d, CE7e
4 Consultas con arrays AC1305 CE7c, CE7d, CE7e
5 Modificaciones y borrados
6 Modelado documental
7 Consultas agregadas I
8 Consultas agregadas II
9 Supuesto I
10 Supuesto II
11 Supuesto III

MongoDB

Logo de MongoDB
Logo de MongoDB

MongoDB es una de las bases de datos NoSQL más conocidas y empleadas. Sigue un modelo de datos documental, donde los documentos se basan en el formato JSON.

MongoDB destaca porque:

  • Soporta esquemas dinámicos: diferentes documentos de una misma colección pueden tener atributos diferentes.
  • Aunque inicialmente tenía un soporte limitado de joins, desde la versión 5.2 se pueden realizar incluso entre colecciones particionadas.
  • Soporte de transacciones sólo a nivel de aplicación. Lo que en un SGBD relacional puede suponer múltiples operaciones, con MongoDB se puede hacer en una sola operación al insertar/actualizar todo un documento de una sola vez, pero si queremos crear una transacción entre dos documentos, la gestión la debe realizar el driver.

huMONGOus

Como curiosidad, su nombre viene de la palabra inglesa humongous, que significa gigantesco/enorme.

Hay una serie de conceptos que conviene conocer antes de entrar en detalle:

  • MongoDB tienen el mismo concepto de base de datos que un SGBD relacional. Dentro de una instancia de MongoDB podemos tener 0 o más bases de datos, actuando cada una como un contenedor de alto nivel.
  • Una base de datos tendrá 0 o más colecciones. Una colección es muy similar a lo que entendemos como tabla dentro de una base de datos relacional. MongoDB ofrece diferentes tipos de colecciones, desde las normales cuyo tamaño crece conforme lo hace el número de documentos, como las colecciones capped, las cuales tienen un tamaño predefinido y que pueden contener una cierta cantidad de información que se sustituirá por nueva cuando se llene.
  • Las colecciones contienen 0 o más documentos, por lo que es similar a una fila o registro de un tabla relacional.
  • Cada documento contiene 0 o más atributos, compuestos de parejas clave/valor. Cada uno de estos documentos no sigue ningún esquema, por lo que dos documentos de una misma colección pueden contener todos los atributos diferentes entre sí. Todo documento contiene un campo _id que hace la función de atributo identificador del documento.
Elementos de MongoDB
Elementos de MongoDB

Así pues, tenemos que una base de datos va a contener varias colecciones, donde cada colección contendrá un conjunto de documentos.

Además, MongoDB soporta índices, igual que cualquier RDMS, para acelerar la búsqueda de datos. Al realizar cualquier consulta, se devuelve un cursor, con el cual podemos realizar operaciones como contar, ordenar, limitar o saltar documentos.

BSON

Mediante JavaScript podemos crear objetos que se representan con JSON. Internamente, MongoDB almacena los documentos mediante BSON (Binary JSON). Podemos consultar la especificación en http://BSONSpec.org

Especificación BSON
Especificación BSON

BSON representa un superset de JSON ya que:

  • Permite almacenar datos en binario
  • Incluye un conjunto de tipos de datos no incluidos en JSON, como pueden ser ObjectId, Date o BinData.
  • Diseñado para ser más eficiente en espacio y realizar más búsquedas de forma más eficiente.

Podemos consultar todos los tipos que soporta un objeto BSON en http://docs.mongodb.org/manual/reference/bson-types/

Un ejemplo de un objeto BSON podría ser:

var yo = {
  nombre: "Aitor",
  apellidos: "Medrano",
  fnac: new Date("Oct 3, 1977"),
  hobbies: ["programación", "videojuegos", "baloncesto"],
  casado: true,
  hijos: 2,
  contacto: {
    bsky: "@aitormedrano.bsky.social",
    email: "a.medrano@edu.gva.es"
  },
  fechaCreacion: new Timestamp()
}

Los documentos BSON tienen las siguientes restricciones:

  • No pueden tener un tamaño superior a 16 MB.
  • El atributo _id queda reservado para la clave primaria.
  • Desde MongoDB 5.0 los nombres de los campos pueden empezar por $ y/o contener el ., aunque en la medida de lo posible, es recomendable evitar su uso.

Además, MongoDB:

  • No asegura que el orden de los campos se respete.
  • Es sensible a los tipos de los datos
  • Es sensible a las mayúsculas.

Por lo que estos documentos son distintos:

{"edad": "18"}
{"edad": 18}
{"Edad": 18}

Si queremos validar si un documento JSON es válido, podemos usar http://jsonlint.com/. Hemos de tener en cuenta que sólo valida JSON y no BSON, por tanto nos dará errores en los tipos de datos propios de BSON.

Puesta en marcha

En la actualidad, MongoDB se comercializa mediante tres productos:

  • Mongo Atlas, como plataforma cloud, con una opción gratuita mediante un clúster de 512MB.
  • MongoDB Community Edition, versión gratuita para trabajar on-premise, con versiones para Windows, MacOS y Linux.
  • MongoDB Enterprise Advanced, versión de pago con soporte, herramientas avanzadas de monitorización y seguridad, y administración automatizada.

En nuestro caso, por comodidad, trabajaremos con la versión cloud de Mongo Atlas. Los pasos necesarios para su creación, configuración y uso, así como la conexión mediante MongoDBCompass los puedes encontrar en la sección de Entorno

Uso del shell

Una vez ya tenemos en marcha MongoDB, llega el momento de conectarnos. Aunque utilizaremos MongoDBCompas, durante esta unidad vamos a practicar los diferentes comandos haciendo uso del cliente mongosh (en versiones anteriores el comando utilizado era mongo). Para ello, en la parte superior derecha, tenemos la opción de >_ Open MongoDB shell.

El shell de MongoDB utiliza JavaScript como lenguaje de interacción con la base de datos, y como buen shell, mediante la fecha hacia arriba visualizaremos el último comando.

Si queremos ver las bases de datos que existen ejecutaremos el comando show dbs:

Atlas atlas-pxc2m9-shard-0 [primary] pruebas> show dbs
iabd                416.00 KiB
pruebas             216.00 KiB
sample_airbnb        52.52 MiB
sample_analytics      8.93 MiB
sample_geospatial     1.29 MiB
sample_guides        40.00 KiB
sample_mflix         47.23 MiB
sample_restaurants    6.72 MiB
sample_supplies       1.05 MiB
sample_training      52.50 MiB
sample_weatherdata    3.32 MiB
admin                40.00 KiB
local                40.00 KiB

Uso externo

Si nos queremos conectar únicamente desde un terminal, podemos instalar únicamente el shell desde https://www.mongodb.com/try/download/shell

Hola MongoDB

Pues una vez que ya nos hemos conectado a MongoDB mediante mongosh, vamos a empezar a interactuar con los datos.

En cualquier momento podemos cambiar la base de datos activa mediante use nombreBaseDatos. Si la base de datos no existiese, MongoDB creará dicha base de datos. Esto es una verdad a medias, ya que la base de datos realmente se crea al insertar datos dentro de alguna colección.

Así pues, vamos a crear nuestra base de datos s8a:

use s8a

Una vez creada, podemos crear nuestra primera colección, que llamaremos personas, e insertaremos una persona con nuestros datos personales mediante el método insertOne, al que le pasamos un objeto JSON:

db.personas.insertOne({ nombre: "Aitor Medrano", edad: 47, profesion: "Profesor" })

Tipos de datos

Cuidado con los tipos, ya que no es lo mismo insertar un atributo con edad:45 (se considera el campo como entero) que con edad:"45", ya que considera el campo como texto.

Tras ejecutar el comando, veremos que nos devuelve un objeto JSON con su ACK y el identificador del documento insertado:

{
  acknowledged: true,
  insertedId: ObjectId('67fa9f27d152b48c68424418')
}

Una vez insertado el documento, sólo nos queda realizar una consulta para recuperar los datos y comprobar que todo funciona correctamente mediante el método findOne:

db.personas.findOne()

Lo que nos dará como resultado un objeto JSON que contiene un atributo _id con el mismo identificador mostrado anteriormente, además de los que le añadimos al insertar la persona:

{
  _id: ObjectId('67fa9f27d152b48c68424418'),
  nombre: 'Aitor Medrano',
  edad: 47,
  profesion: 'Profesor'
}

Como podemos observar, todas las instrucciones van a seguir el patrón de db.nombreColeccion.operacion().

ObjectId

En MongoDB, el atributo _id es único dentro de la colección, y hace la función de clave primaria. Se le asocia un ObjectId, el cual es un tipo BSON de 12 bytes que se crea mediante:

  • el timestamp actual (4 bytes)
  • un valor aleatorio y único por máquina y proceso (5 bytes)
  • un contador inicializado a número aleatorio (3 bytes).

Este objeto lo crea el driver y no MongoDB, por lo cual no deberemos considerar que siguen un orden concreto, ya que clientes diferentes pueden tener timestamps desincronizados. Lo que sí que podemos obtener a partir del ObjectId es la fecha de creación del documento, mediante el método getTimestamp() del atributo _id.

Obteniendo la fecha de creación de un documento

db.personas.findOne()._id
// ObjectId('67fa9f27d152b48c68424418')
db.personas.findOne()._id.getTimestamp()
// 2025-04-12T17:13:11.000Z

Este identificador es global, único e inmutable. Esto es, no habrá dos repetidos y una vez un documento tiene un _id, éste no se puede modificar.

Si en la definición del objeto a insertar no ponemos el atributo identificador, MongoDB creará uno de manera automática. Si lo ponemos nosotros de manera explícita, MongoDB no añadirá ningún ObjectId. Eso sí, debemos asegurarnos que sea único (podemos usar números, cadenas, etc…​).

Por lo tanto, podemos asignar un identificador al insertar:

db.personas.insertOne({_id:4, nombre:"Marina", edad:17 })
// { acknowledged: true, insertedIds: { '0': 4 } }

O también, si queremos podemos hacer que el _id de un documento sea un documento en sí, y no un entero, para ello, al insertarlo, podemos asignarle un objeto JSON al atributo identificador:

db.personas.insertOne({_id:{nombre:'Aitor', apellidos:'Medrano', bsky:'@aitormedrano.bsky.social'}, ciudad:'Elx'})
// {
//     acknowledged: true,
//     insertedId: {
//       nombre: 'Aitor',
//       apellidos: 'Medrano',
//       bsky: '@aitormedrano.bsky.social'
//     }
// }

CRUD

Antes de entrar en detalles en las instrucciones necesarias para realizar las operaciones CRUD, veamos algunos comandos que nos serán muy útiles al interactuar con el shell:

Comando Función
show dbs Muestra el nombre de las bases de datos
show collections Muestra el nombre de las colecciones
db Muestra el nombre de la base de datos que estamos utilizando
db.dropDatabase() Elimina la base de datos actual
db.help() Muestra los comandos disponibles
db.version() Muestra la versión actual del servidor

Y las operaciones básicas para realizar un CRUD, siguiendo la sintaxis db.nombreColeccion.operacion(), son:

  • insertOne: permite insertar un documento
  • find y findOne: recupera los documentos (o el primero) de una colección
  • countDocuments: obtiene la cantidad de documentos de una colección

En el resto de la sesión vamos a hacer un uso intenso del shell de MongoDB. Por ejemplo, si nos basamos en el objeto definido en el apartado de BSON, podemos ejecutar las siguientes instrucciones:

db.personas.insertOne(yo)  // (1)!
// {
//   acknowledged: true,
//   insertedId: ObjectId('67fbe737b496860bf5c3679e')
// }
db.personas.find()  // (2)!
// [
//   {
//     _id: ObjectId('67fa9f27d152b48c68424418'),
//     nombre: 'Aitor Medrano',
//     edad: 47,
//     profesion: 'Profesor'
//   }
//   {
//     _id: ObjectId('67fbe737b496860bf5c3679e'),
//     nombre: 'Aitor',
//     apellidos: 'Medrano',
//     fnac: 1977-10-02T23:00:00.000Z,
//     hobbies: [ 'programación', 'videojuegos', 'baloncesto' ],
//     casado: true,
//     hijos: 2,
//     contacto: {
//       bsky: "@aitormedrano.bsky.social",
//       email: 'a.medrano@edu.gva.es'
//     },
//     fechaCreacion: Timestamp({ t: 1744561974, i: 5 })
//   }
// ]
yo.profesion = "Profesor"
// Profesor
db.personas.insertOne(yo)  // (3)!
// {
//   acknowledged: true,
//   insertedId: ObjectId('67fbe851b496860bf5c3679f')
// }
db.personas.find()  // (4)!
// [
//   {
//     _id: ObjectId('67fa9f27d152b48c68424418'),
//     nombre: 'Aitor Medrano',
//     edad: 47,
//     profesion: 'Profesor'
//   }
//   {
//     _id: ObjectId('67fbe737b496860bf5c3679e'),
//     nombre: 'Aitor',
//     apellidos: 'Medrano',
//     fnac: 1977-10-02T23:00:00.000Z,
//     hobbies: [ 'programación', 'videojuegos', 'baloncesto' ],
//     casado: true,
//     hijos: 2,
//     contacto: {
//       bsky: "@aitormedrano.bsky.social",
//       email: 'a.medrano@edu.gva.es'
//     },
//     fechaCreacion: Timestamp({ t: 1744561974, i: 5 })
//   }
//   {
//     _id: ObjectId('67fbe851b496860bf5c3679f'),
//     nombre: 'Aitor',
//     apellidos: 'Medrano',
//     fnac: 1977-10-02T23:00:00.000Z,
//     hobbies: [ 'programación', 'videojuegos', 'baloncesto' ],
//     casado: true,
//     hijos: 2,
//     contacto: {
//       bsky: "'@aitormedrano.bsky.social",
//       email: 'a.medrano@edu.gva.es'
//     },
//     fechaCreacion: Timestamp({ t: 1744562256, i: 4 }),
//     profesion: 'Profesor'
//   }
// ]
db.personas.countDocuments()    // (5)!
// 3
  1. Si queremos insertar un documento en una colección, hemos de utilizar el método insertOne pasándole como parámetro el documento que queremos insertar, ya sea a partir de una variable o el propio documento en sí.
  2. find recupera todos los documentos de la colección
  3. Al volver a insertar el documento con un nuevo atributo profesion, obtenemos un nuevo ObjectId, ya que es un documento diferente al ya existente en la colección (y no por tener un atributo distinto, sino porque como no tiene atributo _id, le genera uno nuevo).
  4. Modificamos nuestro documento y los volvemos a insertar. Realmente va a crear un nuevo documento, y no se va a quejar de que ya exista, porque nuestro documento no contiene ningún atributo identificador, por lo que considera que se trata de una nueva persona.
  5. Obtenemos la cantidad de documentos de la colección mediante countDocuments.

Con este ejemplo, hemos podido observar cómo los documentos de una misma colección no tienen por qué tener el mismo esquema, ni hemos necesitado definirlo explícitamente antes de insertar datos. Así pues, el esquema se irá generando y actualizando conforme se inserten documentos. Más adelante veremos que podemos definir un esquema para validar que los datos que insertamos cumplan restricciones de tipos de datos o elementos que obligatoriamente deben estar rellenados.

Consultas

Para recuperar los datos de una colección o un documento en concreto usaremos el método find().

Para los siguiente ejemplos nos vamos a centrar en la colección zips de la base de datos de ejemplo sample_trainings que tenemos cargada en MongoAtlas, la cual tiene cargada cerca de 29.000 documentos con información de códigos postales de USA:

use sample_training
db.zips.find()
// {
//   _id: ObjectId('5c8eccc1caa187d17ca6ed40'),
//   city: 'MORRIS',
//   zip: '35116',
//   loc: { y: 33.739172, x: 86.772551 },
//   pop: 3622,
//   state: 'AL'
// }
// ...
// Type "it" for more

El método find() sobre una colección devuelve un cursor a los datos obtenidos, el cual se queda abierto con el servidor y que se cierra automáticamente a los 30 minutos de inactividad o al finalizar su recorrido. Si hay muchos resultados, la consola nos mostrará un subconjunto de los datos (20). Si queremos seguir obteniendo resultados, solo tenemos que introducir it, para que continúe iterando el cursor.

En cambio, si sólo queremos recuperar un documento hemos de utilizar findOne():

db.zips.findOne()
// {
//   _id: ObjectId('5c8eccc1caa187d17ca6ed40'),
//   city: 'MORRIS',
//   zip: '35116',
//   loc: { y: 33.739172, x: 86.772551 },
//   pop: 3622,
//   state: 'AL'
// }

Criterios en consultas

Si queremos indicar un criterio para filtrar los datos, tanto a find como a findOne le pasaremos un documento JSON con los criterios a cumplir. El caso más sencillo es filtrar por el valor de un determinado campo.

db.zips.find({city: "TITUS"})
{
    _id: ObjectId('5c8eccc1caa187d17ca6ee50'),
    city: 'TITUS',
    zip: '36080',
    loc: {
        y: 32.690019,
        x: 86.239334
    },
    pop: 2683,
    state: 'AL'
}    

Si queremos acceder a campos de subdocumentos, siguiendo la sintaxis de JSON, se utiliza la notación punto. Esta notación permite acceder al campo de un documento anidado, da igual el nivel en el que esté y su orden respecto al resto de campos.

Para acceder a la coordenada x usaremos la propiedad loc.x, la cual obligatoriamente deberemos rodear mediante comillas:

db.zips.find({"loc.x": 86.239334})

En cambio, si queremos indicar más de un criterio, el documento con las condiciones contendrá tantos campos como elementos a filtrar. Así pues, para obtener aquellas poblaciones del estado AL dentro de la ciudad DELTA, haríamos :

db.zips.find({city: "DELTA", state: "AL"})
{
    _id: ObjectId('5c8eccc1caa187d17ca6ee6e'),
    city: 'DELTA',
    zip: '36258',
    loc: {
        y: 33.457303,
        x: 85.679279
    },
    pop: 1405,
    state: 'AL'
}

Consejo de Rendimiento

Las consultas disyuntivas, es decir, con varios criterios u operador $and, deben filtrar el conjunto más pequeño cuanto más pronto posible.

Supongamos que vamos a consultar documentos que cumplen los criterios A, B y C. Digamos que el criterio A lo cumplen 40.000 documentos, el B lo hacen 9.000 y el C sólo 200. Si filtramos A, luego B, y finalmente C, el conjunto que trabaja cada criterio es muy grande.

MongoDB - Restringiendo consultas AND
Restringiendo consultas AND

En cambio, si hacemos una consulta que primero empiece por el criterio más restrictivo, el resultado con lo que se intersecciona el siguiente criterio es menor, y por tanto, se realizará más rápido.

MongoDB - Restringiendo consultas AND de menor a mayor
Restringiendo consultas AND de menor a mayor

MongoDB también ofrece operadores lógicos para los campos numéricos:

Comparador Operador
menor que (<) $lt
menor o igual que () $lte
mayor que (>) $gt
mayor o igual que () $gte

Estos operadores se pueden utilizar de forma simultánea sobre un mismo campo o sobre diferentes campos, sobre campos anidados o que forman parte de un array, y se colocan como un nuevo documento en el valor del campo a filtrar, compuesto del operador y del valor a comparar mediante la siguiente sintaxis:

db.<coleccion>.find({ <campo>: { <operador>: <valor> } })

Por ejemplo, para recuperar las poblaciones con menos de 1000 habitantes, o aquellas que tienen entre 2000 y 3000 personas, podemos hacer:

db.zips.find({pop: {$lt:1000} })
db.zips.find({pop: {$gt:2000, $lte:3000} })

Para los campos de texto, además de la comparación directa, podemos usar el operador $ne para obtener los documentos cuyo campos no tienen un determinado valor (not equal). Así pues, podemos usarlo para averiguar todos las poblaciones que no pertenecen al estado de PA:

db.zips.find({state: {$ne:"PA"} })

Por supuesto, podemos tener diferentes operadores en campos distintos. Por ejemplo, si queremos recuperar las poblaciones del estado de AL con menos de 100 habitantes haríamos:

db.zips.find({pop: {$lt:100}, state: "AL" })

Case sensitive

Las comparaciones de cadenas se realizan siguiendo el orden UTF8, similar a ASCII, con lo cual no es lo mismo buscar un rango entre mayúsculas que minúsculas.

Con cierto parecido a la condición de valor no nulo de las BBDD relacionales y teniendo en cuenta que la libertad de esquema puede provocar que un documento tenga unos campos determinados y otro no lo tenga, podemos utilizar el operador $exists si queremos averiguar si un campo existe (y por tanto tiene algún valor).

use s8a
db.personas.find({edad: {$exists:true}})

Polimorfismo

Mucho cuidado al usar polimorfismo y almacenar en un mismo campo un entero y una cadena, ya que al hacer comparaciones para recuperar datos, no vamos a poder mezclar cadenas con valores numéricos. Se considera un antipatrón el mezclar tipos de datos en un campo.

Pese a que ciertos operadores contengan su correspondiente operador negado, MongoDB ofrece el operador $not. Éste puede utilizarse conjuntamente con otros operadores para negar el resultado de los documentos obtenidos.

Por ejemplo, si queremos obtener todas las personas cuya edad no sea múltiplo de 5, podríamos hacerlo así:

db.personas.find({edad: {$not: {$mod: [5,0]}}})

Proyección de campos

Las consultas realizadas hasta ahora devuelven los documentos completos. Si queremos que devuelva un campo determinado o varios campos en concreto, operación conocida como proyección, hemos de pasar un segundo parámetro de tipo JSON con aquellos campos que deseamos mostrar con el valor true o 1. Destacar que si no se indica nada, por defecto siempre mostrará el campo _id

db.zips.find({city: "TITUS"}, {pop: 1})
// {
//   _id: ObjectId('5c8eccc1caa187d17ca6ee50'),
//   pop: 2683
// }

Por lo tanto, si queremos que no se muestre el _id, lo podremos a false o 0:

db.zips.find({city: "TITUS"}, {pop: 1, _id:0})
// { pop: 2683 }

No mezcles

Al hacer una proyección, no podemos mezclar campos que se vean (1) con los que no ( 0). Es decir, hemos de hacer algo similar a:

db.<coleccion>.find({ <consulta> }, {<campo1>: 1, <campo2>: 1})
db.<coleccion>.find({ <consulta> }, {<campo1>: 0, <campo2>: 0})

Así pues, sólo se mezclará la visibilidad de los campos cuando queramos ocultar el _id.

Finalmente, si queremos renombrar un campo como resultado de la consulta, a modo de alias, podemos hacerlo referenciando el campo mediante $ (a la izquierda de los dos puntos ponemos el nuevo nombre (alias) y a la derecha va el campo original con el prefijo $ entre comillas):

db.zips.find({city: "TITUS"}, {poblacion: "$pop", _id:0})
// { poblacion: 2683 }

Condiciones compuestas

Para usar la conjunción o la disyunción, tenemos los operadores $and y $or. Son operadores prefijo, de modo que se ponen antes de las subconsultas que se van a evaluar. Estos operadores trabajan con arrays, donde cada uno de los elementos es un documento con la condición a evaluar, de modo que se realiza la unión entre estas condiciones, aplicando la lógica asociada a AND y a OR.

db.zips.find({ $or:[{city: "TITUS"}, {pop: 3}] })
db.zips.find({ $or:[{city: "TITUS"}, {pop: {$lte:10}}] })

Realmente el operador $and no se suele usar porque podemos anidar en la consulta dos criterios, al poner uno dentro del otro. Así pues, estas dos consultas hacen lo mismo:

db.zips.find({city: "OXFORD", state: "AL"})
db.zips.find({ $and:[ {city: "OXFORD"}, {state: "AL"} ] })

Consejo de Rendimiento

Las consultas conjuntivas, es decir, con varios criterios excluyentes u operador $or, deben filtrar el conjunto más grande cuanto más pronto posible.

Supongamos que vamos a consultar los mismos documentos que cumplen los criterios A (40.000 documentos), B (9.000 documentos) y C (200 documentos).

Si filtramos C, luego B, y finalmente A, el conjunto de documentos que tiene que comprobar MongoDB es muy grande.

MongoDB - Restringiendo consultas OR de menor a mayor
Restringiendo consultas OR de menor a mayor

En cambio, si hacemos una consulta que primero empiece por el criterio menos restrictivo, el conjunto de documentos sobre el cual va a tener que comprobar siguientes criterios va a ser menor, y por tanto, se realizará más rápido.

MongoDB - Restringiendo consultas OR de mayor a menor
Restringiendo consultas OR de mayor a menor

También podemos utilizar el operado $nor, que no es más que la negación de $or y que obtendrá aquellos documentos que no cumplan ninguna de las condiciones.

Autoevaluación

¿Qué obtendríamos al ejecutar la siguiente consulta?

db.zips.find({ city: "OXFORD", $nor:[ {state: "AL"}, {pop: 1306} ] })

Finalmente, si queremos indicar mediante un array los diferentes valores que puede cumplir un campo, podemos utilizar el operador $in:

db.zips.find({ state: {$in: ["AL", "AR"]} })

Por supuesto, también existe su negación mediante $nin.

Una vez sabemos realizar consultas con diferentes operadores, vamos a detallar casos concretos donde podemos realizar filtrados de datos más específicos.

Consultas avanzadas

Preparando los ejemplos

Para los siguientes ejemplos, vamos a utilizar una colección de 10.000 documentos sobre los viajes realizados por los usuarios de una empresa de alquiler de bicicletas, los cuales han sido extraídos de https://ride.citibikenyc.com/system-data.

Esta colección (trips) está cargada en los datos de ejemplo del clúster de MongoAtlas, dentro de la base de datos sample_training.

Un ejemplo de viaje sería el siguiente (puedes comprobar como para los campos que contiene espacios se indican entre comillas):

use sample_training
// switched to db sample_training
db.trips.findOne()
// {
//   _id: ObjectId('572bb8222b288919b68abf81'),
//   tripduration: 265,
//   'start station id': 376,
//   'start station name': 'John St & William St',
//   'end station id': 152,
//   'end station name': 'Warren St & Church St',
//   bikeid: 24119,
//   usertype: 'Subscriber',
//   'birth year': 1962,
//   'start station location': {
//     type: 'Point',
//     coordinates: [ -74.00722156, 40.70862144 ]
//   },
//   'end station location': {
//     type: 'Point',
//     coordinates: [ -74.00910627, 40.71473993
//     ]
//   },
//   'start time': 2016-01-01T00:14:26.000Z,
//   'stop time': 2016-01-01T00:18:52.000Z
// }

Cursores

Al hacer una consulta en el shell, se devuelve un cursor. Este cursor lo podemos guardar en un variable, y partir de ahí trabajar con él como haríamos mediante cualquier lenguaje de programación. Si cur es la variable que referencia al cursor, podremos utilizar los siguientes métodos:

Método Uso Lugar de ejecución
cur.hasNext() true/false para saber si quedan elementos Cliente
cur.next() Pasa al siguiente documento Cliente
cur.limit(cantidad) Restringe el número de resultados a cantidad Servidor
cur.sort({campo:1}) Ordena los datos por campo: 1 ascendente o -1 o descendente Servidor
cur.skip(cantidad) Permite saltar cantidad elementos con el cursor Servidor

La consulta no se ejecuta hasta que el cursor comprueba o pasa al siguiente documento (next/hasNext), por ello que tanto limit como sort (ambos modifican el cursor) sólo se pueden realizar antes de recorrer cualquier elemento del cursor.

Como tras realizar una consulta con find realmente se devuelve un cursor, un uso muy habitual es encadenar una operación de find con sort y/o limit para ordenar el resultado por uno o más campos y posteriormente limitar el número de documentos a devolver.

Así pues, si quisiéramos obtener los tres viajes que más han durado (recuperando sólo el identificador de la bicicleta y la duración del viaje), podríamos hacerlo así:

db.trips.find({},{"bikeid":1, "tripduration":1}).sort({"tripduration":-1}).limit(3)
// [
//   {
//     _id: ObjectId('572bb8222b288919b68ac07c'),
//     tripduration: 326222,
//     bikeid: 18591
//   },
//   {
//     _id: ObjectId('572bb8232b288919b68b0f0d'),
//     tripduration: 279620,
//     bikeid: 17547
//   },
//   {
//     _id: ObjectId('572bb8232b288919b68b0593'),
//     tripduration: 173357,
//     bikeid: 15881
//   }
// ]

También podemos filtrar previamente a ordenar y limitar:

db.trips.find({usertype:"Customer"}).sort({"tripduration":-1}).limit(3)

Finalmente, podemos paginar utilizando el método skip. Conviene destacar, que independientemente del orden en el que indiquemos las operaciones con cursores, se ejecuta así:

  1. find()
  2. sort()
  3. skip()
  4. limit()

Es decir, MongoDB siempre aplicará primero el filtro, luego el ordenamiento, después el salto y finalmente el límite.

Es por ello, que para mostrar viajes de 10 en 10 a partir de la tercera página, podríamos hacer algo así, obteniendo los mismos resultados:

db.trips.find({usertype:"Customer"}).sort({"tripduration":-1}).limit(10).skip(20)
db.trips.find({usertype:"Customer"}).sort({"tripduration":-1}).skip(20).limit(10)

Autoevaluación

A partir de la colección trips, escribe una consulta que recupere los viajes realizados por subscriptores ordenados descendentemente por su duración y que obtenga los documentos de 15 al 20.

Contando Documentos

Para contar el número de documentos, en vez de find usaremos el método countDocuments. Por ejemplo:

db.trips.countDocuments({"birth year":1977})
// 186  
db.trips.countDocuments({"birth year":1977, "tripduration":{$lt:600}})
// 116

count

Desde la versión 4.0, los métodos count a nivel de colección y de cursor están caducados (deprecated), y no se recomienda su utilización. Aún así, es muy común utilizarlo como método de un cursor:

db.trips.find({"birth year":1977, "tripduration":{$lt:600}}).count()

Cuando tenemos muchísimos datos, si no necesitamos exactitud pero queremos un valor estimado el cual tarde menos en conseguirse (utiliza los metadatos de las colecciones), podemos usar estimatedDocumentCount

db.trips.estimatedDocumentCount({"birth year":1977})
// 10.000
db.trips.estimatedDocumentCount({"birth year":1977, "tripduration":{$lt:600}})
// 10.000 

Trabajando con fechas

Cuando hemos estudiado la estructura de BSON hemos comprobado que podemos utilizar tipos de datos asociados a fechas como Timestamp o Date.

Por ejemplo, si creamos un documento con diferentes campos con fechas:

let fechas = {
    hoy: new Date(),
    cumple: new Date("Oct 3, 1977"),
    inicioCurso: new Date("2024-10-01"),
    ahora: new Timestamp(),
    ahoraISO: ISODate(),
    nochevieja: ISODate("2024-12-31T00:00:00.000Z")
}

Al mostrarlo en la consola de mongosh podemos ver cómo ha generado las fechas:

fechas
// {
//   hoy: 2025-04-23T08:53:26.186Z,
//   cumple: 1977-10-02T23:00:00.000Z,
//   inicioCurso: 2024-10-01T00:00:00.000Z,
//   ahora: Timestamp({ t: 0, i: 0 }),
//   ahoraISO: 2025-04-23T08:53:26.186Z,
//   nochevieja: 2024-12-31T00:00:00.000Z
// }

A la hora de realizar consultas, utilizaremos los campos de fecha de forma similar al resto de tipos de datos.

Por ejemplo, podemos ordenar por fechas:

db.trips.find({"bikeid": 24119}, {"start time":1,"stop time":1})
        .sort({"start time":1}).limit(3)
// [
//   {
//     _id: ObjectId('572bb8222b288919b68abf81'),
//     'start time': ISODate('2016-01-01T00:14:26.000Z'),
//     'stop time': ISODate('2016-01-01T00:18:52.000Z')
//   },
//   {
//     _id: ObjectId('572bb8222b288919b68ac20a'),
//     'start time': ISODate('2016-01-01T02:02:40.000Z'),
//     'stop time': ISODate('2016-01-01T02:15:58.000Z')
//   },
//   {
//     _id: ObjectId('572bb8222b288919b68ac3d4'),
//     'start time': ISODate('2016-01-01T03:49:38.000Z'),
//     'stop time': ISODate('2016-01-01T03:54:21.000Z')
//   }
// ]

O utilizar un rango:

db.trips.find({"bikeid": 24119,
   "start time": {$gt:ISODate("2016-01-01T00:00:00.000Z"),
                  $lte:ISODate("2016-01-01T12:00:00.000Z")}}).count()
// 5

También podemos utilizar las fechas y formatearlas al proyectar los campos (por ejemplo, utilizando el operador $dateToString pasamos un campo de tipo fecha a una cadena con el formato deseado):

db.trips.find({"bikeid": 24119,
   "start time": {$gt: ISODate("2016-01-01T00:00:00.000Z"),
                  $lte: ISODate("2016-01-01T06:00:00.000Z")}},
    {bikeid:1,
     fechaInicio: {$dateToString: {date: "$start time", format: "%Y-%m-%d %H:%M"}},
     fechaConsulta: {$dateToString: {date: new Date(), format: "%Y-%m-%d"}},
    })
// [
//   {
//     _id: ObjectId('572bb8222b288919b68abf81'),
//     bikeid: 24119,
//     fechaInicio: '2016-01-01 00:14',
//     fechaConsulta: '2025-04-23'
//   },
//   {
//     _id: ObjectId('572bb8222b288919b68ac20a'),
//     bikeid: 24119,
//     fechaInicio: '2016-01-01 02:02',
//     fechaConsulta: '2025-04-23'
//   },
//   {
//     _id: ObjectId('572bb8222b288919b68ac3d4'),
//     bikeid: 24119,
//     fechaInicio: '2016-01-01 03:49',
//     fechaConsulta: '2025-04-23'
//   }
// ]

Conjunto de valores

Igual que en SQL, a partir de un colección, si queremos obtener todos los diferentes valores que existen en un campo, utilizaremos el método distinct:

> db.trips.distinct('usertype')
< [ 'Customer', 'Subscriber' ]

Si queremos filtrar los datos sobre los que se obtienen los valores, le pasaremos un segundo parámetro con el criterio a aplicar:

> db.trips.distinct('usertype', { "birth year": { $gt: 1990 } } )
< [ 'Subscriber' ]

Expresiones regulares

Finalmente, si queremos realizar consultas sobre partes de un campo de texto, hemos de emplear expresiones regulares. Para ello, tenemos el operador $regexp o, de manera más sencilla, indicando como valor la expresión regular a cumplir:

Por ejemplo, para buscar la cantidad de viajes que salen da alguna estación cuyo nombre contenga Tree podemos hacer:

db.zips.find({city: /Tree/}).count()
db.zips.find({city: /tree/i}).count()
db.zips.find({city: {$regex:/tree/i}}).count()

Búsquedas sobre textos

Si vamos a realizar búsquedas intensivas sobre texto, desde MongoDB han creado un producto específico dentro del ecosistema de Mongo Atlas el cual ofrece un mejor rendimiento y mayor funcionalidad que el uso de expresiones regulares, conocido con Mongo Atlas Search.

Si usamos una solución on-premise, mediante índices de texto y el operator $text podemos realizar búsquedas.

Operador $expr

El operador $expr es un operador de consulta expresiva que permite utilizar expresiones de agregación dentro de las consultas.

Permite utilizar variables y sentencias condicionales, así como comparar campos dentro de un documento. Así pues, si queremos comparar valores entre dos campos, podemos hacerlo mediante $expr referenciando a los campos anteponiendo un dólar ($) delante del campo, de manera que si queremos obtener los viajes que comienzan y finalizan en la misma estación podemos hacer:

db.trips.find({ "$expr": { "$eq": [ "$end station id", "$start station id" ]}})

Al poner el $ delante de un campo, en vez de referenciar al campo, lo que hace es referenciar a su valor, por lo que $end station id está referenciando al valor del campo end station id.

Otros operadores

El operador $type permite recuperar documentos que dependan del tipo de campo que contiene.

db.trips.find({"start station name":{$type:"string"}})

El operador $where permite introducir una expresión JavaScript. No se recomienda su uso al no utilizar los índices existentes, a no ser que no haya manera de expresar un criterio mediante los operadores existentes.

Consultas sobre arrays

Preparando los ejemplos

Para los siguientes ejemplos sobre documentos anidados y arrays, vamos a utilizar una colección de 500 documentos sobre mensajes de un blog.

Esta colección (posts) está cargada tanto en el clúster de MongoAtlas como en el contenedor de Docker (si has seguido las instrucciones) de la base de datos sample_training.

Un ejemplo de mensaje sería:

use sample_training
// switched to db sample_training
db.posts.findOne()
// { _id: ObjectId("50ab0f8bbcf1bfe2536dc3f9"),
//   body: 'Amendment I\n<p>Congress shall make ...."\n<p>\n',
//   permalink: 'aRjNnLZkJkTyspAIoRGe',
//   author: 'machine',
//   title: 'Bill of Rights',
//   tags: [
//     'santa',
//     'xylophone',
//     'math',
//     'dream',
//     'action' ],
//   comments:
//   [ { body: 'Lorem ipsum dolor ...',
//       email: 'HvizfYVx@pKvLaagH.com',
//       author: 'Santiago Dollins' },
//     { body: 'Lorem ipsum dolor sit...',
//       email: 'WpOUCpdD@hccdxJvT.com',
//       author: 'Jaclyn Morado' },
//     { body: 'Lorem ipsum dolor sit amet...',
//       email: 'OgDzHfFN@cWsDtCtx.com',
//       author: 'Houston Valenti' }],
//   date: 2012-11-20T05:05:15.231Z }

Si trabajamos con arrays, vamos a poder consultar el contenido de una posición del mismo modo como si fuera un campo normal, siempre que sea un campo de primer nivel, es decir, no sea un documento embebido dentro de un array. Si hacemos una consulta sobre si un array contiene un elemento, obtendremos todos los documentos que contengan dicho valor:

db.posts.find({tags:"santa"}, {tags:1})
// { _id: ObjectId('50ab0f8bbcf1bfe2536dc3f9'),
//    tags: ['watchmaker', 'santa', 'xylophone', 'math', 'handsaw', 'dream', 'undershirt', 'dolphin', 'tanker', 'action'] }
// { _id: ObjectId('50ab0f8bbcf1bfe2536dc43b'),
//    tags: ['cup', 'community', 'santa', 'height', 'peen', 'beer', 'criminal', 'cousin', 'refund', 'clover'] }
// { _id: ObjectId('50ab0f8bbcf1bfe2536dc50d'),
//    tags: ['santa', 'spear', 'opinion', 'rainbow', 'century', 'puffin', 'romanian', 'scent', 'river', 'supermarket'] }

Si queremos filtrar teniendo en cuenta el número de ocurrencias del array, podemos utilizar:

  • $all para filtrar ocurrencias que tienen todos los valores especificado, es decir, los valores pasados a la consulta serán un subconjunto del resultado. Puede que devuelva los mismos, o un array con más campos (el orden no importa)
  • $in, igual que SQL, para obtener las ocurrencias que cumple con alguno de los valores pasados (similar a usar $or sobre un conjunto de valores de un mismo campo). Si queremos su negación, usaremos $nin, para obtener los documentos que no cumplen ninguno de los valores.

Por ejemplo, si queremos obtener los mensajes que contenga las etiquetas dream y action tendríamos:

db.posts.find( {tags: {$all: ["dream", "action"]}}, {tags:1})
// { "_id": ObjectId('50ab0f8bbcf1bfe2536dc3f9'),
//   "tags": ["watchmaker", "santa", "xylophone", "math", "handsaw", "dream", "undershirt", "dolphin", "tanker", "action"] }

En cambio, si queremos los mensajes que contengan alguna de esas etiquetas haríamos:

db.posts.find( {tags: {$in: ["dream", "mongodb"]}}, {tags:1})
// { "_id": ObjectId('50ab0f8bbcf1bfe2536dc3f9'),
//   "tags": ["watchmaker", "santa", "xylophone", "math", "handsaw", "dream", "undershirt", "dolphin", "tanker", "action"] }

Si el array contiene documentos y queremos filtrar la consulta sobre los campos de los documentos del array, tenemos que utilizar $elemMatch, de manera que obtengamos aquellos que al menos encuentre un elemento que cumpla el criterio. Así pues, si queremos recuperar los mensajes que tienen un comentario cuyo autor sea Santiago Dollins haríamos:

db.posts.find( {comments: {$elemMatch: { author: "Santiago Dollins", email: "xnZKyvWD@jHfVKtUh.com"}}} )

Criterio con notación punto

En el ejemplo anterior, si sólo hubiéramos tenido un campo para el filtrado, podríamos haber utilizado la notación punto comments.author.

Si sólo queremos los comentarios escritos por un determinado autor, además de en el filtrado, hemos de indicarlo en la proyección:

db.posts.find(
  {comments: {$elemMatch: { author: "Santiago Dollins", email: "xnZKyvWD@jHfVKtUh.com"}}},
  {comments: {$elemMatch: { author: "Santiago Dollins", email: "xnZKyvWD@jHfVKtUh.com"}}} )

Si lo que nos interesa es la cantidad de elementos que contiene un array, emplearemos el operador $size. Por ejemplo, para obtener los mensajes que tienen 10 etiquetas haríamos:

db.posts.find( {tags : {$size : 10}} )

Finalmente, a la hora de proyectar los datos, si no estamos interesados en todos los valores de un campo que es un array, podemos restringir el resultado mediante el operador $slice. Así pues, si quisiéramos obtener los mensajes titulados US Constitution y que de esos mensajes, mostrara sólo tres etiquetas y dos comentarios, haríamos:

db.posts.find( {title : "US Constitution"}, {comments: {$slice:2}, tags: {$slice:3}} )
// { _id: ObjectId("50ab0f8bbcf1bfe2536dc416"),
//   body: 'We the personas ...',
//   permalink: 'NhWDUNColpvxFjovsgqU',
//   author: 'machine',
//   title: 'US Constitution',
//   tags: [ 'engineer', 'granddaughter', 'sundial' ],
//   comments: 
//    [ { body: 'Lorem ipsum dolor ...',
//        email: 'ftRlVMZN@auLhwhlj.com',
//        author: 'Leonida Lafond' },
//      { body: 'Lorem ipsum dolor sit...',
//        email: 'dsoLAdFS@VGBBuDVs.com',
//        author: 'Nobuko Linzey' } ],
//   date: 2012-11-20T05:05:15.276Z }

Modificando documentos

Ya hemos visto que podemos añadir datos mediante la operación insertOne. Además, si queremos insertar más de un documento a la vez, podemos hacer uso de la operación insertMany, la cual admite un array de documentos.

Preparando una persona

Para este apartado, vamos a insertar dos veces la misma persona sobre la cual realizaremos las modificaciones:

> db.personas.insertMany([{nombre: "Aitor Medrano", edad: 45, profesion: "Profesor"},
                      {nombre: "Aitor Medrano", edad: 45, profesion: "Profesor"}, 
                      {nombre: "Andreu Medrano", edad: 12, profesion: "Estudiante"}])
< {
    acknowledged: true,
    insertedIds: {
      '0': ObjectId("6532949b88c74b9656c772e3"),
      '1': ObjectId("6532949b88c74b9656c772e4"),
      '2': ObjectId("6532949b88c74b9656c772e5")
    }
  }

Para actualizar (y fusionar datos), se utilizan los métodos updateOne / updateMany dependiendo de cuantos documentos queremos que modifique. Ambos métodos requieren 2 parámetros: el primero es la consulta para averiguar sobre qué documentos, y en el segundo parámetro, los campos a modificar utilizando los operadores de actualización:

db.personas.updateOne({nombre:"Aitor Medrano"}, {$set:{nombre:"Marina Medrano", profesion:"Estudiante", salario: 123456}})

Al realizar la modificación, el shell nos devolverá información sobre cuantos documentos ha encontrado, modificado y más información:

{ acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0 }

Como hay más de una persona con el mismo nombre, al haber utilizado updateOne sólo modificará el primer documento que ha encontrado.

¡Cuidado!

En versiones antiguas de MongoDB, además de utilizar los operadores de actualización, podíamos pasarle como parámetro un documento, de manera que MongoDB realizaba un reemplazo de los campos, es decir, si en el origen había 100 campos y en la operación de modificación sólo poníamos 2, el resultado únicamente contendría 2 campos. Es por ello, que ahora es obligatorio utilizar los operadores.

Si cuando vamos a actualizar, en el criterio de selección no encuentra el documento sobre el que hacer los cambios, no se realiza ninguna acción.

Si quisiéramos que en el caso de no encontrar nada insertase un nuevo documento, acción conocida como upsert (update + insert), hay que pasarle un tercer parámetro al método con el objeto {upsert:true}. Si encuentra el documento, lo modificará, pero si no, creará uno nuevo:

db.personas.updateOne({nombre:"Andreu Medrano"},
                    {$set:{name:"Andreu Medrano", twitter: "@andreumedrano"}},
                    {upsert: true})

Operadores de actualización

MongoDB ofrece un conjunto de operadores para simplificar la modificación de campos.

El operador más utilizado es el operador $set, el cual admite los campos que se van a modificar. Si el campo no existe, lo creará.

Por ejemplo, para modificar el salario haríamos:

db.personas.updateOne({nombre:"Aitor Medrano"}, {$set:{salario: 1000000}})

Mediante $inc podemos incrementar (o decrementar en negativo) el valor de una variable:

db.personas.updateOne({nombre:"Aitor Medrano"}, {$inc:{salario: 1000}})
db.personas.updateOne({nombre:"Marina Medrano"}, {$inc:{salario: -500}})

Para eliminar un campo de un documento, usaremos el operador $unset.

De este modo, para eliminar el campo twitter de una persona haríamos:

db.personas.updateOne({nombre:"Aitor Medrano"}, {$unset:{twitter:''}})

Otros operadores que podemos utilizar son $mul, $min, $max y $currentDate. Podemos consultar todos los operadores disponibles en https://www.mongodb.com/docs/manual/reference/operator/update/

Autoevaluación

Tras realizar la siguiente operación sobre una colección vacía:

db.personas.updateOne({nombre:'yo'}, {'$set':{'hobbies':['gaming', 'sofing']}}, {upsert: true} );

¿Cuál es el estado de la colección?

Finalmente, un caso particular de las actualizaciones es la posibilidad de renombrar un campo mediante el operador $rename:

db.personas.updateMany( {_id:1}, {$rename:{'nickname':'alias', 'cell':'movil'}})

Podemos consultar todas las opciones de configuración de una actualización en https://www.mongodb.com/docs/manual/reference/method/db.collection.update/.

Reemplazando un documento

Desde Mongo 4.2, podemos emplear la operación replaceOne para sustituir todo el contenido por el documento que le pasamos como parámetro.

Así pues, si queremos sustituir el documento de Andreu Medrano, borrando los campos que tuviese y replazandolos por nuevos podríamos hacer:

db.personas.replaceOne({nombre:"Andreu Medrano"},
                     {nombre:"Andreu",
                      apellidos:"Medrano",
                      hobbies:["baloncesto","videojuegos","spiderman"]})

Control de la concurrencia

Cuando se hace una actualización múltiple, MongoDB no realiza la operación de manera atómica (a no ser que utilicemos transacciones desde el driver), lo que provoca que se puedan producir pausas (pause yielding). Cada documento en sí es atómico, por lo que ninguno se va a quedar a la mitad.

MongoDB ofrece el método findAndModify para encontrar y modificar un documento de manera atómica, y así evitar que, entre la búsqueda y la modificación, el estado del documento se vea afectado. Además, devuelve el documento modificado. Un caso de uso muy común es para contadores y casos similares.

db.personas.findAndModify({
  query: {nombre:"Marina Medrano"},
  update: {$inc:{salario:100, edad:-30}},
  new: true
})

Por defecto, el documento devuelto será el resultado que ha encontrado con la consulta. Si queremos que nos devuelva el documento modificado con los cambios deseados, necesitamos utilizar el parámetro new a true. Si no lo indicamos o lo ponemos a false, tendremos el comportamiento por defecto.

Actualizaciones sobre Arrays

Para trabajar con arrays necesitamos nuevos operadores que nos permitan tanto introducir como eliminar elementos de una manera más sencilla que sustituir todos los elementos del array.

Los operadores que podemos emplear para trabajar con arrays son:

Operador Propósito
$push Añade uno o varios elementos
$addToSet Añade un elemento sin duplicados
$pull Elimina un elemento
$pullAll Elimina varios elementos
$pop Elimina el primer o el último

Preparando los ejemplos

Para trabajar con los arrays, vamos a suponer que tenemos una colección de enlaces donde vamos a almacenar un documento por cada site, con un atributo tags con etiquetas sobre el enlace en cuestión

db.enlaces.insertOne({titulo:"www.google.es", tags:["mapas", "videos"]})

De modo que tendríamos el siguiente documento:

{ _id: ObjectId("633c60e8ac452ac9d7f9fe74"),
  titulo: 'www.google.es',
  tags: [ 'mapas', 'videos' ] }

Añadiendo elementos

Si queremos añadir uno o varios elementos, usaremos el operador $push. Cuando queremos añadir varios elementos a la vez, mediante el operador $each le pasamos un array con los datos:

db.enlaces.updateMany({titulo:"www.google.es"},
                     {$push: {tags:"blog"}} )
db.enlaces.updateMany({titulo:"www.google.es"},
                     {$push: {tags:{$each:["calendario", "email", "mapas"]}}})

Al hacer estar modificación, el resultado del documento sería:

{ _id: ObjectId("633c61b5ac452ac9d7f9fe75"),
  titulo: 'www.google.es',
  tags: [ 'mapas', 'videos', 'blog', 'calendario', 'email', 'mapas' ] }

Al utilizar $push no se tiene en cuenta lo que contiene el array, por tanto, si un elemento ya existe, se repetirá y tendremos duplicados. Si queremos evitar los duplicados, usaremos $addToSet:

db.enlaces.updateMany({titulo:"www.google.es"},
                  {$addToSet:{tags:"buscador"}})

Si queremos añadir más de un campo a la vez sin duplicados, debemos anidar el operador $each igual que hemos hecho antes:

db.enlaces.updateMany({titulo:"www.google.es"},
                  {$addToSet:{tags:{$each:["drive", "traductor"]}}})

En cambio, si queremos eliminar elementos de un array, usaremos el operador $pull:

db.enlaces.updateMany({titulo:"www.google.es"},
                  {$pull:{tags:"traductor"}})

Similar al caso anterior, con $pullAll, eliminaremos varios elementos de una sola vez:

db.enlaces.updateMany({titulo:"www.google.es"},
                  {$pullAll:{tags:["calendario", "email"]}})

Otra manera de eliminar elementos del array es mediante $pop, el cual elimina el primero (-1) o el último (1) elemento del array:

db.enlaces.updateMany({titulo:"www.google.es"}, {$pop:{tags:-1}})

Operador posicional

Por último, tenemos el operador posicional, el cual se expresa con el símbolo $ y nos permite modificar el elemento que ocupa una determinada posición del array sin saber exactamente cuál es esa posición.

Supongamos que tenemos las calificaciones de los estudiantes (colección students) en un documento con una estructura similar a la siguiente:

{ "_id" : 1, "notas" : [ 80, 85, 90 ] }

y queremos cambiar la calificación de 80 por 82. Mediante el operador posicional haremos:

db.students.updateOne( { _id: 1, notas: 80 }, { $set: { "notas.$" : 82 } } )

De manera similar, si queremos modificar parte de un documento el cual forma parte de un array, debemos usar la notación punto tras el $.

Por ejemplo, supongamos que tenemos estas calificaciones de un determinado alumno, las cuales forman parte de un objeto dentro de un array:

{ "_id" : 4, "notas" :
  [ { nota: 80, media: 75 },
    { nota: 85, media: 90 },
    { nota: 90, media: 85 } ] }

Podemos observar cómo tenemos cada calificación como parte de un objeto dentro de un array. Si queremos cambiar el valor de media a 89 de la calificación cuya nota es 85, haremos:

db.students.updateOne( { _id: 4, "notas.nota": 85 }, { $set: { "notas.$.media" : 89 } } )

Es decir, el $ referencia al documento que ha cumplido el filtro de búsqueda.

Más operadores posicionales

Además del operador posicional $, tenemos disponible el operador posicional $[] que indica que afecta a todos los elementos del array, y el operador posicional $[identificador] que identifica que elementos del array cumplen una condición para su filtrado.

Podemos consultar toda la documentación disponible sobre estos operadores en http://docs.mongodb.org/manual/reference/operator/update-array/

Borrando documentos

Para borrar, usaremos los método deleteOne o deleteMany, los cuales funcionan de manera similar a findOne y find. Si no pasamos ningún parámetro, deleteOne borrará el primer documento, o en el caso de deleteMany toda la colección documento a documento. Si le pasamos un parámetro, éste será el criterio de selección de documentos a eliminar.

db.personas.deleteOne({nombre:"Marina Medrano"})
db.personas.deleteMany({nombre:"Marina Medrano"})

Al eliminar un documento, no podemos olvidar que cualquier referencia al documento que exista en la base de datos seguirá existiendo. Por este motivo, manualmente también hay que eliminar o modificar esas referencias.

Eliminar un campo

Recordad que eliminar un determinado campo de un documento no se considera una operación de borrado, sino una actualización mediante el operador $unset.

Si queremos borrar toda la colección, es más eficiente usar el método drop, ya que también elimina los índices.

db.personas.drop()

Finalmente, para eliminar una base de datos usaremos el comando db.dropDatabase(), que eliminará la base de datos sobre la que nos encontremos (en nuestro caso, en iabd):

db.dropDatabase()

Administración

Empleando JavaScript

Ya hemos comentado que el shell utiliza JavaScript como lenguaje de interacción, por lo que podemos almacenar los comandos en un script externo y ejecutarlo mediante load():

load("scripts/misDatos.js");
load("/data/db/scripts/misDatos.js");

MongoDB Compass

Si utilizamos mongosh desde MongoDB Compass, no podemos hacer uso del comando load.
Sí que podemos crear variables y fragmentos de código en Javascript, sabiendo que para añadir un salto de línea emplearemos Shift+Enter.

Si hacemos una referencia relativa, lo hace respecto a la ruta desde la cual se ejecuta el shell mongosh.

Otra manera de lanzar un script es hacerlo desde la línea de comandos, pasándole como segundo parámetro el script a ejecutar:

mongosh iabd misDatos.js

Si el código a ejecutar no necesita almacenarse en un script externo, el propio shell permite introducir instrucciones en varias líneas:

for (var i=0;i<10;i++) {
  db.espias.insertOne({"nombre":"James Bond " + i, "agente":"00" + i});
}
// {
//   acknowledged: true,
//   insertedId: ObjectId('67fbf408b496860bf5c367a9')
// }
db.espias.find()
// [
//   {
//     _id: ObjectId('67fbf408b496860bf5c367a0'),
//     nombre: 'James Bond 0',
//     agente: '000'
//   }
//   {
//     _id: ObjectId('67fbf408b496860bf5c367a1'),
//     nombre: 'James Bond 1',
//     agente: '001'
//   }
//   ...
//   {
//     _id: ObjectId('67fbf408b496860bf5c367a9'),
//     nombre: 'James Bond 9',
//     agente: '009'
//   }
// ]

Importando y exportando

Además del propio servidor de MongoDB y el cliente para conectarse a él, MongoDB ofrece un conjunto de herramientas para interactuar con las bases de datos, permitiendo crear y restaurar copias de seguridad.

Si estamos interesados en introducir o exportar una colección de datos mediante JSON, podemos emplear los comandos mongoimport y mongoexport:

mongoimport -d nombreBaseDatos -c coleccion --file nombreFichero.json
mongoexport -d nombreBaseDatos -c coleccion nombreFichero.json

Estas herramientas interactúan con datos JSON y no sobre toda la base de datos.

Un caso particular y muy común es importar datos que se encuentran en formato CSV/TSV. Para ello, emplearemos el parámetro --type csv:

mongoimport --type tsv -d test -c poblacion --headerline --drop poblacionEspanya2013.tsv

En vez de realizar un export, es más conveniente realizar un backup en binario mediante mongodump, el cual genera ficheros BSON. Estos archivos posteriormente se restauran mediante mongorestore:

mongodump -d nombreBaseDatos nombreFichero.bson
mongorestore -d nombreBaseDatos nombreFichero.bson

Autoevaluación

Intenta exportar los datos de la base de datos sample_training desde MongoAtlas. Verás que ha creado una carpeta que contiene dos archivos ¿Cuáles son? ¿Qué contiene cada uno de ellos y cuál es su formato?

Si necesitamos transformar un fichero BSON a JSON (de binario a texto), tenemos el comando bsondump:

bsondump file.bson > file.json

Info

Más información sobre copias de seguridad en https://www.mongodb.com/docs/manual/core/backups/.

Para poder trabajar con MongoDB desde cualquier aplicación necesitamos un driver. MongoDB ofrece drivers oficiales para casi todos los lenguajes de programación actuales.

Monitorización

Tanto mongostat como mongotop permiten visualizar el estado del servidor MongoDB, así como algunas estadísticas sobre su rendimiento. Si trabajamos con MongoAtlas estas herramientas están integradas en las diferentes herramientas de monitorización de la plataforma.

Referencias

Actividades

  • AC1301. (RABD.7 // CE7c, CE7d, CE7e // 3p) Crea un clúster en MongoAtlas, carga los datos de ejemplo y

    1. Adjunta capturas de pantalla del:

      • Dashboard del clúster
      • Bases de datos / colecciones creadas
    2. Conéctate mediante MongoDB Compass y adjunta una captura de pantalla tras conectar con el clúster.

    3. En la base de datos s8a, crea un documento con tus datos personales en la colección ac1301. Tras insertarlo, realiza una consulta para recuperarlo.
    4. Recupera las colecciones existentes en la base de datos.

    Adjunta un documento con las capturas realizadas, así como los comandos necesarios y la salida obtenida tras su ejecución.

  • AC1302. (RABD.7 // CE7c, CE7d, CE7e // 3p) Haciendo uso de mongosh, escribe los comandos necesarios y adjunta el resultado de las consultas para:

    1. Obtener las bases de datos creadas.
    2. Sobre la base de datos sample_training y la colección zips:
      1. Recupera todos los documentos.
      2. Recupera el primer documento.
      3. Recupera los documentos de la ciudad de SAN DIEGO.
      4. Averigua cuantos documentos hay de la ciudad de SAN DIEGO.
      5. Recupera los documentos cuya ciudad sea SAN DIEGO pero no pertenezcan al estado de CA.
      6. Cual es el código postal de la ciudad de ALLEN que no tiene habitantes (sólo recupera el zip, no nos interesa ningún otro campo, ni el _id).

  • AC1303. (RABD.7 // CE7c, CE7d, CE7e // 3p) Escribe los comandos necesarios para realizar las siguientes consultas sobre la colección zips de la base de datos sample_training:

    1. Averigua cuantos documentos tienen menos de 100 personas (campo pop).
    2. Obtén los estados de la ciudad de SAN DIEGO (Solución: [ 'CA', 'TX' ]).
    3. Listado con los 5 códigos postales más poblados (muestra los documentos completos).
    4. Cantidad de documentos que no tienen menos de 5.000 habitantes ni más de 1.000.000 (debes utilizar el operador $nor).
    5. Cuantos documentos tienen más habitantes que su propio código postal (campo zip).

  • AR1304. (RABD.7 // CE7c, CE7d, CE7e // 3p) Escribe los comandos necesarios para realizar las siguientes consultas sobre la colección trips de la base de datos sample_training:

    1. xxx

  • AC1305. (RABD.7 // CE7c, CE7d, CE7e // 3p) Escribe los comandos necesarios para realizar las siguientes consultas sobre la colección sales de la base de datos sample_supplies:

    1. Recupera los documentos que han vendido algún elemento (forma parte del array items) de nombre notepad.
    2. Averiguar cuantos documentos tienen elementos etiquetados (tags) como office.
    3. Recupera las ventas que tengan las etiquetas stationary, office y general.
    4. Recupera la ventas que tengan exactamente 4 eiquetas.

  • AR1306. (RABD.7 // CE7c, CE7d, CE7e // 3p) Escribe los comandos necesarios para realizar las siguientes operaciones sobre la colección posts:

    1. Cuantos mensajes tienen las etiquetas restaurant o moon.
    2. Los mensajes que tienen algún comentario que ha escrito el usuario Salena Olmos.
    3. Los comentarios (con su cuerpo y email) que ha escrito el usuario Salena Olmos.
    4. Recupera los mensajes que en body contengan la palabra earth, y devuelve el título, 3 comentarios y 5 etiquetas.

  • AC1307. (RABD.7 // CE7c, CE7d, CE7e // 3p) Escribe los comandos necesarios para realizar las siguientes operaciones sobre la colección zips:

    1. Crea una entrada con los siguientes datos:

      { city: 'ELX',
        zip: '03206',
        loc: {  x: 38.265500, y: -0.698459 },
        pop: 230224,
        state: 'España' }
      
    2. Crea una entrada con los datos del código postal donde vives (si es el mismo código postal, crea uno diferente).

    3. Modifica la población de tu código postal a 1.000.000.
    4. Incrementa la población de todos los documentos de España en 666 personas.
    5. Añade un campo prov a ambos documentos con valor Alicante.
    6. Modifica los documentos de España y añade un atributo tags que contenga un array vacío.
    7. Modifica todos los documentos de la provincia de Alicante y añade al atributo tags el valor sun.
    8. Modifica el valor de sun de tu código postal y sustitúyelo por house.
    9. Renombra en los documentos de la provincia de Alicante el atributo prov por provincia
    10. Elimina las coordenadas del zip 03206.
    11. Elimina tu entrada.

  • AR1316. (RABD.7 // CE7c, CE7d, CE7e // 3p) Una vez finalizada la unidad, responde todas las preguntas del cuestionario inicial, con al menos un par de líneas para cada una de las cuestiones.