Ir al contenido
wannacode

Bases de datos documentales

En resumen: Una base de datos documental como MongoDB guarda cada registro como un documento con campos anidados y arrays, sin un esquema único para todos los registros.

Problema

Productos que no se parecen

En la tabla products cada producto tiene las mismas columnas: nombre, categoría, precio, stock. La tienda quiere mostrar más. El peso y los alérgenos del pan, el origen y el tueste del café, el volumen del detergente. La tabla deja dos malas opciones. La primera son decenas de columnas que son NULL para la mayoría de los productos. La segunda es una tabla aparte de «propiedad, valor», donde todo es texto y cada lectura es un JOIN más.

Un pedido tiene un problema parecido. Para mostrárselo al cliente, lo compones a partir de cuatro tablas:

SQLbase de datos: tienda

El sitio siempre muestra el pedido entero: el cliente, las líneas, los precios. Y la base de datos lo vuelve a componer cada vez.

La idea

El documento

Una base de datos documental guarda cada registro como un documento: un objeto con campos, como JSON. Los documentos están en una colección, que se parece a una tabla pero no decide los campos. Cada documento tiene los suyos. A diferencia de una base clave-valor, una base documental ve dentro del valor y busca por sus campos.

El campo es un simulador con los comandos de MongoDB. Su consola se llama mongosh, y los comandos empiezan con db. y el nombre de la colección:

documental (como MongoDB)simulador

El pan tiene weight_g y allergens, el café origin y roast, el detergente volume_ml. Un valor puede ser un array (allergens) u otro documento. _id es la clave primaria: cada documento tiene una y no se repite.

Más precisamente, para curiosos

Si no das un _id, MongoDB añade por sí mismo un ObjectId('…'): 12 bytes que contienen el momento de creación y una parte aleatoria. El simulador los reparte en orden (ObjectId('6720a0000000000000000001'), luego …0002), para que la salida sea la misma siempre. Los reales no van así. Sin sort() el simulador devuelve los documentos en el orden en que entraron. MongoDB no promete ningún orden sin sort(). La salida es como en mongosh 2: el texto va entre comillas simples, y los documentos largos se parten con un campo por línea.

Pruébalo

Buscar en los documentos

find(filter, projection) busca. El filtro es un documento con los campos que tienen que coincidir. { price: { $lt: 3 } } significa «precio menor que 3»: $lt es «menor que», $lte «menor o igual», $gt y $gte «mayor», $ne «distinto», $in «uno de los enumerados». $exists pregunta si el campo existe. La proyección dice qué campos devolver: 1 significa «sí», y _id: 0 quita _id, que si no viene siempre. .sort({ price: 1 }) ordena de forma ascendente (-1 es descendente), y countDocuments cuenta:

documental (como MongoDB)simulador

El filtro { allergens: 'leche' } encuentra los documentos cuyo array contiene 'leche'. Ojo: { allergens: ['leche'] } busca justo ese array, con un elemento y nada más.

Muestra los nombres de los productos que contienen gluten, sin _id.

documental (como MongoDB)simulador

Míralo

Un error de escritura en el nombre del campo

La libertad tiene un precio. Una tabla conoce los nombres de sus columnas: un UPDATE de una columna prise que no existe da un error. Una colección no conoce nombres. El café se abarata, 12,90 €, pero hay una errata en el nombre del campo:

documental (como MongoDB)simulador

La base de datos responde modifiedCount: 1 y no da ningún error. Ha añadido un campo nuevo prise, y price sigue siendo 14.9. Por eso el café no sale entre los productos por menos de 13 €. Un error así solo se descubre cuando alguien nota el precio antiguo en el sitio.

Por eso el programa que escribe en una base de datos documental vigila la forma por sí mismo: una clase o un esquema en el código para cada tipo de documento. MongoDB también puede comprobar los documentos al escribir contra reglas de la colección.

Más precisamente, para curiosos

Las reglas se definen con db.createCollection('products', { validator: { $jsonSchema: … } }) o más tarde con el comando collMod. Dicen qué campos son obligatorios y de qué tipo. El simulador no tiene esa comprobación.

La idea

Incrustar o referenciar

El pedido del principio puede ser un solo documento, con el cliente y las líneas dentro. Esto se llama incrustar:

documental (como MongoDB)simulador

findOne lo devuelve entero en una lectura, sin JOIN. Llegas a un campo del documento incrustado con un punto: 'customer.city'. La ruta va entre comillas porque tiene un punto. Lo mismo funciona a través de un array. En este campo la colección orders ya tiene los pedidos 53–60 de la tienda, y 'items.product_id': 1 encuentra aquellos en los que alguna línea es del pan blanco:

documental (como MongoDB)simulador

El nombre y el precio de las líneas son copias, y es a propósito: el pedido conserva el precio del día de la compra, como unit_price en order_items. El nombre del cliente también es una copia. Si Maria cambia su nombre, los pedidos antiguos conservan el viejo. Para un pedido eso es lo correcto, pero el perfil del cliente vive en su propia colección, y el pedido solo lleva customer.id.

Incrustas cuando la parte pertenece al documento, se lee con él y es pequeña: un pedido tiene unas pocas líneas, no miles. Usas una referencia cuando algo lo comparten muchos documentos o crece sin fin. Las reseñas del café son así: hay muchas, se escriben en cualquier momento y cada una tiene su propio autor. Están en su propia colección, y product_id apunta al producto, como una clave foránea:

documental (como MongoDB)simulador

Incrustar es desnormalizar: lecturas más rápidas a cambio de copias que pueden desviarse.

Más precisamente, para curiosos

Un documento en MongoDB pesa como máximo 16 MB, así que un array que crece sin fin tarde o temprano no cabe en el documento. Las referencias se unen en aggregate con la etapa $lookup (con from, localField, foreignField y as), que hace algo parecido a un LEFT JOIN. El simulador solo la conoce de esa forma. Si necesitas $lookup en cada consulta, probablemente tus datos son relacionales.

Pruébalo

Cambios dentro del documento

updateOne(filter, change) cambia el primer documento que encuentra, y updateMany todos. El cambio es un operador: $set pone un valor (y crea el campo si no existe), $inc suma a un número, $push añade a un array, $unset quita un campo. Este campo vuelve a tener los pedidos 53–60. El pedido 60 está entregado, se le añade mantequilla, y el pan blanco pasa a tres:

documental (como MongoDB)simulador

'items.$.qty' significa «la qty de la línea que encontró el filtro». El filtro 'items.product_id': 1 encuentra la línea del pan blanco, y $ es su lugar en el array. El documento cambia entero de una vez: nadie puede leer el pedido a medio cambiar.

Sin un operador mongosh se niega: updateOne({ _id: 60 }, { status: 'delivered' }) da el error Update document requires atomic operators. Y $set sobre todo el array items lo reemplaza, y las líneas antiguas desaparecen.

Viktoria (pedido 58) quiere un pan integral más (producto 2) y dos yogures: { product_id: 6, name: 'Yogur', qty: 2, price: 1.1 }.

documental (como MongoDB)simulador

La idea

Agregación

Las preguntas de «cuánto» y «cuánto por» (ingresos por ciudad, productos más vendidos) se escriben con aggregate: un array de etapas por las que pasan los documentos una tras otra. Esto es una pipeline:

  • $match filtra, como WHERE;
  • $unwind divide el pedido en un documento por cada línea de items;
  • $group reúne los documentos en grupos por _id, como GROUP BY, con $sum, $avg, $min y $max;
  • $project conserva o calcula campos, y $sort y $limit ordenan y cortan.

Los ingresos por ciudad sin los pedidos cancelados, y después el número de pedidos:

documental (como MongoDB)simulador

{ $sum: 1 } cuenta los documentos del grupo, como COUNT(*). Sin $unwind los grupos sumarían pedidos, no líneas. $round redondea al céntimo: si no, una suma de números fraccionarios sale como 22.799999999999997 en lugar de 22,80 €, como con cualquier número de coma flotante.

Más precisamente, para curiosos

MongoDB no promete un orden de los grupos después de $group, así que después va un $sort, y con valores iguales también una segunda clave (_id: 1). El simulador da los grupos en el orden en que aparecieron. Suma los números fraccionarios un poco distinto de MongoDB y a veces difiere en la última cifra. Después de $round ambos dan lo mismo.

Ejercicio

Envíos, alérgenos y los más vendidos

Los campos de las tareas tienen los pedidos 53–60 y los productos del principio de la lección.

1. El mensajero viene a por los pedidos pagados.

Cambia el estado de todos los pedidos con status: 'paid' a 'shipped'.

documental (como MongoDB)simulador

2. Resulta que el bollo de mantequilla lleva también huevo.

Añade 'huevo' a los alérgenos del bollo de mantequilla (producto 3).

documental (como MongoDB)simulador

3. El almacén quiere saber qué se vende más.

¿Qué productos vendieron 5 unidades o más (sin los pedidos cancelados)? Muéstralos del más vendido hacia abajo, y con las mismas unidades por nombre.

documental (como MongoDB)simulador

La idea

Cuándo una base documental y cuándo no

Una base de datos documental es buena cuando un registro se lee y se escribe entero y su forma cambia: un catálogo de productos distintos, perfiles y ajustes, el contenido de un sitio, eventos de una aplicación, un pedido que se muestra de una vez.

Es una mala elección cuando los datos están conectados en todas las direcciones. La tienda pregunta «qué cliente compró qué», «qué productos se compran juntos», «ingresos por categoría y mes». Cada una de estas preguntas pasa por clientes, pedidos y productos a la vez. En SQL son JOIN sobre tablas normalizadas, donde cada hecho está escrito una vez. Aquí son copias que hay que mantener iguales, o un $lookup en cada consulta.

Lo mismo vale para una operación que toca muchos documentos: un pago que baja el stock de cinco productos y guarda el pedido. El cambio de un documento es todo o nada. Varios documentos necesitan una transacción, que es más compleja y lenta que una sola escritura.

Más precisamente, para curiosos

Son bases de datos documentales MongoDB (la más extendida, con mongosh y aggregate como en el simulador), Couchbase, Apache CouchDB, Firestore en Google Cloud y Amazon DocumentDB, que acepta los comandos de MongoDB. MongoDB tiene transacciones sobre muchos documentos desde la versión 4.0. PostgreSQL tiene el tipo jsonb: una columna con un documento en una tabla normal, con índices y operadores para sus campos. Ese suele ser el buen término medio: tablas para los datos conectados y jsonb para las propiedades distintas de los productos.

Test

Ponte a prueba

1. El comando db.products.updateOne({ _id: 17 }, { $set: { prise: 12.9 } }) devuelve modifiedCount: 1. ¿Qué ha pasado?
2. ¿Qué conviene incrustar en el documento de un pedido?
3. ¿Qué encuentra db.products.find({ allergens: 'leche' })?
4. Un pago baja el stock de cinco productos y guarda el pedido. ¿Por qué una base de datos documental no es aquí la mejor elección?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·