Parte II — Il back-end JavaScript · Capitolo 7

NoSQL, MongoDB e Mongoose

~40 min di lettura5 widget interattivi3 tavole

In questo capitolo

  1. Pregi e limiti dei database relazionali
  2. Perché NoSQL: il caso del social network
  3. Le quattro famiglie NoSQL
  4. Il modello documentale
  5. Relazionale contro documentale: lo stesso dominio
  6. Aggregati: potenza e prezzo
  7. MongoDB: struttura e installazione
  8. Query: insert, find, update, delete
  9. L'aggregation pipeline
  10. MongoDB da Node.js: il driver ufficiale
  11. Mongoose: schemi, modelli e validazione
  12. Gli esercizi di laboratorio
  13. Verifica le tue conoscenze

1. Pregi e limiti dei database relazionali

Prima di parlare di NoSQL conviene ricordare perché i database relazionali sono stati lo standard per moltissimi anni: offrono solidità e caratteristiche che restano fondamentali in molti contesti applicativi, e sceglierne un altro senza sapere cosa si sta lasciando è un errore di progettazione, non un atto di modernità.

VantaggioChe cosa garantisce
Integrità referenzialeI vincoli come chiavi primarie e chiavi esterne assicurano che i dati collegati restino coerenti nel sistema
Proprietà ACIDAtomicità, Consistenza, Isolamento, Durabilità: le transazioni sono affidabili ed è possibile gestire gli errori mantenendo il database in uno stato consistente
Linguaggio standard SQLPotente per query, aggregazioni, join tra tabelle e ordinamenti; facilita la collaborazione e il trasferimento di competenze
Dati strutturatiBen definiti, normalizzati, con relazioni chiare: si evitano duplicazioni, si facilita la manutenzione e si migliora la manipolazione

I limiti emergono in scenari precisi, e sono cinque. La scalabilità orizzontale è complicata: distribuire i dati su molte macchine richiede soluzioni avanzate come lo sharding, che aumentano la complessità operativa. Lo schema fisso dà poca flessibilità: aggiungere o modificare colonne, o cambiare relazioni, può essere oneroso in schemi già popolati o con molti dati. Le prestazioni degradano su query complesse o join multipli, quando molte tabelle devono essere unite su grandi dataset. I dati semi-strutturati o non strutturati — immagini, documenti, JSON complessi — richiedono tipi speciali o workaround, perché non sono il modello nativo. Infine c'è il compromesso tra consistenza ed efficienza: la consistenza va garantita a ogni transazione, e questo può diminuire le prestazioni.

2. Perché NoSQL: il caso del social network

L'esempio con cui l'esercitazione introduce il problema è concreto: dovendo progettare un social network, come si gestiscono i post con commenti e like? E i like in tempo reale? E le relazioni tra utenti — amicizie, follower? Il dominio è semplice da disegnare — un USER che crea POST, un POST che ha COMMENT e riceve LIKE — ma è proprio la sua semplicità concettuale a rendere evidente il costo della normalizzazione: per mostrare un singolo post con i suoi commenti e i suoi like servono più join, e questo va fatto milioni di volte al minuto.

I database relazionali hanno dominato per decenni, ma non sempre sono la soluzione ideale: quando i dati diventano enormi, distribuiti o poco strutturati emergono i limiti. Gli esempi tipici citati sono tre — social network, dati sensoriali e IoT, log di sistema — e non è un caso che coincidano con diversi dei temi proposti per il progetto d'esame.

Idea chiave

NoSQL significa "Not Only SQL": non è un rifiuto di SQL, ma il riconoscimento che esistono casi in cui modelli alternativi sono più adatti. E non indica un singolo modello, bensì una famiglia di approcci, la cui caratteristica comune è la flessibilità: niente schema fisso e scalabilità orizzontale.

3. Le quattro famiglie NoSQL

TipologiaDescrizioneEsempi
Key-Value StoreAccesso rapidissimo tramite chiaveRedis, DynamoDB
Document StoreDocumenti JSON-likeMongoDB, CouchDB
Column StoreOttimizzati per query analiticheCassandra, HBase
Graph DBGestione di relazioni complesseNeo4j
Nota del redattore

Vale la pena notare l'ironia della quarta riga: il problema delle "relazioni tra utenti" che ha aperto il discorso — amicizie, follower — è esattamente ciò per cui esistono i graph database, non i document store. Il modello documentale che studieremo è la scelta giusta per aggregati (un post con i suoi commenti), non per reti (chi segue chi, a distanza di tre salti). Sapere che le famiglie sono quattro serve proprio a evitare di usare l'unica che si conosce per tutto.

4. Il modello documentale

La struttura del modello documentale si descrive in quattro righe che conviene imparare nell'ordine esatto:

La forza di questo modello è la sua flessibilità: non esiste uno schema rigido e ogni documento può avere campi differenti. Un documento contiene un identificatore univoco (_id), campi semplici (stringhe, numeri, booleani) e campi complessi (oggetti annidati, array, BLOB). L'esempio dell'esercitazione mostra tutte e tre le cose insieme:

{
  "_id": 12345678,
  "nome": "Manuel Andruccioli",
  "eta": 25,
  "indirizzo": {
    "citta": "Misano Adriatico",
    "cap": "47843"
  },
  "interessi": ["calcio", "musica", "viaggi"],
  "insegnamenti": [
    {"corso": "Applicazioni e Servizi Web", "anno": 2025},
    {"corso": "Applicazioni e Servizi Web", "anno": 2024},
    {"corso": "Smart Vehicular System", "anno": 2026},
    {"corso": "Smart Vehicular System", "anno": 2024},
    {"corso": "Fondamenti di Tec Web", "anno": 2026},
    {"corso": "Big Data LP", "anno": 2024}
  ]
}

Guardate indirizzo: in un modello relazionale sarebbero due colonne, o addirittura una tabella a parte con una chiave esterna. Guardate insegnamenti: in un modello relazionale sarebbe una tabella di associazione. Qui sono dentro il documento, e recuperare l'utente significa recuperare tutto quanto in una sola lettura.

5. Relazionale contro documentale: lo stesso dominio

L'esercitazione mette a confronto le due modellazioni sullo stesso dominio del social network. Nella versione relazionale ci sono quattro tabelle — User (id, name, age, city, zip), Post (id, user_id, content, timestamp), Comment (id, post_id, user_id, content, timestamp) e Like (id, post_id, user_id, timestamp) — legate da chiavi esterne. Nella versione documentale le possibilità sono due, ed è istruttivo che l'esercitazione le mostri entrambe.

// Documenti Post
{
  "id": 1,
  "content": "Hello world!",
  "timestamp": "2024-01-01 10:00:00",
  "like_count": 5
},
{
  "id": 2,
  "content": "My first post",
  "timestamp": "2024-01-02 11:30:00",
  "like_count": 3
}

Qui i post sono documenti autonomi e i like sono denormalizzati in un contatore: non serve contare le righe di una tabella, il numero è già lì. Si perde però l'informazione su chi ha messo like.

// Documento utente con post, commenti e like annidati
{
  "_id": 1,
  "name": "Manuel",
  "age": 25,
  "city": "Misano",
  "zip": "47843",
  "posts": [
    {
      "id": 1,
      "content": "Hello world!",
      "timestamp": "2024-01-01 10:00:00",
      "comments": [
        {
          "id": 1,
          "user_id": 2,
          "content": "Nice post!",
          "timestamp": "2024-01-01 10:05:00"
        }
      ],
      "likes": [2, 3]
    }
  ]
}

Qui l'intera attività di un utente sta in un unico documento: una sola lettura restituisce profilo, post, commenti e like. È la forma estrema dell'aggregato — comodissima per il caso "mostrami il profilo", scomoda per il caso "mostrami tutti i commenti scritti da Alice ovunque".

Relazionale Documentale User id, name, city Post id, user_id Comment post_id, user_id Like post_id, user_id mostrare un post = più join nessuna duplicazione integrità garantita dalle FK schema fisso _id, name, age, city, zip posts: [ id, content, timestamp comments: [ ... ] likes: [2, 3] ] mostrare un post = una lettura duplicazione possibile coerenza a carico dell'applicazione ogni documento può essere diverso
Tavola 7.1 — Lo stesso dominio, due modellazioni: a sinistra la normalizzazione paga in integrità ciò che costa in join, a destra l'aggregato paga in duplicazione ciò che guadagna in letture.

6. Aggregati: potenza e prezzo

Il concetto che tiene insieme tutto il capitolo è quello di aggregato: un documento può contenere un aggregato, cioè un gruppo di oggetti correlati trattati come un'unità. L'esempio classico è un ordine con i suoi articoli in un singolo documento.

VantaggiSvantaggi
Coerenza e semplicità di gestioneDuplicazione e rischio di inconsistenze se i dati si ripetono altrove
Accesso senza join a dati correlati
Facilità di gestione in un sistema distribuito

La terza voce merita attenzione perché spiega il legame tra modello documentale e scalabilità orizzontale: se tutto ciò che serve per rispondere a una richiesta sta in un solo documento, quel documento può vivere su una macchina qualsiasi del cluster senza che nessuna query debba attraversare la rete per ricomporre i pezzi. È l'aggregato a rendere praticabile lo sharding, non il contrario.

I casi d'uso in cui i database documentali sono molto utilizzati sono cinque: IoT (raccolta dati da sensori e dispositivi connessi), analytics in tempo reale (sentiment analysis, social media monitoring), log ed eventi (monitoraggio, tracciamento di web service), social network (post, commenti, relazioni tra utenti) ed e-commerce (prodotti e ordini con caratteristiche eterogenee).

Per l'esame

La domanda tipica non è "meglio SQL o NoSQL" ma "perché avete scelto un database documentale per il vostro progetto". La risposta convincente parte dall'aggregato: si individua l'unità di accesso dominante dell'applicazione (l'ordine con i suoi articoli, il post con i suoi commenti, la sessione di sensore con le sue letture) e si mostra che modellarla come documento elimina i join sul percorso critico. Aggiungete la contropartita — duplicazione e coerenza a carico dell'applicazione — perché dimostra che la scelta è consapevole e non di default.

7. MongoDB: struttura e installazione

MongoDB è uno dei database NoSQL più popolari e diffusi: è un database documentale open source, progettato per essere scalabile, flessibile e facile da usare. Le caratteristiche principali elencate a lezione sono cinque: il modello documentale con strutture flessibili e annidate; il supporto allo sharding nativo, che facilita la distribuzione dei dati su più server; il supporto a query complesse, aggregazioni, indicizzazioni avanzate e operazioni geospaziali; il supporto a molti linguaggi di programmazione, JavaScript incluso; il supporto a dati geospaziali e time-series.

La gerarchia degli oggetti ricalca quella del modello documentale, con la corrispondenza mentale al mondo relazionale: un'istanza gestisce uno o più database, un database contiene una o più collezioni (il corrispettivo delle tabelle), una collezione contiene documenti (il corrispettivo dei record), un documento contiene un insieme di campi.

istanza mongod database di sistema admin · local · config database dbMovies collezione movies documento { _id, title, year } collezione users documenti con campi anche diversi tra loro nessuno schema imposto dal database corrispondenze con il mondo relazionale collezione → tabella · documento → record · campo → colonna ma senza schema fisso e senza join obbligatori i database sono isolati tra loro: le operazioni su uno non influenzano gli altri
Tavola 7.2 — La gerarchia di MongoDB, con i database di sistema e la corrispondenza — approssimativa e utile — con il vocabolario relazionale.

Un'istanza può contenere più database, ognuno indipendente, con le proprie collezioni, identificati da nomi univoci e isolati tra loro: le operazioni su un database non influenzano gli altri. Esistono alcuni database di sistema: admin per la gestione globale, local per i dati specifici dell'istanza, config per la configurazione dello sharding.

Una collezione è un insieme di documenti all'interno di un database: è simile a una tabella relazionale, ma con maggiore flessibilità, perché non richiede uno schema fisso e i documenti al suo interno possono avere strutture diverse. È identificata da un nome univoco all'interno del database ed è possibile creare indici su campi specifici per migliorare le prestazioni delle query.

Un documento è l'unità fondamentale di dati ed è rappresentato in formato BSON (Binary JSON), che consente di memorizzare dati complessi e annidati. Ogni documento ha il campo speciale _id che funge da identificatore univoco all'interno della collezione; se non viene specificato, MongoDB lo genera automaticamente — è l'ObjectId che si vede negli esempi. Ogni campo è una coppia chiave-valore, dove la chiave è una stringa univoca all'interno dell'oggetto e il valore può essere semplice (stringa, numero, booleano) o complesso (oggetto, array, BLOB).

Installazione ed esecuzione

Un'installazione si compone principalmente di due programmi: mongod, l'eseguibile che avvia il demone del database, e mongosh, la shell per l'interazione. È possibile eseguire MongoDB in modi diversi — sorgenti, package manager, container, servizi cloud — ma durante le lezioni si usa Docker:

docker run --name my-db mongo

e, in un altro terminale, per interagire con il database:

docker exec -it my-db mongosh

Per l'esplorazione visuale c'è Mongo Express, un'interfaccia web di gestione che agevola l'esplorazione del database e l'interazione con esso, usata di norma in fase di sviluppo. Negli esempi è fornito un file docker compose minimale che esegue MongoDB e un'istanza di Mongo Express, raggiungibile su http://localhost:8081:

cd examples/example-01
docker compose up

I comandi di base nella shell

ComandoEffetto
show dbsStampa la lista dei database
use <db-name>Seleziona il database su cui eseguire i comandi
db.createCollection("myColl")Crea una collezione
db.getCollectionNames()Elenca le collezioni disponibili nel database
db.myFantasticCollection.insertOne({ x: 1 })Inserisce un documento, creando la collezione se non c'è
db.myFantasticCollection.find()Ricerca documenti (è possibile usare condizioni)
db.myFantasticCollection.countDocuments()Conta i documenti in una collezione

8. Query: insert, find, update, delete

Per interagire con i dati si usano istruzioni simili a SQL ma adattate al modello documentale. I comandi principali sono Insert, Find (con FindOne, Count, Aggregate), Update e Delete.

Insert

const result = await collection.insertOne({ name: "Mario", age: 30 });
console.log(`New document id: ${result.insertedId}`);

const result = await collection.insertMany([
  { name: "Luigi", age: 28 },
  { name: "Peach", age: 25 }
]);
console.log(`New document ids: ${result.insertedIds}`);

Find

La firma completa è db.collection.find(<query>, <projection>, <options>). findOne restituisce il primo documento, find restituisce un cursore su cui iterare:

const doc = await collection.findOne();
console.log(doc);

const cursor = collection.find();
await cursor.forEach(doc => console.log(doc));

Le condizioni si esprimono nella forma { <field>: { <operator>: <value> } }, dove field è il campo su cui applicare la condizione, operator l'operatore di confronto e value il valore:

OperatoreDescrizioneEsempio
$eq, $neequals, not equals{ age: { $eq: 30 } }
$gt, $ltgreater/less than{ age: { $gt: 30 } }
$gte, $ltegreater/less or equal{ age: { $lte: 30 } }
$in, $ninin, not in{ age: { $in: [25, 30, 35] } }
$and, $orAND e OR logici{ $or: [{ age: { $lt: 25 } }, { age: { $gt: 35 } }] }
$notnegazione{ age: { $not: { $gt: 30 } } }
$existsil campo esiste{ name: { $exists: true } }

L'elenco completo è molto più lungo e comprende, tra gli altri, $all, $elemMatch e $size per gli array, $regex per le espressioni regolari, $type, $mod, $expr, $jsonSchema, gli operatori bitwise e quelli geospaziali come $near, $geoWithin e $geoIntersects.

Projection e options

La projection specifica quali campi includere (1) o escludere (0) nei risultati; le options controllano ordinamento, limite e salto. Molte opzioni sono disponibili anche come metodi sul cursore restituito da find(), concatenabili per costruire query più complesse:

const cursor = collection.find({}, { name: 1, age: 1, _id: 0 });

const cursor = collection.find()
  .sort({ age: -1 })   // Ordina per età decrescente
  .limit(10)           // Limita a 10 risultati
  .skip(5);            // Salta i primi 5 risultati

Update e Delete

const result = await collection.updateOne(
  { name: "Mario" },
  { $set: { age: 31 } }
);
console.log(`Matched ${result.matchedCount}, Modified ${result.modifiedCount}`);

const result = await collection.updateMany(
  { age: { $lt: 30 } },
  { $inc: { age: 1 } }
);

const result = await collection.deleteOne({ name: "Mario" });
console.log(`Deleted ${result.deletedCount} document`);

const result = await collection.deleteMany({ age: { $lt: 30 } });
Attenzione

Nelle update il documento di aggiornamento va sempre espresso con un operatore ($set, $inc, $push…). Passare un oggetto nudo non aggiorna i campi indicati: sostituisce l'intero documento. È l'errore più comune di chi arriva da SQL, dove UPDATE ... SET tocca solo le colonne nominate.

9. L'aggregation pipeline

L'aggregation pipeline è un framework potente che permette di trasformare e combinare i dati. Funziona come una catena di fasi (stage) applicate sequenzialmente ai documenti: ogni stage riceve in input i documenti e produce un output che viene passato allo stage successivo.

db.collection.aggregate([
  { <stage1> },
  { <stage2> },
  ...
])
StageCosa fa
$matchFiltra i documenti
$projectSeleziona e trasforma i campi
$groupRaggruppa i documenti e calcola aggregazioni
$sortOrdina i risultati
$limit / $skipLimitano o saltano documenti
$lookupJoin tra collezioni
$unwindDecompone array in documenti singoli
$addFieldsAggiunge campi calcolati
$bucketRaggruppa valori in intervalli (es. fasce di età)
$facetEsegue più aggregazioni parallele in un'unica query

L'esempio più semplice conta quanti documenti hanno la stessa età:

db.users.aggregate([
  { $group: { _id: "$eta", count: { $sum: 1 } } },
  { $sort: { count: -1 } }
])
// { "_id": 25, "count": 5 }
// { "_id": 30, "count": 3 }
// { "_id": 28, "count": 2 }

Con più stage si compone un ragionamento completo — filtra, proietta, ordina:

db.users.aggregate([
  { $match: { eta: { $gt: 25 } } },
  { $project: { nome: 1, eta: 1, _id: 0 } },
  { $sort: { eta: 1 } }
])
// { "nome": "Bob", "eta": 28 }
// { "nome": "Alice", "eta": 30 }
// { "nome": "Charlie", "eta": 35 }

E con $lookup si ottiene ciò che il modello documentale aveva promesso di evitare — il join — quando serve davvero:

db.orders.aggregate([
  {
    $lookup: {
      from: "customers",
      localField: "customerId",
      foreignField: "_id",
      as: "customer"
    }
  },
  { $unwind: "$customer" }
])
Nota del redattore

La coppia $lookup + $unwind è un idioma da riconoscere: $lookup aggiunge un array di documenti corrispondenti (anche quando è uno solo), e $unwind lo "srotola" trasformando l'array in un campo singolo. Senza $unwind ogni ordine avrebbe un customer che è una lista di un elemento, scomoda da usare nella vista.

10. MongoDB da Node.js: il driver ufficiale

Per quanto mongosh sia comodo, nelle applicazioni reali è necessario interagire con il database tramite codice. Il driver ufficiale è l'approccio di più basso livello e permette la massima flessibilità; tutti i metodi visti finora sono disponibili attraverso di esso.

const { MongoClient } = require('mongodb');

const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri);

client.connect()
  .then(() => {
    const database = client.db('myDB');
    const collection = database.collection('myColl');
    const document = { myKey: 'My Value' };
    return collection.find({})
  })
  .then(async result => {
    for await (const doc of result) {
      console.log(doc);
    }
  })
  .catch(error => {
    console.error(`Error connecting to MongoDB
        or inserting document: ${error}`);
  })
  .finally(() => {
    client.close();
  });

Riconoscete la struttura: è la programmazione asincrona di Node del capitolo 4, con la catena di then, il catch per gli errori e il finally che chiude la connessione in ogni caso. Il ciclo for await serve perché find restituisce un cursore, cioè un flusso di documenti e non un array già materializzato.

11. Mongoose: schemi, modelli e validazione

Mongoose è una libreria di ODM (Object Document Mapper) per MongoDB, che permette di gestire i dati con schemi definiti, semplificando le operazioni sul database. Fornisce un'interfaccia per creare, leggere, aggiornare e cancellare documenti, rendendo l'utilizzo di MongoDB più strutturato e scalabile; e soprattutto, gli schemi definiti per le collezioni permettono di validare i dati.

Idea chiave

C'è una tensione interessante da notare: MongoDB è schema-less per scelta, e Mongoose reintroduce lo schema per scelta. Non è una contraddizione. Il database non impone una struttura — quindi collezioni eterogenee restano possibili e la migrazione non richiede ALTER TABLE — ma l'applicazione dichiara la struttura che si aspetta, e la fa rispettare nel punto in cui i dati entrano. Lo schema si sposta dal database al codice.

In Mongoose è sempre necessario uno schema che descriva la struttura dei documenti della collezione. Nello schema si specificano il tipo dei dati, i valori di default, l'obbligatorietà e altri vincoli. I tipi disponibili sono: String, Number, Date, Buffer, Boolean, Mixed, ObjectId, Array, Decimal128, Map, Schema, UUID e BigInt.

I passaggi principali sono quattro: connessione al database, definizione di uno Schema, compilazione da Schema a Modello, utilizzo del Modello.

Uno schema più ricco

const schema = new mongoose.Schema({
  name: String,
  living: Boolean,
  updated: { type: Date, default: Date.now },
  age: { type: Number, min: 18, max: 65, required: true },
  array: [],
  ofNumbers: [Number],
  ofStrings: [String],
  ofDates: [Date],
  ofArrayOfNumbers: [[Number]],
  nested: {
    stuff: { type: String, lowercase: true, trim: true }
  }
});

Notate min, max, required, default, lowercase, trim: sono vincoli e trasformazioni dichiarativi, applicati da Mongoose prima che il documento arrivi al database. È la stessa idea della validazione con express-validator vista nel capitolo 5, ma sul confine tra applicazione e persistenza invece che tra rete e applicazione.

Query e query builder

Le query sui modelli si scrivono con metodi simili a quelli del driver ufficiale, oppure con il query builder, che esprime le stesse condizioni con una catena leggibile:

Person
  .find({
    occupation: 'host',
    'name.last': 'Ghost',
    age: { $gt: 17, $lt: 66 },
    likes: { $in: ['vaporizing', 'talking'] }
  })
  .limit(10)
  .sort({ occupation: -1 })
  .select({ name: 1, occupation: 1 })
  .exec()
Person
  .find({ occupation: 'host' })
  .where('name.last').equals('Ghost')
  .where('age').gt(17).lt(66)
  .where('likes').in(['vaporizing', 'talking'])
  .limit(10)
  .sort('-occupation')
  .select('name occupation')
  .exec()
API di MongooseEffetto
model.find()Legge tutti i dati dalla collezione
model.findById(id)Trova un elemento dato l'id
model.findByIdAndUpdate(id, newContent)Modifica l'elemento dato l'id
model.findByIdAndDelete(id)Elimina l'elemento dato l'id
model.countDocuments()Conta il numero di documenti nella collezione

La corrispondenza con le rotte REST del capitolo 5 è quasi meccanica: GET /films chiama find(), GET /films/:id chiama findById, PUT /films/:id chiama findByIdAndUpdate, DELETE /films/:id chiama findByIdAndDelete. È esattamente questo il ponte che l'esercizio 03 chiede di costruire.

12. Gli esercizi di laboratorio

L'esercitazione propone quattro esercizi in scala crescente, e insieme costituiscono il percorso completo dalla shell al back-end del progetto.

Esercizio 01 — Query

Si usa un docker compose che avvia MongoDB con dati di esempio e Mongo Express; le query si eseguono con node queries.js (oppure node queries.js 1 per eseguirne una sola). Il database contiene tre collezioni: Utenti (nome, eta, interessi come array, indirizzo come oggetto annidato con citta e cap), Corsi (titolo, anno, docente) e Iscrizioni (utente, corso, voto). Le venti query coprono, nell'ordine: tutti gli utenti; l'utente di nome Alice; gli utenti con età maggiore di 25; quelli che abitano a Roma; quelli che hanno musica tra gli interessi; la proiezione di nome ed età; il conteggio di chi vive a Milano; gli utenti con età compresa tra 25 e 30; quelli con interessi su viaggi e yoga; quelli che non hanno calcio tra gli interessi; quelli con almeno due interessi; quelli che hanno il campo indirizzo.cap; l'aggiornamento dell'età di Bob a 31; l'aggiunta di cinema agli interessi di Carla; l'incremento di 1 dell'età di tutti gli utenti sotto i 30; l'eliminazione dell'utente Elena; il conteggio degli utenti per città; l'età media; i corsi con voto medio maggiore di 27; e infine un $lookup tra iscrizioni e corsi.

Notate la progressione: le prime dodici sono find con operatori, le successive quattro sono update e delete, le ultime quattro richiedono l'aggregation pipeline. Non è un elenco casuale, è un piano di studio.

Esercizio 02 — Mongoose

Si chiede di modellare con gli schemi di Mongoose un dominio con tre entità: Utenti (nome e mail), Articoli (titolo, contenuto, voto e autore, che è un utente del sistema) e Commenti agli articoli (contenuto, articolo di riferimento e utente che lo ha realizzato). Vanno inoltre realizzati gli endpoint per l'inserimento e il recupero di queste entità. Gli schemi e i modelli vanno in models.js, la logica delle rotte direttamente in index.js; per testare si usa un client come Postman o httpie, e per osservare il database si usa Mongo Express.

Esercizio 03 — API su MongoDB

È la ripresa dell'esercizio del capitolo 5, con la persistenza vera al posto dell'array in memoria: un webserver Express che espone API RESTful per gestire un insieme di film salvati su MongoDB, con le cinque operazioni consuete. La struttura fornita è quella già nota — routes per le rotte, controllers per la logica, models per lo schema — e il compito è implementare il modello Mongoose in moviesModel.js, esportarlo, e usarlo nei metodi di moviesControllers.js. La connessione avviene in index.js:

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/dbMovies');

L'esercizio precisa i confini: si gestiscono solo i casi in cui l'id specificato esiste, e si assume che i dati inseriti siano sempre corretti. Il compose file del database carica anche un valore iniziale.

Esercizio 04 — Query con parametri

Si aggiunge una rotta che riceve in GET una query contenente un attore e due anni come parametri, e restituisce tutti i film in cui compare quell'attore e che siano stati pubblicati nell'intervallo di tempo definito dalle due date. È l'incontro tra i due mondi del capitolo: la query string di Express (req.query) diventa un filtro Mongo con $gte e $lte sull'anno e una ricerca dentro un array per l'attore.

Per l'esame

Tre punti da avere pronti. Uno: la differenza tra driver ufficiale e ODM — il primo è di più basso livello e dà massima flessibilità, il secondo aggiunge schemi, validazione e struttura. Due: la differenza tra find con operatori e aggregate — la prima filtra e proietta, la seconda trasforma e combina attraverso una catena di stage, ed è l'unica che raggruppa ($group) e unisce collezioni ($lookup). Tre: perché nel vostro progetto avete annidato certi dati e referenziato altri, cioè dove avete messo il confine dell'aggregato.

Verifica le tue conoscenze

Quali vantaggi dei database relazionali si perdono passando a un documentale?

Quattro. L'integrità referenziale garantita da chiavi primarie ed esterne, che mantiene coerenti i dati collegati. Le proprietà ACID delle transazioni (MongoDB supporta comunque le transazioni, ma il modello di riferimento cambia). Il linguaggio standard SQL, potente per query, aggregazioni, join e ordinamenti, e utile perché facilita collaborazione e trasferimento di competenze. La struttura normalizzata dei dati, che evita duplicazioni e facilita la manutenzione. In compenso si guadagnano scalabilità orizzontale, assenza di schema fisso e naturalezza nel gestire dati semi-strutturati.

Che cosa significa "NoSQL" e quali sono le quattro famiglie principali?

Significa Not Only SQL: non è un rifiuto di SQL ma il riconoscimento che in certi casi modelli alternativi sono più adatti; non indica un singolo modello ma una famiglia di approcci, accomunati da flessibilità (niente schema fisso) e scalabilità orizzontale. Le quattro tipologie sono: Key-Value Store (accesso rapidissimo tramite chiave — Redis, DynamoDB), Document Store (documenti JSON-like — MongoDB, CouchDB), Column Store (ottimizzati per query analitiche — Cassandra, HBase) e Graph DB (relazioni complesse — Neo4j).

Che cos'è un aggregato e quali sono i suoi vantaggi e svantaggi?

Un aggregato è un gruppo di oggetti correlati trattati come un'unità: l'esempio classico è un ordine con i suoi articoli in un singolo documento. I vantaggi sono coerenza e semplicità di gestione, accesso senza join ai dati correlati e facilità di gestione in un sistema distribuito, perché tutto ciò che serve per rispondere sta su una sola macchina. Lo svantaggio è la duplicazione, con il conseguente rischio di inconsistenze se gli stessi dati si ripetono altrove.

Descrivete la gerarchia degli oggetti in MongoDB.

Un'istanza può contenere più database, indipendenti e isolati tra loro, con nomi univoci; esistono i database di sistema admin (gestione globale), local (dati specifici dell'istanza) e config (configurazione dello sharding). Un database contiene collezioni, simili a tabelle ma senza schema fisso, identificate da un nome univoco nel database e indicizzabili su campi specifici. Una collezione contiene documenti, l'unità fondamentale, rappresentati in BSON (Binary JSON), ciascuno con il campo _id come identificatore univoco, generato automaticamente se non specificato. Un documento contiene campi, coppie chiave-valore con valori semplici o complessi.

Che differenza c'è tra find e aggregate?

find interroga la collezione con una query, una projection e delle options: filtra i documenti, sceglie quali campi restituire e controlla ordinamento, limite e salto, restituendo un cursore. aggregate esegue una pipeline, cioè una catena di stage applicati in sequenza, in cui l'output di uno è l'input del successivo: permette di trasformare e combinare i dati, non solo di selezionarli. Solo la pipeline dispone di $group per raggruppare e calcolare aggregazioni, di $lookup per unire collezioni, di $unwind, $bucket, $facet e $addFields.

A cosa serve la coppia $lookup + $unwind?

$lookup realizza un join tra collezioni: indica la collezione from, il campo locale localField, il campo remoto foreignField e il nome del campo risultato as. Il risultato è però sempre un array di documenti corrispondenti, anche quando la corrispondenza è unica. $unwind decompone quell'array in documenti singoli, così che ogni ordine abbia un campo customer che è un oggetto e non una lista di un elemento. Nell'esempio dell'esercitazione i due stage servono a portare i dati del cliente dentro ogni ordine.

Perché nelle update di MongoDB serve un operatore come $set?

Perché il secondo argomento di updateOne/updateMany non è "l'elenco dei campi da cambiare" ma un documento di aggiornamento. Con { $set: { age: 31 } } si modifica solo il campo indicato; con { $inc: { age: 1 } } lo si incrementa. Il risultato riporta matchedCount e modifiedCount, che sono numeri diversi e vanno letti entrambi: un documento può corrispondere alla query ma non essere modificato se il valore era già quello.

Che cos'è Mongoose e che rapporto ha con l'assenza di schema di MongoDB?

Mongoose è un ODM (Object Document Mapper): fornisce un'interfaccia per creare, leggere, aggiornare e cancellare documenti in modo più strutturato e scalabile rispetto al driver, e richiede sempre uno schema che descriva la struttura dei documenti, con tipi, valori di default, obbligatorietà e vincoli come min, max, required, trim. Non contraddice la natura schema-less di MongoDB: il database continua a non imporre struttura, ma l'applicazione dichiara quella che si aspetta e la fa rispettare nel punto di ingresso dei dati, ottenendo validazione automatica.

Quali sono i quattro passaggi per usare Mongoose?

Connessione al database (con mongoose.connect, indicando l'URI e il nome del database), definizione di uno Schema (new mongoose.Schema({...})), compilazione da Schema a Modello (mongoose.model("nomeCollezione", schema)) e utilizzo del Modello — istanziando documenti e salvandoli con save(), oppure interrogando con find, findById, findByIdAndUpdate, findByIdAndDelete, countDocuments.

Come si collegano le rotte REST del capitolo precedente ai metodi di Mongoose?

Quasi uno a uno, ed è esattamente ciò che chiede l'esercizio 03. GET /films corrisponde a model.find(); GET /films/:id a model.findById(id); POST /films alla creazione di un documento dal modello seguita da save(); PUT /films/:id a model.findByIdAndUpdate(id, newContent); DELETE /films/:id a model.findByIdAndDelete(id). La struttura del progetto mantiene separati routes (quali URL esistono), controllers (cosa fare) e models (com'è fatto il dato).