Utilizzare Mongoose con Mongo DB

Corso completo Web Developer - Parte 3 - 3.2

Nella lezione precedente abbiamo esplorato insieme come utilizzare MongoDB, e ora è il momento di fare un passo avanti e conoscere Mongoose, l'ODM che ci aiuterà ad interagire con il database attraverso JavaScript. Un altro passo verso la creazione di applicazioni web complete con Node.js, Express e MongoDB.

Introduzione

Interagire con il database attraverso la Mongo Shell va bene per fare qualche test ma non è il modo ideale per gestire un database all'interno di un'applicazione. Mongoose ci servirà proprio come strumento per collegare un'applicazione Node JS ad un database Mongo e performare con JavaScript le stesse azioni che abbiamo visto nella lezione precedente con la Shell di Mongo.

Il ruolo degli ORM / ODM

Questo concetto di utilizzare uno strumento per collegare un'applicazione ad un database è definito ORM (Object Relational Mapping) nel caso di database relazionali, e ODM (Object Document Mapping) nel caso di database non relazionali. In entrambi i casi, l'idea è quella di utilizzare un linguaggio di programmazione per interagire con il database, invece di utilizzare un linguaggio di query specifico per il database stesso.

Preparazione del progetto

Per prima cosa dobbiamo avere un progetto Node JS pronto con il quale poter lavorare. Creiamo un'applicazione semplice con Express.

mkdir mongoose-demo
cd mongoose-demo
npm init -y
npm install express

Creiamo un file index.js con il seguente codice:

touch index.js (in Windows PowerShell usa il comando "New-Item" al posto di "touch")

Ed ora inseriamo il seguente codice:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(port, () => {
  console.log(`Server in esecuzione sulla porta ${port}`);
})

Avviamo il server con node index.js e visitiamo http://localhost:3000 per verificare che tutto funzioni correttamente.

Adesso installiamo Mongoose con npm install mongoose nella nostra applicazione

npm install mongoose

Ora prima che possiamo provare a connetterci con Mongoose ad un database Mongo, dobbiamo assicuraci che il database sia in esecuzione.

In Windows possiamo utilizzare il comando mongod per avviare il database in una Windows PowerShell, mentre in macOS possiamo utilizzare brew services start mongodb-community nel terminale.

Una volta che abbiamo controllato il tutto possiamo provare a connetterci al database.

Connettersi con Mongoose ad un Mongo DB

Per prima cosa dobbiamo importare Mongoose nel nostro file index.js. Aggiungiamo la seguente riga di codice all'inizio del file.

const mongoose = require('mongoose');

Adesso possiamo utilizzare il metodo connect di Mongoose per connetterci al database. Il metodo connect accetta come primo parametro la stringa di connessione al database, e come secondo parametro un oggetto di opzioni.

Il seguente codice puoi aggiungerlo subito dopo la definizione della porta del server.

mongoose.connect('mongodb://127.0.0.1:27017/anagrafe', { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connesso al database'))
  .catch(err => console.error('Impossibile connettersi al database', err));

A questo punto se tutto è andato a buon fine dovremmo vedere il messaggio Connesso al database nel terminale.

Notiamo che abbiamo passato un URL al metodo connect di Mongoose. Questo URL è il punto di accesso al database, e contiene il protocollo mongodb://, l'indirizzo del server 127.0.0.1, la porta 27017 e il nome del database anagrafe.

Il database anagrafe è il database che abbiamo creato nella lezione precedente con la Mongo Shell ma se non lo avessimo creato, Mongoose lo avrebbe creato in automatico per noi.

Definizione di un Mongoose Model

Adesso che siamo connessi al database, possiamo definire un Model. Un Mongoose Model è semplicemente una classe in JavaScript che serve a rappresentare nella nostra applicazione un entità che è presente nel database.

Prima di poter definire un Model dobbiamo creare uno Schema. Uno Schema è un oggetto che definisce la struttura di un documento. In altre parole, definisce quali proprietà ha un documento e quali tipi di dati possono avere queste proprietà.

Ad esempio, nel caso di una Persona, potremmo definire uno Schema in questo modo:

const personaSchema = new mongoose.Schema({
  nome: String,
  cognome: String,
  eta: Number,
  luogo_di_nascita: String,
});

Come possiamo vedere, uno Schema è semplicemente un oggetto JavaScript che definisce le proprietà di un documento e i tipi di dati che possono avere queste proprietà.

Adesso che abbiamo definito uno Schema, possiamo utilizzarlo per creare un Model. Per creare un Model dobbiamo utilizzare il metodo model di Mongoose. Il metodo model accetta come primo parametro il nome del Model, e come secondo parametro lo Schema che abbiamo creato.

const Persona = mongoose.model('Persona', personaSchema);

Facciamo attenzione al nome del Model che stiamo creando. Il nome del Model deve essere singolare e deve iniziare con una lettera maiuscola. Mongoose infatti, quando crea un Model, crea una collection nel database per noi.

Ora che abbiamo salvato questo Model in una costante Persona possiamo utilizzarlo per creare un nuovo documento.

const giulio = new Persona({
  nome: 'Giulio',
  cognome: 'Rossi',
  eta: 30,
  luogo_di_nascita: 'Roma',
});

Per ora abbiamo creato un nuovo documento ma non l'abbiamo ancora salvato nel database. Per salvare un documento nel database dobbiamo utilizzare il metodo save del documento stesso.

Per ora utilizzeremo la console di Node per provare a salvare il documento nel database.

Assicurati di stoppare il server dell'applicazione che abbiamo appena creato con CTRL + C.

Se utilizzi Windows

Se stai utilizzando Windows, a causa di un bug di Node su Windows dovremo utilizzare il comando node -i -e "$(< index.js)" per avviare la console di Node.

Inoltre se sei su Windows devi assicurarti che utilizzi il terminale di Git Bash e non quello di Windows PowerShell. Nel caso non dovessi avere Git Bash installato ti consiglio di leggere il mio articolo in cui spiego come installarlo qui: Iniziare a programmare

  1. Apri Git Bash come Amministratore
  1. Conferma la finestra di pop up
  1. Naviga fino alla cartella del progetto
  1. Avvia la console di Node con il seguente comando node -i -e "$(< index.js)" e acconsenti all'accesso al network

Se stai utilizzando macOS o Linux puoi semplicemente utilizzare i seguenti due comandi:

  1. node
  2. .load index.js

Una volta avviata la console di Node, dovremmo ritrovarci con una console simile alla seguente.

Per Windows:

Ora proviamo a salvare il documento nel database.

giulio.save();
Se utilizzi Mac
  1. Semplicemente nel terminale assicurati di stare alla cartella base del progetto
  2. Avvia la console di Node con il comando node
  3. Carica il file index.js con il comando .load index.js e dovresti avere una console simile alla seguente

Ora proviamo a salvare il documento nel database.

giulio.save();

Quello che abbiamo appena visto era un esempio di come creare un nuovo documento e salvarlo nel database ma per farlo abbiamo in parte ancora usato la console di Node. L'unica operazione che abbiamo fatto dalla console è stata quella del save().

Nelle prossime sezioni andremo a vedere come definire le operazioni CRUD in JavaScript e Mongoose e continueremo ad aiutarci con la console di Node per vedere i risultati.

CRUD con Mongoose

Insert (CRUD - Create)

Proviamo ad utilizzare il metodo insertMany per inserire più persone nello stesso momento nella nostra collection Persona.

Possiamo aggiungere il seguente codice alla fine del nostro file index.js

Persona.insertMany([
  { nome: "Luigi", cognome: "Rossi", eta: 40, luogo_di_nascita: "catania" },
  { nome: "Mario", cognome: "Verdi", eta: 50, luogo_di_nascita: "trento" },
  { nome: "Giulio", cognome: "Bianchi", eta: 20, luogo_di_nascita: "verona" }
]).then(data => {
  console.log(data);
}).catch(err => {
  console.log(err);
});

Questo è come il file index.js dovrebbe apparire adesso

const mongoose = require('mongoose');
const express = require('express');
const app = express();
const port = 3000;

mongoose.connect('mongodb://127.0.0.1:27017/anagrafe', { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connesso al database'))
  .catch(err => console.error('Impossibile connettersi al database', err));

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.listen(port, () => {
  console.log(`Server in esecuzione sulla porta ${port}`);
})

const personaSchema = new mongoose.Schema({
  nome: String,
  cognome: String,
  eta: Number,
  luogo_di_nascita: String,
});

const Persona = mongoose.model('Persona', personaSchema);

const giulio = new Persona({
  nome: 'Giulio',
  cognome: 'Rossi',
  eta: 30,
  luogo_di_nascita: 'Roma',
});

Persona.insertMany([
  { nome: "Luigi", cognome: "Rossi", eta: 40, luogo_di_nascita: "catania" },
  { nome: "Mario", cognome: "Verdi", eta: 50, luogo_di_nascita: "trento" },
  { nome: "Giulio", cognome: "Bianchi", eta: 20, luogo_di_nascita: "verona" }
]).then(data => {
  console.log(data);
}).catch(err => {
  console.log(err);
});

Il metodo insertMany non necessita di un save() in quanto salva automaticamente i documenti nel database nel momento in cui il codice JavaScript viene eseguito.

Per vedere i risultati dobbiamo riavviare la console di Node. Per uscire dalla console basta che premi ctrl + c per due volte e per rientrare:

  • Windows: node -i -e "$(< index.js)"
  • Mac: node e poi .load index.js

Ora dovrai vedere un risultato simile al seguente

Per vedere i documenti inseriti nel database possiamo utilizzare il metodo find. Nella prossima sezione vedremo come fare.

Find (CRUD - Read)

Per trovare tutti i documenti della collection Persona possiamo utilizzare il metodo find. Mentre sei nella console Node puoi provare ad eseguire il seguente codice

Persona.find({}).then(data => console.log(data))

Il risultato che dovresti ottenere dovrebbe essere simile al seguente

Nel mio risultato puoi notare che ho due volte Giulio Rossi. Questo perchè nella sezione precedente ho utilizzato due volte il metodo save() e quindi ho creato un duplicato.

Se provassi a riavviare la console di Node vedrai che questi dati che stiamo aggiungendo al database con il metodo insertMany verranno ricreati ogni volta e quindi finiremo per avere duplicati. Questo va bene per adesso visto che stiamo cercando di scoprire i metodi di Mongoose ed ovviamente ci sono modi per evitare ciò che vedremo in seguito.

Tornando al nostro metodo find, se gli passiamo un oggetto vuoto {} ci restituirà tutti i documenti della collection Persona. Se invece volessimo trovare solo le persone che si chiamano Giulio possiamo passare un oggetto con la chiave nome e il valore Giulio come segue

Persona.find({ nome: 'Giulio' }).then(data => console.log(data))

Prova anche te e noterai che avrai come output tutti i documenti attualmente salvati nel tuo database con il nome Giulio.

Puoi utilizzare anche il metodo findById al quale dovrai passare l'ID che Mongo ha generato per ogni documento.

Per esempio questo è l'ID di una persona nel mio database: 6454abfa0d0d25f230a3c082

Ora provo a trovare questa persona utilizzando il metodo findById come segue

Persona.findById('6454abfa0d0d25f230a3c082').then(data => console.log(data))

E questo è il risultato che ottengo

Ovviamente come puoi immaginare il risultato è un elemento soltanto. Puoi provare anche tu la stessa cosa ma assicurati di utilizzare un ID che esiste nel tuo database. Puoi vedere gli ID dei documenti utilizzando il metodo find({}) che abbiamo visto in precedenza e prendere l'ID di un documento dal risultato.

Update (CRUD - Update)

Abbiamo visto come creare e leggere i documenti. Ora vediamo come aggiornarli.

Ci sono svariati modi per aggiornare un documento in un database. Alcuni di quelli comuni sono: updateOne e updateMany.

Andiamo a vedere come utilizzare il metodo updateOne per aggiornare un documento. Dalla console di Node esegui il seguente codice

Persona.updateOne({ nome: 'Luigi' }, { nome: 'Vincenzo' }).then(data => console.log(data))

Questo codice andrà a cercare un documento con il nome Luigi e lo aggiornerà con il nome Vincenzo. Se tutto va bene dovresti vedere un risultato simile al seguente

Nota come l'output è un riassunto di quello che Mongoose ha eseguito.

A volte però vorremmo poter ricevere come output il documento aggiornato invece di un riassunto dell'aggiornamento eseguito. Per fare ciò possiamo trovare due metodi: findOneAndUpdate e findByIdAndUpdate.

Proviamo ad utilizzare il metodo findOneAndUpdate per aggiornare un documento. Dalla console di Node esegui il seguente codice

Persona.findOneAndUpdate({ nome: 'Vincenzo' }, { nome: 'Raffaele' }, { new: true }).then(data => console.log(data))

Questo codice andrà a cercare un documento con il nome Vincenzo e lo aggiornerà con il nome Raffaele. Nota come abbiamo aggiunto un terzo parametro che è un oggetto con la chiave new e il valore true. Questo ci permette di ricevere come output il documento aggiornato invece del documento prima dell'aggiornamento.

Se tutto va bene dovresti vedere un risultato simile al seguente

Il metodo findByIdAndUpdate funziona allo stesso modo del metodo findOneAndUpdate ma invece di passare un oggetto con il nome Vincenzo gli passeremo l'ID del documento che vogliamo aggiornare.

Delete (CRUD - Delete)

Abbiamo visto come creare, leggere e aggiornare i documenti. Ora vediamo come eliminarli.

È molto semplice e i metodi che possiamo trovare sono simili a quelli che abbiamo visto negli altri casi. Troviamo metodi come deleteOne, deleteMany, findOneAndDelete e findByIdAndDelete.

Proviamo ad utilizzare il metodo deleteOne per eliminare un documento. Dalla console di Node esegui il seguente codice

Persona.deleteOne({ nome: 'Raffaele' }).then(data => console.log(data))

Questo codice andrà a cercare un documento con il nome Raffaele e lo eliminerà. Se tutto va bene dovresti vedere un risultato simile al seguente

Nell'output troveremo una chiave deletedCount che ci indica quanti documenti sono stati eliminati, nel nostro caso uno.

In questo caso va bene che come output non riceviamo il documento, questo appunto perchè l'abbiamo appena eliminato ed è quindi inutile riceverlo come output.

E con questa operazione abbiamo concluso le operazioni CRUD con Mongoose!

Mongoose Schema Validations

Per introdurti al concetto di validazione dei dati con Mongoose voglio farti un esempio pratico tornando al concetto del registro anagrafe per tenere traccia della popolazione in una nazione.

Immagina di essere la persona addetta a questo registro e che ogni giorno ricevi una lista di persone che si sono trasferite in un'altra città. La lista è composta da persone che si sono trasferite da una città ad un'altra e per ogni persona hai il nome, il cognome, l'età e la città di provenienza.

Ora immagina che per qualche motivo tu riceva una lista con un nome, un cognome, un'età e una città di provenienza che non sono validi. Ad esempio il nome è vuoto, il cognome è vuoto, l'età è negativa e la città di provenienza è vuota.

Cosa faresti in questo caso? Probabilmente non aggiungeresti queste persone al registro anagrafe perchè i dati non sono validi, giusto?

Ecco, questo è quello che fanno le validazioni dei dati. Le validazioni dei dati sono un modo per assicurarsi che i dati che stiamo inserendo nel database siano validi.

Creiamo un nuovo file chiamato car.js che utilizzeremo per fare un esempio pratico di validazione dei dati con un entità Car.

touch car.js (in Windows PowerShell usa il comando "New-Item" al posto di "touch")

In questo file andiamo ad aggiungere il seguente codice

const mongoose = require('mongoose');

mongoose.connect('mongodb://127.0.0.1:27017/cars', { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connesso al database'))
  .catch(err => console.error('Impossibile connettersi al database', err));

A questo punto andiamo a creare uno schema per la nostra entità Car aggiungendo dei campi che rappresentano le informazioni che vogliamo salvare per ogni auto. Per fare ciò andiamo ad aggiungere il seguente codice:

const carSchema = new mongoose.Schema({
  marca: {
    type: String,
    required: true
  },
  modello: String,
  anno: Number,
  colore: String,
  prezzo: Number
});

In questo caso abbiamo aggiunto 5 campi allo schema car ed il campo marca ha delle informazioni aggiuntive rispetto agli altri. Possiamo vedere che abbiamo aggiunto il seguente codice:

marca: {
  type: String,
  required: true
}

Questo ci dice che il campo marca deve essere di tipo String e che è obbligatorio. Quindi se provassimo ad inserire un auto senza il campo marca l'inserimento non andrà a buon fine perchè la validazione dei dati fallirà.

Proviamo ad inserire un auto con tutti i campi necessari e vediamo cosa succede. Per fare ciò andiamo ad aggiungere il seguente codice:

const Car = mongoose.model('Car', carSchema);

const panda = new Car({
  marca: 'Fiat',
  modello: 'Panda',
  anno: 2010,
  colore: 'Bianco',
  prezzo: 5000
});

panda.save().then(data => console.log(data)).catch(err => console.error(err));

In questo caso abbiamo creato un nuovo documento Car e lo abbiamo salvato nel database.

Apriamo la console di Node e facciamo il load del file car.js:

  • Windows: node -i -e "$(< car.js)"
  • Mac: node e poi .load car.js

Come puoi vedere il documento è stato salvato correttamente nel database.

Ora proviamo ad inserire un documento senza il campo marca e vediamo cosa succede. Per fare ciò andiamo ad aggiungere il seguente codice:

const clio = new Car({
  modello: 'Clio',
  anno: 2015,
  colore: 'Bianco',
  prezzo: 4000
});

clio.save().then(data => console.log(data)).catch(err => console.error(err));

Noteremo che i logs ci mostreranno chiaramente un errore e che tipo di errore è stato generato.

Cosí è come possiamo validare i dati che stiamo inserendo nel database.

Mongoose Validations per gli Updates

Le validazioni che abbiamo appena visto funzionano di default solo per i nuovi documenti che stiamo inserendo nel database ma non funzionano per gli updates di documenti già esistenti a meno che non viene esplicitamente specificato.

Per fare un esempio con il registro dell'anagrafe, immagina che una persona si sia trasferita da una città ad un'altra e che tu debba aggiornare il suo indirizzo. Se la persona non ti fornisce il nuovo indirizzo, tu non aggiornerai il suo esistente indirizzo perchè non hai un nuovo indirizzo valido.

Ecco, per fare la stessa cosa con Mongoose abbiamo bisogno di specificare esplicitamente durante l'update che vogliamo validare i dati.

Facciamo un esempio in cui vogliamo aggiornare la marca di un'auto da Fiat a Peugeout:

Car.updateOne({ marca: 'Fiat' }, { marca: "Peugeot" }, {new: true, runValidators: true}).then(data => console.log(data)).catch(err => console.error(err));

Se l'aggiornamento è andato a buon fine vedremo il seguente output:

Ora proviamo a fare il find delle auto Peugout e vediamo se il documento è stato aggiornato correttamente:

Car.find({ marca: 'Peugeot' }).then(data => console.log(data)).catch(err => console.error(err));

Come puoi vedere abbiamo aggiornato la marca da Fiat a Peugeot. Ora abbiamo un'auto Peugeot Panda che nel mondo reale sembra un pò una follia 😄 Sentiti libero di cambiare il modello da Panda ad un modello Peugeot.

Puoi notare che ora non abbiamo più auto con marca Fiat:

Car.find({ marca: 'Fiat' }).then(data => console.log(data)).catch(err => console.error(err));

Nel caso avevi più records di auto con la marca Fiat avresti dovuto usare il metodo updateMany invece di updateOne. Puoi rieseguire il codice per aggiornare la marca da Fiat a Peugeot per gli altri records che hai nel database con marca Fiat e poi controllare con il metodo find che non hai più auto con marca Fiat.

Ora, considerando che abbiamo il campo marca con una validazione required: true, proviamo a fare un update in cui il campo marca è null e vediamo cosa succede:

Car.updateOne({ marca: 'Peugeot' }, { marca: null }, {new: true, runValidators: true}).then(data => console.log(data)).catch(err => console.error(err));

Vedremo che ovviamente l'update fallirà e che il documento non verrà aggiornato.

Conclusione

Complimenti per essere arrivata/o alla fine di questa intensa lezione! 🎉

Per fare un breve recap di quello che abbiamo visto:

  • Abbiamo installato Mongoose e creato una connessione con il database MongoDB
  • Abbiamo creato uno schema per i documenti che vogliamo inserire nel database
  • Abbiamo creato un modello per i documenti che vogliamo inserire nel database
  • Abbiamo eseguito le operazioni CRUD attraverso Mongoose e MongoDB nel nostro database

Abbiamo visto tante tante cose durante questo corso ed è finalmente il momento di mettere in pratica tutto quello che abbiamo imparato!

Nella prossima lezione infatti andremo a creare un'applicazione FullStack con Node JS, Express, MongoDB e Mongoose!! 🚀

Ora ti saluto e ti aspetto nella prossima lezione! 👋