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:
- Tots els productes de la categoria
"peripherics"amb preu inferior a 100 euros, mostrant nomésnom,preuivaloracio(sense_id). - Productes amb valoració superior a 4.5 i estoc superior a 0, ordenats per valoració descendent.
- Productes que tinguin el tag
"wireless"o el tag"4k". - El producte més car de la col·lecció (usa
sortilimit). - Productes sense el camp
especificacions.connexio(usa$exists: false). - Incrementa en 10 unitats l'estoc de tots els productes de la categoria
"peripherics". - Afegeix el tag
"recomanat"(sense duplicats) a tots els productes amb valoració >= 4.7. - Elimina el camp
especificacions.pes_kgde tots els documents que el tinguin. - Upsert: actualitza (o crea) un producte amb nom
"Auriculars Sony WH-1000XM5"amb preu 299.99 i categoria"audio". - Compta quants productes hi ha per categoria (usa
countDocumentsamb filtre de categoria, una crida per categoria).
Lliura les queries en un fitxer .js amb comentaris que expliquin qué fa cadascuna.