Salta el contingut

Queries i CRUD en MongoDB

Les operacions CRUD (Create, Read, Update, Delete) de MongoDB es fan a través del shell mongosh o des de codi d'aplicació amb drivers oficials (Python/pymongo, Node.js, Java, etc.). La sintaxi és JavaScript orientat a objectes, molt diferent del SQL declaratiu que coneixeu de PostgreSQL.

Connexió i entorn de treball

mongosh — el shell oficial

mongosh és el shell interactiu de MongoDB a partir de la versió 5.x (substitueix l'antic mongo). Es pot usar directament al contenidor Docker:

# Connectar-se al contenidor Docker (sense autenticació)
docker exec -it mongodb-nom-cognom mongosh

# Connectar-se amb autenticació
docker exec -it mongodb-nom-cognom mongosh \
  --username admin --password admin123 --authenticationDatabase admin

# Connectar des de fora del contenidor
mongosh "mongodb://admin:admin123@localhost:27017"

Un cop dins de mongosh, els comandaments bàsics de navegació:

// Mostrar bases de dades
show dbs

// Seleccionar (o crear) una base de dades
use bigdata_nom_cognom

// Mostrar col·leccions de la BD actual
show collections

// Comptar documents d'una col·lecció
db.productes.countDocuments()

// Sortir del shell
exit

MongoDB Compass

MongoDB Compass és l'eina gràfica oficial. Permet:

  • Explorar bases de dades i col·leccions visualment.
  • Executar queries amb un editor de filtres visual.
  • Construir i depurar pipelines d'agregació pas a pas.
  • Veure i crear índexs.
  • Analitzar l'esquema real dels documents d'una col·lecció.

Cadena de connexió per a Compass: mongodb://admin:admin123@localhost:27017

Preparació: el dataset de pràctica

Abans d'executar les queries de la pràctica, cal tenir una col·lecció de treball. Farem servir una col·lecció productes d'un e-commerce:

// Inserir documents de mostra
use bigdata_nom_cognom

db.productes.insertMany([
  {
    _id: 1,
    nom: "Portàtil Lenovo ThinkPad",
    categoria: "informatica",
    preu: 899.99,
    estoc: 15,
    valoracio: 4.7,
    tags: ["portàtil", "treball", "professional"],
    especificacions: {
      processador: "Intel i7",
      ram_gb: 16,
      disc_gb: 512,
      pes_kg: 1.8
    },
    disponible: true,
    data_alta: ISODate("2025-01-10")
  },
  {
    _id: 2,
    nom: "Ratolí Logitech MX Master",
    categoria: "peripherics",
    preu: 89.99,
    estoc: 42,
    valoracio: 4.9,
    tags: ["ratolí", "wireless", "ergonòmic"],
    especificacions: {
      connexio: "Bluetooth",
      bateria_dies: 70,
      pes_kg: 0.14
    },
    disponible: true,
    data_alta: ISODate("2025-02-05")
  },
  {
    _id: 3,
    nom: "Monitor Samsung 27\"",
    categoria: "monitors",
    preu: 349.00,
    estoc: 0,
    valoracio: 4.3,
    tags: ["monitor", "4k", "IPS"],
    especificacions: {
      resolucio: "3840x2160",
      hz: 60,
      connexions: ["HDMI", "DisplayPort", "USB-C"]
    },
    disponible: false,
    data_alta: ISODate("2025-01-20")
  },
  {
    _id: 4,
    nom: "Teclat mecànic Keychron K2",
    categoria: "peripherics",
    preu: 129.00,
    estoc: 28,
    valoracio: 4.6,
    tags: ["teclat", "mecànic", "wireless"],
    especificacions: {
      connexio: "Bluetooth",
      layout: "TKL",
      switch: "Brown"
    },
    disponible: true,
    data_alta: ISODate("2025-03-01")
  },
  {
    _id: 5,
    nom: "Disc dur extern WD 2TB",
    categoria: "emmagatzematge",
    preu: 69.99,
    estoc: 5,
    valoracio: 4.1,
    tags: ["disc", "extern", "backup"],
    especificacions: {
      capacitat_gb: 2000,
      connexio: "USB 3.0",
      pes_kg: 0.18
    },
    disponible: true,
    data_alta: ISODate("2024-11-15")
  }
])

CREATE — Inserció de documents

insertOne

Insereix un sol document. Retorna l'_id del document inserit.

db.productes.insertOne({
  nom: "Webcam Logitech C920",
  categoria: "peripherics",
  preu: 79.99,
  estoc: 20,
  valoracio: 4.5,
  tags: ["webcam", "HD", "streaming"],
  disponible: true,
  data_alta: new Date()
})
// Resultat:
// { acknowledged: true, insertedId: ObjectId("...") }

Si no especifiques _id

MongoDB genera automàticament un ObjectId com a _id. Si especifiques _id manualment (com en els exemples anteriors amb valors enters), has d'assegurar-te que és únic. Intentar inserir un document amb un _id ja existent lançarà un error de clau duplicada.

insertMany

Insereix múltiples documents en una sola operació. Molt més eficient que múltiples insertOne per a càrregues massives.

db.logs.insertMany([
  { usuari: "joan@exemple.cat", accio: "login", ts: new Date(), ip: "192.168.1.10" },
  { usuari: "maria@exemple.cat", accio: "compra", ts: new Date(), ip: "10.0.0.5" },
  { usuari: "joan@exemple.cat", accio: "logout", ts: new Date(), ip: "192.168.1.10" }
])
// Resultat:
// { acknowledged: true, insertedCount: 3, insertedIds: { 0: ..., 1: ..., 2: ... } }

Per defecte, insertMany és ordenat: si un document falla (p. ex. _id duplicat), s'atura i no insereix els documents restants. Per inserir tots els vàlids ignorant els errors: { ordered: false }.

db.productes.insertMany(
  [ { _id: 99, nom: "..." }, { _id: 1, nom: "Duplicat!" } ],
  { ordered: false }
)

READ — Consultes amb find()

find() bàsic

// Tots els documents (equivalent a SELECT * FROM productes)
db.productes.find()

// Formatar la sortida de manera llegible
db.productes.find().pretty()

// Un sol document que compleixi el filtre (equivalent a LIMIT 1)
db.productes.findOne({ categoria: "peripherics" })

Operadors de comparació

Operador SQL equivalent Exemple
$eq = { preu: { $eq: 89.99 } } o simplement { preu: 89.99 }
$ne != { disponible: { $ne: true } }
$gt > { preu: { $gt: 100 } }
$gte >= { preu: { $gte: 100 } }
$lt < { preu: { $lt: 100 } }
$lte <= { preu: { $lte: 100 } }
$in IN (...) { categoria: { $in: ["peripherics", "monitors"] } }
$nin NOT IN (...) { categoria: { $nin: ["emmagatzematge"] } }
// Productes amb preu entre 50 i 200 euros
db.productes.find({ preu: { $gte: 50, $lte: 200 } })

// Productes de les categories informatica o monitors
db.productes.find({ categoria: { $in: ["informatica", "monitors"] } })

// Productes amb estoc igual a 0 (esgotats)
db.productes.find({ estoc: 0 })

Operadors lògics

// $and — tots els filtres han de complir-se (per defecte és implícit)
db.productes.find({
  $and: [
    { preu: { $lt: 200 } },
    { disponible: true }
  ]
})

// Equivalent simplificat (sense $and explícit)
db.productes.find({ preu: { $lt: 200 }, disponible: true })

// $or — almenys un filtre ha de complir-se
db.productes.find({
  $or: [
    { categoria: "peripherics" },
    { valoracio: { $gte: 4.7 } }
  ]
})

// $not — nega el filtre (atenció: la sintaxi és diferent)
db.productes.find({ preu: { $not: { $gt: 500 } } })

// $nor — cap dels filtres ha de complir-se
db.productes.find({
  $nor: [
    { categoria: "emmagatzematge" },
    { disponible: false }
  ]
})

Consultes sobre documents niuats (notació punt)

Per accedir a camps dins de subdocuments, s'utilitza la notació punt. Les cometes al nom del camp en la notació punt són obligatòries:

// Buscar productes amb processador Intel i7 (camp dins de subdocument)
db.productes.find({ "especificacions.processador": "Intel i7" })

// Productes lleugers (pes inferior a 0.5 kg)
db.productes.find({ "especificacions.pes_kg": { $lt: 0.5 } })

// Productes amb connexió Bluetooth
db.productes.find({ "especificacions.connexio": "Bluetooth" })

Les cometes en la notació punt són obligatòries en mongosh

db.productes.find({ especificacions.connexio: "Bluetooth" }) donarà error de sintaxi. Cal escriure sempre { "especificacions.connexio": "Bluetooth" }.

Consultes sobre arrays

// Productes que tinguin el tag "wireless" (cerca dins l'array)
db.productes.find({ tags: "wireless" })

// $all — l'array ha de contenir TOTS els valors especificats
db.productes.find({ tags: { $all: ["mecànic", "wireless"] } })

// $size — l'array ha de tenir exactament N elements
db.productes.find({ tags: { $size: 3 } })

// $elemMatch — almenys un element de l'array ha de complir tots els criteris
// (útil quan els elements de l'array són objectes)
db.comandes.find({
  linies: {
    $elemMatch: { preu_unitari: { $gt: 100 }, quantitat: { $gte: 2 } }
  }
})

Projecció: seleccionar camps

El segon argument de find() és la projecció: quins camps retornar. 1 = incloure, 0 = excloure. No es pot barrejar inclusions i exclusions (excepte amb _id).

// Només nom i preu (i _id per defecte)
db.productes.find({}, { nom: 1, preu: 1 })

// Nom i preu sense _id
db.productes.find({}, { nom: 1, preu: 1, _id: 0 })

// Tot menys les especificacions (exclusió)
db.productes.find({}, { especificacions: 0 })

// Productes disponibles, mostrant nom, preu i valoració
db.productes.find(
  { disponible: true },
  { nom: 1, preu: 1, valoracio: 1, _id: 0 }
)

Ordenació, limit i skip

// Ordenar per preu ascendent (1) o descendent (-1)
db.productes.find().sort({ preu: 1 })
db.productes.find().sort({ preu: -1 })

// Ordenació per múltiples camps: primer categoria, despres preu descendent
db.productes.find().sort({ categoria: 1, preu: -1 })

// Limitar el nombre de resultats
db.productes.find().sort({ preu: -1 }).limit(3)

// Paginació: saltar els primers 10 i agafar 5
db.productes.find().sort({ data_alta: -1 }).skip(10).limit(5)

Rendiment de skip() en col·leccions grans

skip() a MongoDB ha de recórrer tots els documents fins al punt indicat. En col·leccions de milions de documents, un skip(500000) és molt lent. La millor estratègia de paginació en producció és usar el valor de l'_id o d'un camp indexat de l'últim document retornat com a cursor (range-based pagination).

UPDATE — Actualització de documents

updateOne i updateMany

// Actualitzar el preu d'un producte ($set)
db.productes.updateOne(
  { _id: 1 },                        // filtre (quin document)
  { $set: { preu: 849.99 } }         // modificació
)

// Actualitzar disponibilitat de tots els productes sense estoc ($set + $expr)
db.productes.updateMany(
  { estoc: 0 },
  { $set: { disponible: false } }
)

Operadors d'actualització

Operador Funció Exemple
$set Estableix el valor d'un camp { $set: { preu: 99 } }
$unset Elimina un camp del document { $unset: { camp_obsolet: "" } }
$inc Incrementa un valor numèric { $inc: { estoc: -1 } }
$mul Multiplica un valor numèric { $mul: { preu: 1.10 } }
$rename Reanomena un camp { $rename: { "nom_antic": "nom_nou" } }
$push Afegeix un element a un array { $push: { tags: "oferta" } }
$pull Elimina un element d'un array { $pull: { tags: "oferta" } }
$addToSet Afegeix a l'array si no existeix { $addToSet: { tags: "wireless" } }
$pop Elimina el primer o últim element { $pop: { tags: 1 } }
// Descompte del 10% en tots els perifèrics ($mul)
db.productes.updateMany(
  { categoria: "peripherics" },
  { $mul: { preu: 0.90 } }
)

// Afegir tag "oferta" sense duplicats ($addToSet)
db.productes.updateOne(
  { _id: 2 },
  { $addToSet: { tags: "oferta" } }
)

// Decrementar l'estoc en 1 quan es ven un producte ($inc)
db.productes.updateOne(
  { _id: 4, estoc: { $gt: 0 } },   // comprova que hi ha estoc
  { $inc: { estoc: -1 } }
)

// Afegir un camp nou a un document existent ($set)
db.productes.updateOne(
  { _id: 5 },
  { $set: { "especificacions.garantia_anys": 3 } }
)

// Eliminar un camp ($unset)
db.productes.updateMany(
  {},
  { $unset: { camp_temporal: "" } }
)

Upsert: inserir si no existeix

L'opció upsert: true crea el document si el filtre no troba cap coincidència:

// Actualitza el document si existeix, o el crea si no existeix
db.productes.updateOne(
  { nom: "Altaveu Bluetooth" },
  {
    $set: {
      categoria: "audio",
      preu: 49.99,
      disponible: true,
      data_alta: new Date()
    }
  },
  { upsert: true }
)

replaceOne

Substitueix el document complet (excepte l'_id). Diferent de updateOne amb $set, que és parcial:

// Substitueix tot el document
db.productes.replaceOne(
  { _id: 6 },
  { nom: "Nou producte complet", categoria: "audio", preu: 29.99 }
)

DELETE — Eliminació de documents

// Eliminar un sol document que compleixi el filtre
db.productes.deleteOne({ _id: 6 })

// Eliminar tots els documents que compleixin el filtre
db.productes.deleteMany({ disponible: false })

// Eliminar TOTS els documents d'una col·lecció (però no la col·lecció en si)
db.logs.deleteMany({})

// Eliminar la col·lecció sencera (inclou índexs)
db.logs.drop()

No hi ha recurs sense còpia de seguretat

deleteMany({}) i drop() són operacions irreversibles si no hi ha còpia de seguretat o replica set. En producció, sempre cal verificar el filtre amb un find() previ.

Transaccions multi-document

Des de MongoDB 4.0, les transaccions ACID multi-document estan disponibles en Replica Sets. Des de 4.2, també en clústers shardeds. Les transaccions permeten garantir que un conjunt d'operacions es completen totes o cap.

Quan usar transaccions a MongoDB

Les transaccions MongoDB estan dissenyades per a casos específics on la consistència entre múltiples documents és crítica (p. ex. transferències bancàries). En la majoria de casos d'ús de MongoDB, el bon schema design (embedding) elimina la necessitat de transaccions, ja que una sola escriptura en un document és atòmica per definició.

// Exemple de transacció: transferir estoc entre dos productes
const session = db.getMongo().startSession()
session.startTransaction()

try {
  const col = session.getDatabase("bigdata_nom_cognom").productes

  // Decrementar estoc del producte origen
  col.updateOne(
    { _id: 1, estoc: { $gte: 5 } },
    { $inc: { estoc: -5 } },
    { session }
  )

  // Incrementar estoc del producte destí
  col.updateOne(
    { _id: 2 },
    { $inc: { estoc: 5 } },
    { session }
  )

  session.commitTransaction()
  print("Transacció completada correctament")
} catch (e) {
  session.abortTransaction()
  print("Error: transacció revertida —", e.message)
} finally {
  session.endSession()
}

AC5074/03/02 — Miniactivitat

Queries sobre la col·lecció de productes e-commerce.

Usant la col·lecció productes creada en aquesta unitat (o ampliant-la fins a tenir com a mínim 15 documents de categories diverses), escriu les 10 queries mongosh següents:

  1. Tots els productes de la categoria "peripherics" amb preu inferior a 100 euros, mostrant només nom, preu i valoracio (sense _id).
  2. Productes amb valoració superior a 4.5 i estoc superior a 0, ordenats per valoració descendent.
  3. Productes que tinguin el tag "wireless" o el tag "4k".
  4. El producte més car de la col·lecció (usa sort i limit).
  5. Productes sense el camp especificacions.connexio (usa $exists: false).
  6. Incrementa en 10 unitats l'estoc de tots els productes de la categoria "peripherics".
  7. Afegeix el tag "recomanat" (sense duplicats) a tots els productes amb valoració >= 4.7.
  8. Elimina el camp especificacions.pes_kg de tots els documents que el tinguin.
  9. Upsert: actualitza (o crea) un producte amb nom "Auriculars Sony WH-1000XM5" amb preu 299.99 i categoria "audio".
  10. Compta quants productes hi ha per categoria (usa countDocuments amb filtre de categoria, una crida per categoria).

Lliura les queries en un fitxer .js amb comentaris que expliquin qué fa cadascuna.