Ja tenim connexió i models. Toca moure dades. CRUD són les quatre operacions que sostenen qualsevol aplicació amb estat: crear, llegir, actualitzar i esborrar. Amb Mongoose cadascuna té diverses maneres d'escriure's, i no totes són equivalents: unes validen i altres no, unes retornen el document anterior i altres el nou, unes executen la consulta i altres només la construeixen.

Aquesta lliçó recorre les quatre amb els seus paranys reals, tradueix els errors de Mongoose a la jerarquia del mòdul 6, i culmina amb el moment anunciat des de la primera lliçó del mòdul: src/cataleg-dades.js es retira i src/repositoris/esdeveniments.js ocupa el seu lloc amb la mateixa signatura pública, sense que els controladors ni el domini se n'assabentin.

Contingut

  1. Crear documents
  2. Errors de clau duplicada i la jerarquia del mòdul 6
  3. Llegir: Query mandrosos, projeccions i lean()
  4. Ordenar, limitar i paginar
  5. Operadors de consulta essencials
  6. Actualitzar: runValidators, $set, $inc
  7. Esborrar i esborrat lògic
  8. El repositori: jubilant cataleg-dades.js
  9. POST /comandes contra la base de dades
  10. Errors de Mongoose i el seu estat HTTP

Crear documents

Hi ha dos camins, i la diferència no és estètica.

// Cami 1: instanciar i desar. Permet manipular el document abans d'escriure.
const esdeveniment = new Esdeveniment({
  esdevenimentId: 'evt-001', titol: 'Concierto de Otono', sala: 'Teatro Almendra',
  organitzadorId: 'org-almendra', categoria: 'musica', estat: 'publicat', duracioMinuts: 95,
  sessions: [
    { sessioId: 'ses-001-1', dataHora: new Date('2026-10-03T20:00:00Z'), aforament: 400, venudes: 312, preuCentims: 2500 },
    { sessioId: 'ses-001-2', dataHora: new Date('2026-10-04T19:00:00Z'), aforament: 400, venudes: 289, preuCentims: 2500 },
  ],
});
// Encara no hi ha res a la base: el document viu en memoria.
const desat = await esdeveniment.save(); // valida, dispara pre('save') i escriu

// Cami 2: crear directament. Equival a new + save en una linia.
const altre = await Esdeveniment.create({ esdevenimentId: 'evt-002', /* ... */ });
// Insercio massiva: una sola anada i tornada per a molts documents.
const diversos = await Esdeveniment.insertMany([documentA, documentB], { ordered: false });

save() i create() retornen el document ja persistit, amb el seu _id, els seus timestamps i els seus virtuals. insertMany és molt més ràpid que un bucle de create, però per defecte és ordenat: si el tercer document falla, els dos primers ja estan escrits i els següents no s'intenten; amb { ordered: false } els intenta tots i t'informa dels fallits, que és el que voldràs en una llavor. Quin fer servir: new + save() et deixa aplicar lògica entre construir i escriure (calcular el total, generar codis); create() és més directe quan les dades ja vénen completes.

Errors de clau duplicada i la jerarquia del mòdul 6

esdevenimentId i codi tenen índex únic. Si insereixes un duplicat, MongoDB respon amb E11000 duplicate key error. Si no el tradueixes, el gestor del mòdul 6 el tractarà com un bug i retornarà un 500. Però no és un bug: és un conflicte d'estat operatiu, i mereix un 409.

// src/repositoris/errors-mongo.js
'use strict';

const { ConflicteDEstat, ErrorDeValidacio } = require('../errors.js');

/**
 * Tradueix un error de Mongoose/MongoDB a la jerarquia de l'aplicacio. Si no el
 * reconeix el retorna tal qual: sera un 500, i esta be, perque significa que
 * es una fallada que no haviem previst.
 */
function traduirErrorDeMongo(error) {
  // Clau duplicada: violacio d'un index unic -> 409.
  if (error.code === 11000) {
    const camp = Object.keys(error.keyPattern ?? {})[0] ?? 'desconegut';
    return new ConflicteDEstat(`Ja existeix un registre amb el mateix ${camp}`, {
      codi: 'ESTAT_INVALID', detalls: { camp },
    });
  }
  // Validacio de l'esquema: un o diversos camps incompleixen les regles -> 400.
  if (error.name === 'ValidationError') {
    return new ErrorDeValidacio("El document no compleix l'esquema", {
      codi: 'DADES_INVALIDES',
      detalls: Object.entries(error.errors).map(([camp, f]) => ({ camp, missatge: f.message })),
    });
  }
  // Identificador amb forma invalida ('hola' no es un ObjectId) -> 400, no 500.
  if (error.name === 'CastError') {
    return new ErrorDeValidacio(`El valor de '${error.path}' no es valid`, {
      codi: 'DADES_INVALIDES', detalls: { camp: error.path, valor: String(error.value) },
    });
  }
  return error;
}

module.exports = { traduirErrorDeMongo };

Fixa't on viu aquesta traducció: a la capa de repositori, no al controlador. És coherent amb l'arquitectura de la lliçó 07-01: el controlador no ha de saber que existeix MongoDB, i per tant tampoc què és un E11000. El repositori tradueix del llenguatge del motor al de l'aplicació.

Llegir: Query mandrosos, projeccions i lean()

Aquí hi ha la sorpresa que descol·loca gairebé tothom la primera vegada:

// Aixo NO executa cap consulta: construeix un objecte Query.
const consulta = Esdeveniment.find({ sala: 'Teatro Almendra' });
console.log(consulta.constructor.name); // 'Query'

// Query te .then(), aixi que await l'executa: es un "thenable", no una
// promesa. Per aixo es pot refinar abans d'executar.
const publicats = await consulta.where('estat').equals('publicat').sort({ titol: 1 }).limit(20);

Que sigui mandrós és un avantatge: pots construir la consulta per parts segons els filtres opcionals que arriben per query string i executar-la al final. El parany és que una consulta sense await no fa res i no dóna error. Regla: sempre await, o .exec(), que retorna una promesa de veritat i millora les traces de pila.

const cataleg = await Esdeveniment.find({ estat: 'publicat' });        // array, buit si no n'hi ha
const un = await Esdeveniment.findOne({ esdevenimentId: 'evt-001' });  // document o null
const perId = await Esdeveniment.findById('507f1f77bcf86cd799439011'); // document o null

Cap no llança si no troba res: retornen null o [], i convertir aquest null en un RecursNoTrobat és feina del repositori o del controlador. Amb .orFail() llança en lloc de retornar null, la qual cosa de vegades és més còmoda.

Projecció: demanar només els camps necessaris. Menys bytes, menys memòria, menys feina del motor.

// Inclusiva: nomes aquests camps (mes _id, llevat que l'excloguis).
await Esdeveniment.find({ estat: 'publicat' }, { esdevenimentId: 1, titol: 1, _id: 0 });
// Exclusiva: tot menys aixo. No es poden barrejar inclusio i exclusio
// (llevat de _id, que es la unica excepcio).
await Esdeveniment.find({}).select('-sessions');

Aquest darrer exemple importa: la portada del catàleg no necessita les 7 sessions amb el seu aforament i el seu preu, només el títol i la sala.

lean() retorna objectes JavaScript plans en lloc de documents de Mongoose. Per defecte, Mongoose embolcalla cada resultat en una instància amb seguiment de canvis, getters, virtuals i mètodes: construir-la és car. Un objecte lean no té save(), ni virtuals, ni conversió de getters —et arriben els tipus BSON crus—, però es construeix gairebé de franc.

La regla: fes servir lean() en tota lectura que acabi en res.json(); no el facis servir quan hagis de modificar el document. En llistats grans la diferència és de diverses vegades en temps i memòria. A Escena Viva hi ha un matís: el nostre repositori retorna objectes del domini, així que farà lean() i després Esdeveniment.desDeJSON(...), més barat que hidratar dues vegades.

Ordenar, limitar i paginar

// Paginacio classica per desplacament.
const esdeveniments = await Esdeveniment.find({ estat: 'publicat' })
  .sort({ titol: 1 })
  .skip((pagina - 1) * perPagina)
  .limit(perPagina)
  .lean();

Per a les primeres pàgines és perfecte. Però skip es degrada linealment: per servir la pàgina 5.000 el motor localitza i descarta 100.000 documents abans de començar a retornar; no els salta màgicament, els recorre. A més, si algú insereix o esborra entre dues peticions, veuràs elements repetits o te'n saltaràs algun, perquè el desplaçament es calcula sobre un conjunt que ha canviat. L'alternativa és la paginació per cursor (o keyset): en lloc de "salta't 100.000", dius "dóna'm els següents a partir d'aquest valor".

/** Paginacio per cursor sobre un camp ordenat i unic (aqui, el _id). */
async function paginarEsdeveniments({ despresDe = null, limit = 20 } = {}) {
  const filtre = { estat: 'publicat' };
  // Nomes documents posteriors a l'ultim vist: fa servir l'index, no descarta res.
  if (despresDe) filtre._id = { $gt: despresDe };
  const documents = await Esdeveniment.find(filtre).sort({ _id: 1 }).limit(limit).lean();
  return { documents, seguent: documents.length === limit ? documents.at(-1)._id : null };
}

El cost és constant independentment de la profunditat i és estable davant d'insercions. El preu: no pots saltar a la pàgina 37, només avançar. Per a un catàleg amb desplaçament infinit o una API de consum automàtic, és el correcte.

Operadors de consulta essencials

Operador Significat Exemple a Escena Viva
$eq / $ne Igual (implícit) / diferent { estat: { $ne: 'esborrany' } }
$gt / $gte / $lt / $lte Comparacions { preuCentims: { $lte: 3000 } }
$in / $nin És / no és a la llista { sala: { $in: ['Teatro Almendra', 'Auditorio Ribera'] } }
$and / $or Lògics Publicats o del propi organitzador
$regex / $exists Expressió regular / el camp existeix Cerca per títol
$expr Comparar dos camps entre si venudes < aforament
$elemMatch Condicions sobre el mateix element d'un array Sessions en un rang
// 1. Esdeveniments amb alguna sessio en un rang de dates.
// COMPTE: sense $elemMatch, les dues condicions les poden complir sessions DIFERENTS.
const rang = { $gte: new Date('2026-03-01'), $lt: new Date('2026-03-16') };
await Esdeveniment.find({ sessions: { $elemMatch: { dataHora: rang } } }).lean();

// 2. Esdeveniments amb alguna sessio amb entrades lliures: comparar dos camps del
// mateix document exigeix $expr, perque el filtre normal compara amb literals.
await Esdeveniment.find({
  estat: 'publicat',
  sessions: { $elemMatch: { $expr: { $lt: ['$venudes', '$aforament'] } } },
}).lean();

// 3. Cerca per titol insensible a majuscules. Escapem l'entrada de
// l'usuari: un regex sense escapar es un vector de denegacio de servei.
await Esdeveniment.find({ titol: { $regex: escapar(terme), $options: 'i' } }).lean();

El punt 1 és un error clàssic: { 'sessions.dataHora': { $gte: a, $lt: b } } no significa "una sessió dins del rang", sinó "alguna sessió posterior a a i alguna anterior a b", cosa que pot complir un esdeveniment sense cap sessió al març.

Actualitzar: runValidators, $set, $inc

// Sense portar el document: rapid, pero no dispara pre('save').
// Retorna { acknowledged, matchedCount, modifiedCount, upsertedId }.
await Esdeveniment.updateOne({ esdevenimentId: 'evt-003' }, { $set: { estat: 'publicat' } });

// Actualitzar i rebre el document resultant.
const actualitzat = await Esdeveniment.findByIdAndUpdate(
  id,
  { $set: { duracioMinuts: 120 } },
  { new: true, runValidators: true },
);

Les dues opcions són gairebé obligatòries:

  • new: true retorna el document després del canvi. Per defecte —i això sorprèn tothom— retorna l'anterior. Sense ella respondràs al client amb dades velles.
  • runValidators: true executa els validadors de l'esquema. Sense ella et saltes la validació per complet: podries posar estat: 'inventat' encara que hi hagi un enum, o aforament: -5 encara que hi hagi min: 1. És el forat més gran i silenciós de Mongoose. Activa'l globalment amb mongoose.set('runValidators', true). I compte amb una limitació real: en validar una actualització, this és la consulta, no el document, així que un validador que compara venudes amb aforament no té accés a aforament si no l'estàs actualitzant també. La invariant crítica no pot descansar només allà.
Operador Què fa Ús a Escena Viva
$set / $unset Assigna / elimina un camp Canviar estat
$inc Suma (o resta) atòmicament Incrementar venudes
$push / $pull Afegeix / treu d'un array Afegir o retirar una sessió
$addToSet / $min / $max Afegeix si no hi és / assigna si és menor o major Etiquetes, marques històriques

I aquí apareix la primera peça del problema de l'aforament:

// Increment ATOMIC del comptador d'una sessio concreta. L'operador posicional $
// apunta a l'element de l'array que ha casat amb el filtre.
await Esdeveniment.updateOne(
  { esdevenimentId: 'evt-001', 'sessions.sessioId': 'ses-001-1' },
  { $inc: { 'sessions.$.venudes': 2 } },
);

Per què és un avenç? Perquè $inc no fa llegir-modificar-escriure al teu procés: mana al motor la instrucció "suma 2 a aquest camp", i el motor l'aplica sota el seu propi blocatge de document. Dues peticions simultànies amb $inc: 1 deixen el comptador a +2, mai a +1. S'ha acabat la pèrdua d'actualitzacions.

Però no n'hi ha prou, i convé veure-ho ara per no endur-se una falsa sensació de seguretat: $inc suma incondicionalment. Si quedava una entrada i arriben dues compres, totes dues sumen i venudes acaba valent aforament + 1. Hem resolt la pèrdua d'escriptures, no la comprovació de la condició. La peça que falta —un filtre que formi part de la mateixa operació atòmica— arriba a la lliçó 07-06.

Esborrar i esborrat lògic

await Esdeveniment.deleteOne({ esdevenimentId: 'evt-009' });
await Esdeveniment.findByIdAndDelete(id);              // retorna l'esborrat, o null
const { deletedCount } = await Entrada.deleteMany({ estat: 'anullada' });

// Pero a Escena Viva gairebe mai no esborrem comandes ni entrades: les marquem.
await Comanda.findByIdAndUpdate(comandaId, { $set: { estat: 'anullat' } }, { new: true, runValidators: true });
await Entrada.updateMany({ comandaId }, { $set: { estat: 'anullada' } });

Les raons són de negoci: comptabilitat (una comanda cobrada i després anul·lada ha de continuar apareixent al tancament del mes), traçabilitat (amb l'idPeticio del mòdul 6 podem reconstruir què va passar, però no si el document no existeix), integritat referencial (sense claus foranes, esborrar una comanda deixa entrades òrfenes) i errors humans (un esborrat lògic es reverteix canviant un camp; un de físic, restaurant una còpia de seguretat, amb sort). El preu —totes les consultes han de filtrar per estat— és baix i previsible.

El repositori: jubilant cataleg-dades.js

Ha arribat el moment. src/cataleg-dades.js exposava obtenirCataleg i obtenirEsdevenimentPerId llegint dades/esdeveniments.json. El nou mòdul exposa exactament el mateix, llegint MongoDB.

// src/repositoris/esdeveniments.js
'use strict';

const { Esdeveniment: ModelEsdeveniment } = require('../models/esdeveniment.js');
const { Esdeveniment } = require('../domini/esdeveniment.js');
const { traduirErrorDeMongo } = require('./errors-mongo.js');

/**
 * Converteix un document pla de Mongo en una instancia del domini.
 * Es la frontera: d'aqui cap amunt, ningu no sap que existeix Mongo.
 */
function aDomini({ esdevenimentId, sessions, ...resta }) {
  return Esdeveniment.desDeJSON({
    ...resta,
    id: esdevenimentId,
    sessions: sessions.map(({ sessioId, dataHora, ...dades }) => ({
      ...dades, // aforament, venudes, preuCentims
      id: sessioId,
      dataHora: dataHora.toISOString(),
    })),
  });
}

/** Cataleg complet, com a instancies del domini. */
async function obtenirCataleg() {
  try {
    // lean(): nomes llegim i transformem, no necessitem documents de Mongoose.
    const docs = await ModelEsdeveniment.find({ estat: { $ne: 'esborrany' } }).sort({ titol: 1 }).lean();
    return docs.map(aDomini);
  } catch (error) {
    throw traduirErrorDeMongo(error);
  }
}

/** Un esdeveniment pel seu identificador de negoci, o null si no existeix. */
async function obtenirEsdevenimentPerId(esdevenimentId) {
  const document = await ModelEsdeveniment.findOne({ esdevenimentId }).lean();
  return document ? aDomini(document) : null;
}

module.exports = { obtenirCataleg, obtenirEsdevenimentPerId };

El canvi als controladors és literalment una línia: on src/controladors/esdeveniments.js feia require('../cataleg-dades.js'), ara fa require('../repositoris/esdeveniments.js'). Això és tot. llistarEsdeveniments, carregarEsdeveniment, obtenirEsdeveniment i llistarSessionsDeLEsdeveniment continuen funcionant sense tocar una coma, perquè continuen rebent instàncies d'Esdeveniment del domini amb els seus getters ocupacio, exhaurida i recaptacioCentims. Això és el que compra el patró repositori, i és la mateixa porta per la qual entrarà Sequelize a la lliçó 07-05.

POST /comandes contra la base de dades

El controlador ja rep dades validades per zod a req.dadesValidades. El que canvia és d'on surten i on van.

// src/repositoris/comandes.js (requires omesos per brevetat)
'use strict';

// Genera un codi d'entrada EV-<any>-<6 digits>.
const generarCodi = (any, n) => `EV-${any}-${String(n).padStart(6, '0')}`;

async function crearComanda({ usuariId, sessioId, quantitat, canal }) {
  const esdeveniment = await Esdeveniment.findOne({ 'sessions.sessioId': sessioId });
  if (!esdeveniment) {
    throw new RecursNoTrobat(`No existeix la sessio ${sessioId}`, { codi: 'SESSIO_NO_TROBADA' });
  }
  const sessio = esdeveniment.sessions.find((element) => element.sessioId === sessioId);
  const lliures = sessio.aforament - sessio.venudes;
  if (lliures < quantitat) {
    throw new ConflicteDEstat('No queden entrades suficients', {
      codi: 'AFORAMENT_INSUFICIENT',
      detalls: { lliures, sollicitades: quantitat },
    });
  }

  try {
    // ATENCIO: entre la comprovacio d'aforament i aquest $inc hi ha una finestra en
    // la qual una altra peticio es pot colar. Continua existint sobrevenda. Ho resolem
    // a la llico 07-06 amb una actualitzacio atomica condicional.
    await Esdeveniment.updateOne(
      { esdevenimentId: esdeveniment.esdevenimentId, 'sessions.sessioId': sessioId },
      { $inc: { 'sessions.$.venudes': quantitat } },
    );

    const comanda = await Comanda.create({
      usuariId, esdevenimentId: esdeveniment.esdevenimentId, sessioId, quantitat, canal,
      totalCentims: sessio.preuCentims * quantitat,
      estat: 'pagat',
    });

    const any = new Date().getUTCFullYear();
    const base = await Entrada.countDocuments();
    const entrades = await Entrada.insertMany(
      Array.from({ length: quantitat }, (unused, i) => ({
        codi: generarCodi(any, base + i + 1), comandaId: comanda._id, sessioId, estat: 'valida',
      })),
    );
    return { comanda, entrades };
  } catch (error) {
    throw traduirErrorDeMongo(error);
  }
}

module.exports = { crearComanda };

Deixa aquest comentari en majúscules on és. És el problema del curs, ara amb nom i línia concreta: hem guanyat atomicitat per operació, però no per conjunt, i a més la comanda i les entrades es poden crear sense que l'aforament s'hagi reservat, o a l'inrevés. A la 07-06 aquesta funció es reescriurà sencera i el comentari desapareixerà.

Errors de Mongoose i el seu estat HTTP

Error Quan apareix Traducció Estat
ValidationError Un camp incompleix l'esquema DADES_INVALIDES 400
CastError findById('hola'), data no analitzable DADES_INVALIDES 400
E11000 Índex únic violat ESTAT_INVALID 409
DocumentNotFoundError orFail() sense resultat ..._NO_TROBAT 404
MongoServerSelectionError / MongoNetworkError La base no respon SERVEI_EXTERN_CAIGUT 503

El CastError mereix atenció especial perquè és la fallada més freqüent en producció: un client demana GET /comandes/12345 amb un identificador que no és un ObjectId, Mongoose no el pot convertir i llança. Sense traducció, la teva aplicació retorna un 500 i el teu panell d'alertes s'omple d'errors que no són bugs: són peticions mal formades i mereixen un 400. La taula ESTAT_PER_CODI del mòdul 6 fa la resta; només cal donar-li el codi correcte.

Errors Comuns i Consells

  • Oblidar runValidators: true. El teu enum i els teus min deixen d'existir a les actualitzacions.
  • Oblidar new: true. Retornes l'estat anterior al canvi i passes mitja tarda buscant el bug al frontend.
  • No fer servir await. Un Query sense executar no falla: simplement no passa res, o passa a destemps.
  • Hidratar documents complets només per serialitzar-los. lean() i una projecció són l'optimització més barata que existeix.
  • Paginar amb skip en un catàleg gran. Funciona fins que no funciona, i falla just quan tens trànsit.
  • Interpolar entrada d'usuari en un $regex. Un patró com (a+)+$ pot penjar el motor. Escapa sempre.
  • Consell: fes servir .orFail() quan l'absència sigui un error real, i activa mongoose.set('debug', true) en desenvolupament: veuràs cada consulta que s'envia i descobriràs consultes que no sabies que feies.

Exercicis

Exercici 1: comandes per usuari

Afegeix a src/repositoris/comandes.js una funció llistarComandesDUsuari(usuariId, { pagina, perPagina }) que retorni les comandes no anul·lades d'un usuari, de la més recent a la més antiga, amb projecció de sessioId, quantitat, totalCentims, estat i createdAt, fent servir lean(). Indica quin índex necessita.

Exercici 2: cercador del catàleg

Escriu cercarEsdeveniments({ sala, categoria, desDe, finsA, nomesAmbEntrades }) que construeixi el filtre de forma incremental (afegint només les condicions rebudes) i retorni els esdeveniments publicats que hi casin. Fes servir $elemMatch per al rang de dates i $expr per a les entrades lliures.

Exercici 3: anul·lar una comanda

Implementa anullarComanda(comandaId) que comprovi que la comanda existeix i està en estat pagat o emes (si no, ConflicteDEstat amb ESTAT_INVALID), la marqui com a anullat, marqui les seves entrades com a anullades i retorni l'aforament. Anota què passa si el procés mor entre dues d'aquestes operacions.

Solucions

Exercici 1.

async function llistarComandesDUsuari(usuariId, { pagina = 1, perPagina = 20 } = {}) {
  return Comanda.find({ usuariId, estat: { $ne: 'anullat' } })
    .select('sessioId quantitat totalCentims estat createdAt')
    .sort({ createdAt: -1 })
    .skip((pagina - 1) * perPagina)
    .limit(perPagina)
    .lean();
}

Índex: { usuariId: 1, estat: 1, createdAt: -1 }, seguint la regla ESR — igualtat primer, ordenació després i en el mateix sentit.

Exercici 2.

async function cercarEsdeveniments({ sala, categoria, desDe, finsA, nomesAmbEntrades = false } = {}) {
  const filtre = { estat: 'publicat' };
  if (sala) filtre.sala = sala;
  if (categoria) filtre.categoria = categoria;

  const condicions = {};
  if (desDe) condicions.dataHora = { $gte: new Date(desDe) };
  if (finsA) condicions.dataHora = { ...condicions.dataHora, $lte: new Date(finsA) };
  if (nomesAmbEntrades) condicions.$expr = { $lt: ['$venudes', '$aforament'] };
  if (Object.keys(condicions).length > 0) filtre.sessions = { $elemMatch: condicions };

  return Esdeveniment.find(filtre).sort({ titol: 1 }).lean();
}

Exercici 3.

async function anullarComanda(comandaId) {
  const comanda = await Comanda.findById(comandaId).orFail();
  if (!['pagat', 'emes'].includes(comanda.estat)) {
    throw new ConflicteDEstat(`Una comanda ${comanda.estat} no es pot anullar`, { codi: 'ESTAT_INVALID' });
  }
  comanda.estat = 'anullat';
  await comanda.save();
  await Entrada.updateMany({ comandaId }, { $set: { estat: 'anullada' } });
  await Esdeveniment.updateOne(
    { esdevenimentId: comanda.esdevenimentId, 'sessions.sessioId': comanda.sessioId },
    { $inc: { 'sessions.$.venudes': -comanda.quantitat } },
  );
  // Si el proces mor entre aquestes tres escriptures, l'estat queda incoherent:
  // comanda anullada amb entrades valides, o aforament mai retornat. Cada operacio es
  // atomica per separat, pero el CONJUNT no ho es. Aixo son les transaccions (07-06).
  return comanda;
}

Conclusió

Ja saps moure dades amb Mongoose de veritat: crear amb save() o create() sabent què retorna cadascun, llegir entenent que un Query és mandrós i que lean() i les projeccions són l'optimització més rendible, paginar sense caure al parany de skip, filtrar amb els operadors adequats —inclosos $elemMatch i $expr, que eviten errors subtils sobre arrays—, actualitzar sense saltar-te la validació i esborrar lògicament per no perdre la història. I saps traduir els errors del motor a la jerarquia del mòdul 6 des de la capa correcta. Sobretot, src/cataleg-dades.js està jubilat: src/repositoris/esdeveniments.js ocupa el seu lloc amb la mateixa signatura, els controladors no se n'han assabentat i el domini continua rebent les seves instàncies d'Esdeveniment. L'arquitectura de la lliçó 07-01 acaba de demostrar que servia per a alguna cosa.

Queda un deute anotat en majúscules dins de crearComanda: $inc ens ha donat atomicitat per operació, però no per conjunt, i la sobrevenda continua viva. A la lliçó següent aparquem aquest problema una mica més per atacar el modelatge avançat: incrustar davant de referenciar, populate i el problema N+1, desnormalització deliberada, el framework d'agregació aplicat a informes reals d'Escena Viva —que substituiran els que al mòdul 3 fèiem llegint CSV— i índexs de debò, amb explain() per veure si les teves consultes els fan servir o estan recorrent la col·lecció sencera.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats