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à.
| Vantaggio | Che cosa garantisce |
|---|---|
| Integrità referenziale | I vincoli come chiavi primarie e chiavi esterne assicurano che i dati collegati restino coerenti nel sistema |
| Proprietà ACID | Atomicità, Consistenza, Isolamento, Durabilità: le transazioni sono affidabili ed è possibile gestire gli errori mantenendo il database in uno stato consistente |
| Linguaggio standard SQL | Potente per query, aggregazioni, join tra tabelle e ordinamenti; facilita la collaborazione e il trasferimento di competenze |
| Dati strutturati | Ben 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.
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.
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.
| Tipologia | Descrizione | Esempi |
|---|---|---|
| Key-Value Store | Accesso rapidissimo tramite chiave | Redis, DynamoDB |
| Document Store | Documenti JSON-like | MongoDB, CouchDB |
| Column Store | Ottimizzati per query analitiche | Cassandra, HBase |
| Graph DB | Gestione di relazioni complesse | Neo4j |
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.
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.
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".
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.
| Vantaggi | Svantaggi |
|---|---|
| Coerenza e semplicità di gestione | Duplicazione 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).
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.
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.
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).
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
| Comando | Effetto |
|---|---|
show dbs | Stampa 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 |
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.
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}`);
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:
| Operatore | Descrizione | Esempio |
|---|---|---|
$eq, $ne | equals, not equals | { age: { $eq: 30 } } |
$gt, $lt | greater/less than | { age: { $gt: 30 } } |
$gte, $lte | greater/less or equal | { age: { $lte: 30 } } |
$in, $nin | in, not in | { age: { $in: [25, 30, 35] } } |
$and, $or | AND e OR logici | { $or: [{ age: { $lt: 25 } }, { age: { $gt: 35 } }] } |
$not | negazione | { age: { $not: { $gt: 30 } } } |
$exists | il 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.
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
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 } });
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.
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> },
...
])
| Stage | Cosa fa |
|---|---|
$match | Filtra i documenti |
$project | Seleziona e trasforma i campi |
$group | Raggruppa i documenti e calcola aggregazioni |
$sort | Ordina i risultati |
$limit / $skip | Limitano o saltano documenti |
$lookup | Join tra collezioni |
$unwind | Decompone array in documenti singoli |
$addFields | Aggiunge campi calcolati |
$bucket | Raggruppa valori in intervalli (es. fasce di età) |
$facet | Esegue 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" }
])
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.
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.
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.
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.
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.
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 Mongoose | Effetto |
|---|---|
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.
L'esercitazione propone quattro esercizi in scala crescente, e insieme costituiscono il percorso completo dalla shell al back-end del progetto.
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.
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.
È 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.
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.
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.
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.
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).
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.
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.
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.
$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.
$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.
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.
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.
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).