Il CRUD in MongoDB: come gestire i documenti all'interno delle collections, e come utilizzare gli operatori.
Dopo aver visto cos'è Mongo, come si installa in Linux e Windows, e di come aprire la shell, oggi ci occupiamo delle classiche operazioni da effettuare su un database, come l'inserimento, la ricerca, la modifica e la cancellazione di record, che in MongoDB si chiamano documenti.
Cos'è un CRUD
CRUD è l'acronimo di Create, Read, Update, Delete. Vediamo come diventano questi voci MongoDB e confrontiamole con l'equivalente dei database relazionai (RDBMS) come ad esempio MySQL.
Crud | MongoDB | RDBMS |
Create | Insert | Insert |
Read | Find | Select |
Update | Update | Update |
Delete | Remove | Delete |
MongoDB non utilizza un linguaggio SQL per effettuare le query, bensì mette a disposizione dei metodi, ed in supporto di questi degli operatori.
Operatori in MongoDB
Mongo mette a disposizione molti operatori, in aggiunti ai classici AND e OR, e tutti vanno precedenti dal simbolo di dollaro "$".
Vediamoli in rapida rassegna
Nelle query di ricerca:
- $gt e $lt
- $and e $or
- $in e $nin
- $type
- $exists
- $regex
Nelle query di update:
- $set e $unset
- $inc
Nelle query di update su array:
- $pop
- $push
- $pull
- $pullALL
- $addToSet
In generale tutti gli operatori hanno la stessa struttura ad eccezione di $and e $or, per cui capita la logica, sarà semplice il loro utilizzo.
Vedremo in azione i singoli operatori negli esempi che seguiranno.
Il metodo insert: creare nuovi documenti
L'insert serve ad inserire un nuovo record (che in Mongo è un documento di tipo json), all'interno di una tabella (che in Mongo è una collection) del database.
Avviamo mongod, cioè il server di mongo, come abbiamo imparato a fare negli articoli precedenti
mongod --dbpath "C:\Program Files\MongoDB\Server\4.4\data"
Dopodichè apriamo la shell mongo e spostiamoci sul database demo
# mongo
MongoDB shell version v4.4.1
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("b9018304-0861-48ed-a3d8-825ed54c1475") }
MongoDB server version: 4.4.1
---
The server generated these startup warnings when booting:
2020-10-16T08:37:30.231+02:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
2020-10-16T08:37:30.232+02:00: This server is bound to localhost. Remote systems will be unable to connect to this server. Start the server with --bind_ip <address> to specify which IP addresses it should serve responses from, or with --bind_ip_all to bind to all interfaces. If this behavior is desired, start the server with --bind_ip 127.0.0.1 to disable this warning
---
---
Enable MongoDB's free cloud-based monitoring service, which will then receive and display
metrics about your deployment (disk utilization, CPU, operation statistics, etc).
The monitoring data will be available on a MongoDB website with a unique URL accessible to you
and anyone you share the URL with. MongoDB may use this information to make product
improvements and to suggest MongoDB products and deployment options to you.
To enable free monitoring, run the following command: db.enableFreeMonitoring()
To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
---
> use demo
switched to db demo
Con il comando db possiamo sapere in quale database siamo. Ricordiamo ci cliccare sempre invio dopo ogni istruzione.
> db
demo
Abbiamo così la conferma di essere nel database "demo".
Adesso creiamo un documento in javascript
> var doc = {"name":"Mario", "surname":"Rossi", "city":"Torino"}
Se adesso nella shell digitiamo "doc" ci verrà restituito il documento json, ed è perfetto per essere poi salvato nel nostro database
> doc
{ "name" : "Mario", "surname" : "Rossi", "city" : "Torino" }
Per inserire il documento (il record) nella collection (la tabella) "users" del database "demo", procediamo utilizzando il metodo insert
> db.users.insert( doc )
WriteResult({ "nInserted" : 1 })
users è una proprietà del database, mentre insert è un metodo della collection users che riceve come argomento un oggetto json, il nostro doc.
La risposta è stata { "nInserted" : 1 }, ed indica che è stato aggiunto un nuovo documento.
Avremmo potuto tranquillamente inserite la stringa json direttamente dentro la funzione insert, senza quindi prima creare una variabile doc. Proviamo questo secondo insert
> db.users.insert( { "name" : "Angelo", "surname" : "Bianchi", "city" : "Vicenza" } )
WriteResult({ "nInserted" : 1 }))
Proviamo ad estrarre i documenti creati utilizzando il metodo find
> db.users.find()
{ "_id" : ObjectId("5f8941ff96ee60d39728a311"), "name" : "Mario", "surname" : "Rossi", "city" : "Torino" }
{ "_id" : ObjectId("5f8945bf96ee60d39728a312"), "name" : "Angelo", "surname" : "Bianchi", "city" : "Vicenza" }
Mongo ha aggiunto automaticamente l'attributo (chiave) "_id" di tipo "ObjectId", ed è ottenuto unendo tra loro vari valori
- la timestamp
- un identificativo della macchina
- l'id del processo (PID)
- numeri casuali
Serve ad indentificare in modo univoco un documento e quindi è una sorta di chiave primaria. Inoltre è immutabile, cioè il suo valore non è modificabile con un update nella stessa collection.
Questo attributo è obbligatorio in tutti i documenti, ed in una assenza viene automaticamente aggiunto, come nel nostro esempio
I metodi Find e FindOne: cercare documenti
I metodi find e findOne sono simili alle select utilizzate in SQL, e servono rispettivamente a
- find: estrarre tutti i documenti della collection
- findOne: estre solo un documento
Iniziamo con il vedere l'utilizzo di findOne che è silimite ad effettuare un "limit 1" in SQL. Se non passiamo argomenti verrà estratto il primo documento.
> db.users.findOne()
{
"_id" : ObjectId("5f8941ff96ee60d39728a311"),
"name" : "Mario",
"surname" : "Rossi",
"city" : "Torino"
}
Possiamo passare una condizione al metodo, ed è come effettuare un where in SQL. Aggiungiamo quindi un criterio di ricerca.
Ad esempio, voglio estrarre tutti i documenti con la chiave "city" valorizzata con il valore "Vicenza".
> db.users.findOne( {"city":"Vicenza"} )
{
"_id" : ObjectId("5f8945bf96ee60d39728a312"),
"name" : "Angelo",
"surname" : "Bianchi",
"city" : "Vicenza"
}
Come altro parametro, facoltativo, del metodo findOne, possiamo definire quali attributi (chiavi) vogliamo ottenere come risposta, specificando il nome dell'attributo, seguito da "true".
Ad esempio vogliamo ottenere solo il nome
> db.users.findOne( {"city":"Vicenza"}, {"name":true} )
{ "_id" : ObjectId("5f8945bf96ee60d39728a312"), "name" : "Angelo" }
Mongo ci restituisce il "name" e sempre anche l'identificativo "_id" a meno che noi imponiamo di non riceverlo, in questo modo.
> db.users.findOne( {"city":"Vicenza"}, {"name":true, "_id": false} )
{ "name" : "Angelo" }
Ancora un esempio
> db.users.findOne( {"city":"Vicenza"}, {"surname":true, "name":true, "_id": false} )
{ "name" : "Angelo", "surname" : "Bianchi" }
Con findOne andiamo ad estrarre un solo risultato, con find invece andiamo ad estrarre più risultati.
Abbiamo già visto il suo utilizzo, senza condizioni. Prima di testarlo nuovamente, aggiungiamo un nuovo documento
> db.users.insert( { "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" } )
WriteResult({ "nInserted" : 1 })
Adesso cerchiamo tutti i documenti
> db.users.find()
{ "_id" : ObjectId("5f8941ff96ee60d39728a311"), "name" : "Mario", "surname" : "Rossi", "city" : "Torino" }
{ "_id" : ObjectId("5f8945bf96ee60d39728a312"), "name" : "Angelo", "surname" : "Bianchi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f894b2fc517274edb561887"), "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" }
Aggiungiamo la condizione di ricerca "city" con valore "Vicenza", ed estraiamo solo "name" e "surname"
> db.users.find( {"city":"Vicenza"}, {"surname":true, "name":true, "_id": false} )
{ "name" : "Angelo", "surname" : "Bianchi" }
{ "name" : "Maria", "surname" : "Verdi" }
Cambiamo esempio perchè per le nostre prove abbiamo bisogno di molti documenti.
Creiamo una nuova collection chiamata "punteggio", che verrà popolato con i punteggi di 1000 studenti, ottenuti in una prova, un esame, o un quiz.
Creiamo in javascript
- un array di nome "tipo" i cui valori sono prova, esame e quiz
- un primo ciclo for per i 1000 studenti
- al suo interno un second ciclo for eseguito 3 volte (per le 3 tipologie di punteggio), ed ogni volta creiamo un documento in cui inseritamo un identificativo dello studente utilizzando l'indice "i" del primo ciclo, il tipo di punteggio, ed un punteggio random
var tipo = ["esame","prova","quiz"];
for (var i=0; i<1000; i++){
for (var j=0; j<3; j++){
db.punteggio.insert( {"studente":i, "tipo": tipo[j], "score":Math.round(Math.random()*100) } );
}
}
Adesso passiamo tutto questo in una unica riga, nella shell mongo
> var tipo = ["esame","prova","quiz"]; for (var i=0; i<1000; i++){for (var j=0; j<3; j++){db.punteggio.insert( {"studente":i, "tipo": tipo[j], "score":Math.round(Math.random()*100) } ); }}
WriteResult({ "nInserted" : 1 })
Abbiamo inserito 3000 documenti! Adesso estrai tutti i dati
> db.punteggio.find()
{ "_id" : ObjectId("5f8950aeb0344625fab8680e"), "studente" : 0, "tipo" : "esame", "score" : 39 }
{ "_id" : ObjectId("5f8950aeb0344625fab8680f"), "studente" : 0, "tipo" : "prova", "score" : 79 }
{ "_id" : ObjectId("5f8950aeb0344625fab86810"), "studente" : 0, "tipo" : "quiz", "score" : 27 }
{ "_id" : ObjectId("5f8950aeb0344625fab86811"), "studente" : 1, "tipo" : "esame", "score" : 55 }
{ "_id" : ObjectId("5f8950aeb0344625fab86812"), "studente" : 1, "tipo" : "prova", "score" : 100 }
{ "_id" : ObjectId("5f8950aeb0344625fab86813"), "studente" : 1, "tipo" : "quiz", "score" : 60 }
{ "_id" : ObjectId("5f8950aeb0344625fab86814"), "studente" : 2, "tipo" : "esame", "score" : 12 }
{ "_id" : ObjectId("5f8950aeb0344625fab86815"), "studente" : 2, "tipo" : "prova", "score" : 53 }
{ "_id" : ObjectId("5f8950aeb0344625fab86816"), "studente" : 2, "tipo" : "quiz", "score" : 65 }
{ "_id" : ObjectId("5f8950aeb0344625fab86817"), "studente" : 3, "tipo" : "esame", "score" : 54 }
{ "_id" : ObjectId("5f8950aeb0344625fab86818"), "studente" : 3, "tipo" : "prova", "score" : 68 }
{ "_id" : ObjectId("5f8950aeb0344625fab86819"), "studente" : 3, "tipo" : "quiz", "score" : 81 }
{ "_id" : ObjectId("5f8950aeb0344625fab8681a"), "studente" : 4, "tipo" : "esame", "score" : 21 }
{ "_id" : ObjectId("5f8950aeb0344625fab8681b"), "studente" : 4, "tipo" : "prova", "score" : 27 }
{ "_id" : ObjectId("5f8950aeb0344625fab8681c"), "studente" : 4, "tipo" : "quiz", "score" : 90 }
{ "_id" : ObjectId("5f8950aeb0344625fab8681d"), "studente" : 5, "tipo" : "esame", "score" : 62 }
{ "_id" : ObjectId("5f8950aeb0344625fab8681e"), "studente" : 5, "tipo" : "prova", "score" : 9 }
{ "_id" : ObjectId("5f8950aeb0344625fab8681f"), "studente" : 5, "tipo" : "quiz", "score" : 26 }
{ "_id" : ObjectId("5f8950aeb0344625fab86820"), "studente" : 6, "tipo" : "esame", "score" : 30 }
{ "_id" : ObjectId("5f8950aeb0344625fab86821"), "studente" : 6, "tipo" : "prova", "score" : 71 }
Type "it" for more
Verranno visualizzati i primi 20. Per vedere i 20 successivi digita "it" come indicato dalla shell...
> it
{ "_id" : ObjectId("5f8950aeb0344625fab86822"), "studente" : 6, "tipo" : "quiz", "score" : 59 }
{ "_id" : ObjectId("5f8950aeb0344625fab86823"), "studente" : 7, "tipo" : "esame", "score" : 82 }
{ "_id" : ObjectId("5f8950aeb0344625fab86824"), "studente" : 7, "tipo" : "prova", "score" : 3 }
{ "_id" : ObjectId("5f8950aeb0344625fab86825"), "studente" : 7, "tipo" : "quiz", "score" : 26 }
{ "_id" : ObjectId("5f8950aeb0344625fab86826"), "studente" : 8, "tipo" : "esame", "score" : 20 }
{ "_id" : ObjectId("5f8950aeb0344625fab86827"), "studente" : 8, "tipo" : "prova", "score" : 7 }
{ "_id" : ObjectId("5f8950aeb0344625fab86828"), "studente" : 8, "tipo" : "quiz", "score" : 39 }
{ "_id" : ObjectId("5f8950aeb0344625fab86829"), "studente" : 9, "tipo" : "esame", "score" : 76 }
{ "_id" : ObjectId("5f8950aeb0344625fab8682a"), "studente" : 9, "tipo" : "prova", "score" : 19 }
{ "_id" : ObjectId("5f8950aeb0344625fab8682b"), "studente" : 9, "tipo" : "quiz", "score" : 27 }
{ "_id" : ObjectId("5f8950aeb0344625fab8682c"), "studente" : 10, "tipo" : "esame", "score" : 92 }
{ "_id" : ObjectId("5f8950aeb0344625fab8682d"), "studente" : 10, "tipo" : "prova", "score" : 14 }
{ "_id" : ObjectId("5f8950aeb0344625fab8682e"), "studente" : 10, "tipo" : "quiz", "score" : 73 }
{ "_id" : ObjectId("5f8950aeb0344625fab8682f"), "studente" : 11, "tipo" : "esame", "score" : 57 }
{ "_id" : ObjectId("5f8950aeb0344625fab86830"), "studente" : 11, "tipo" : "prova", "score" : 41 }
{ "_id" : ObjectId("5f8950aeb0344625fab86831"), "studente" : 11, "tipo" : "quiz", "score" : 98 }
{ "_id" : ObjectId("5f8950aeb0344625fab86832"), "studente" : 12, "tipo" : "esame", "score" : 79 }
{ "_id" : ObjectId("5f8950aeb0344625fab86833"), "studente" : 12, "tipo" : "prova", "score" : 73 }
{ "_id" : ObjectId("5f8950aeb0344625fab86834"), "studente" : 12, "tipo" : "quiz", "score" : 79 }
{ "_id" : ObjectId("5f8950aeb0344625fab86835"), "studente" : 13, "tipo" : "esame", "score" : 65 }
Type "it" for more
E così via.
Per avere una visualizzazione più leggibile utilizziamo il metodo pretty
> db.punteggio.find().pretty()
{
"_id" : ObjectId("5f8950aeb0344625fab8680e"),
"studente" : 0,
"tipo" : "esame",
"score" : 39
}
{
"_id" : ObjectId("5f8950aeb0344625fab8680f"),
"studente" : 0,
"tipo" : "prova",
"score" : 79
}
{
"_id" : ObjectId("5f8950aeb0344625fab86810"),
"studente" : 0,
"tipo" : "quiz",
"score" : 27
}
...............
Ho mostrato solo i primi 3 documenti per rendere il risultato leggibile altrimenti avreste dovuto scorrere di molto la pagina .
Adesso specifichiamo delle condizioni di ricerca.
Ad esempio proviamo ad estrarre lo studente 19: abbiamo 3 documenti
> db.punteggio.find( {"studente":19} ).pretty()
{
"_id" : ObjectId("5f8950aeb0344625fab86847"),
"studente" : 19,
"tipo" : "esame",
"score" : 48
}
{
"_id" : ObjectId("5f8950aeb0344625fab86848"),
"studente" : 19,
"tipo" : "prova",
"score" : 19
}
{
"_id" : ObjectId("5f8950aeb0344625fab86849"),
"studente" : 19,
"tipo" : "quiz",
"score" : 63
}
Possiamo combiare più condizioni concatenadole con la virgola.
Ad esempio cerchiamo lo studente "19" e il tipo "quiz": abbiamo un solo documento
> db.punteggio.find( {"studente":19, "tipo":"quiz"} ).pretty()
{
"_id" : ObjectId("5f8950aeb0344625fab86849"),
"studente" : 19,
"tipo" : "quiz",
"score" : 63
}
Specifichiamo anche quali attributi restituire e non restituire
> db.punteggio.find( {"studente":19, "tipo":"quiz"}, {"score":true,"_id":false} ).pretty()
{ "score" : 63 }
Nota: le chiavi le inserisco sempre tra virgolette, in quanto solo sempre stringhe e in json necessitano di virgolette. Tuttavia Mongo consente di inserire anche senza, purchè non inizino con un numero
Per cui avremmo potuto scrivere
> db.punteggio.find( {studente:19, tipo:"quiz"}, {score:true,_id:false} ).pretty()
{ "score" : 63 }
L'operatore $gt e $lt
I primi operatori che vediamo sono $gt e $lt
- $gt: significa "grather than", cioè "maggiore di"
- $lt: significa "lower than", cioè "minore di"
Vediamoli subito in azione: cerchiamo tutti i documenti che hanno uno" score" > 95
Nnel metodo find si indica la chiave "score" seguito dalla condizione, sempre tra parentesi graffe: possiamo infatti considerare la condizione come un sottodocumento dell'attributo "score" ... quindi occhio alle graffe!
> db.punteggio.find( { "score": { $gt : 95 } } )
{ "_id" : ObjectId("5f8974ead04019ba8686d7a6"), "studente" : 23, "tipo" : "esame", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d7c5"), "studente" : 33, "tipo" : "prova", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686d7db"), "studente" : 40, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686d7fa"), "studente" : 51, "tipo" : "esame", "score" : 99 }
{ "_id" : ObjectId("5f8974ead04019ba8686d7fc"), "studente" : 51, "tipo" : "quiz", "score" : 99 }
{ "_id" : ObjectId("5f8974ead04019ba8686d82a"), "studente" : 67, "tipo" : "esame", "score" : 98 }
{ "_id" : ObjectId("5f8974ead04019ba8686d82d"), "studente" : 68, "tipo" : "esame", "score" : 99 }
{ "_id" : ObjectId("5f8974ead04019ba8686d83f"), "studente" : 74, "tipo" : "esame", "score" : 100 }
{ "_id" : ObjectId("5f8974ead04019ba8686d856"), "studente" : 81, "tipo" : "quiz", "score" : 100 }
{ "_id" : ObjectId("5f8974ead04019ba8686d876"), "studente" : 92, "tipo" : "prova", "score" : 99 }
{ "_id" : ObjectId("5f8974ead04019ba8686d877"), "studente" : 92, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d88c"), "studente" : 99, "tipo" : "quiz", "score" : 98 }
{ "_id" : ObjectId("5f8974ead04019ba8686d8a9"), "studente" : 109, "tipo" : "prova", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d8b7"), "studente" : 114, "tipo" : "esame", "score" : 98 }
{ "_id" : ObjectId("5f8974ead04019ba8686d8bc"), "studente" : 115, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d8c6"), "studente" : 119, "tipo" : "esame", "score" : 99 }
{ "_id" : ObjectId("5f8974ead04019ba8686d8c7"), "studente" : 119, "tipo" : "prova", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d8f3"), "studente" : 134, "tipo" : "esame", "score" : 98 }
{ "_id" : ObjectId("5f8974ead04019ba8686d902"), "studente" : 139, "tipo" : "esame", "score" : 98 }
{ "_id" : ObjectId("5f8974ead04019ba8686d922"), "studente" : 149, "tipo" : "quiz", "score" : 97 }
Type "it" for more
>
E visto che siamo già bravi, aggiungiamo un secondo criterio di ricerca, andando ad estrarre solo il "tipo" con valore "quiz".
Le condizioni vanno concatenate con una virgola all'interno della parentesi graffa che contiene le condizioni.
> db.punteggio.find( { "score":{$gt:95}, "tipo":"quiz" } )
{ "_id" : ObjectId("5f8974ead04019ba8686d7db"), "studente" : 40, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686d7fc"), "studente" : 51, "tipo" : "quiz", "score" : 99 }
{ "_id" : ObjectId("5f8974ead04019ba8686d856"), "studente" : 81, "tipo" : "quiz", "score" : 100 }
{ "_id" : ObjectId("5f8974ead04019ba8686d877"), "studente" : 92, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d88c"), "studente" : 99, "tipo" : "quiz", "score" : 98 }
{ "_id" : ObjectId("5f8974ead04019ba8686d8bc"), "studente" : 115, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d922"), "studente" : 149, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686d92b"), "studente" : 152, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d931"), "studente" : 154, "tipo" : "quiz", "score" : 99 }
{ "_id" : ObjectId("5f8974ead04019ba8686d982"), "studente" : 181, "tipo" : "quiz", "score" : 100 }
{ "_id" : ObjectId("5f8974ead04019ba8686d9be"), "studente" : 201, "tipo" : "quiz", "score" : 98 }
{ "_id" : ObjectId("5f8974ead04019ba8686da06"), "studente" : 225, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686da0c"), "studente" : 227, "tipo" : "quiz", "score" : 100 }
{ "_id" : ObjectId("5f8974ead04019ba8686da15"), "studente" : 230, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686da69"), "studente" : 258, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686dae7"), "studente" : 300, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686daf0"), "studente" : 303, "tipo" : "quiz", "score" : 98 }
{ "_id" : ObjectId("5f8974ead04019ba8686db29"), "studente" : 322, "tipo" : "quiz", "score" : 98 }
{ "_id" : ObjectId("5f8974ead04019ba8686db59"), "studente" : 338, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686db62"), "studente" : 341, "tipo" : "quiz", "score" : 100 }
Type "it" for more
Passiamo adesso all'operatore $lt che opera in modo analogo ma visualizza i risultati minori di un certo valore. Lascio a voi provare lo stesso esempio precedente con la condizione minore di.
Adesso effettuiamo una ricerca all'interno di un intervallo, ad esempio > 93 e < 98. Utilizziamo entrambi gli operatori all'interno dello stesso attributo "score".
> db.punteggio.find( { "score":{$gt:95, $lt:98}, "tipo":"quiz"} )
{ "_id" : ObjectId("5f8974ead04019ba8686d7db"), "studente" : 40, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686d877"), "studente" : 92, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d8bc"), "studente" : 115, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686d922"), "studente" : 149, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686d92b"), "studente" : 152, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686da06"), "studente" : 225, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686da15"), "studente" : 230, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686da69"), "studente" : 258, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686dae7"), "studente" : 300, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686db59"), "studente" : 338, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ead04019ba8686dc10"), "studente" : 399, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686dc70"), "studente" : 431, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686dc97"), "studente" : 444, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ead04019ba8686dcb2"), "studente" : 453, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ebd04019ba8686de0b"), "studente" : 568, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ebd04019ba8686ded7"), "studente" : 636, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ebd04019ba8686df8b"), "studente" : 696, "tipo" : "quiz", "score" : 96 }
{ "_id" : ObjectId("5f8974ebd04019ba8686e09f"), "studente" : 788, "tipo" : "quiz", "score" : 97 }
{ "_id" : ObjectId("5f8974ebd04019ba8686e288"), "studente" : 951, "tipo" : "quiz", "score" : 96 }
Type "it" for more
Visto che siamo bravi e non vogliamo visualizzare le chiave "_id" nel risultato della ricerca, chiusa la graffa (cioè il sottodocumento) che contiene le condizioni, aggiungiamo un'altra graffa (un nuovo sottodocumento) in cui chiediamo di non visualizzare la chiave "_id"
> db.punteggio.find( { "score":{$gt:95, $lt:98}, "tipo":"quiz"}, {"_id": false } )
{ "studente" : 40, "tipo" : "quiz", "score" : 97 }
{ "studente" : 92, "tipo" : "quiz", "score" : 96 }
{ "studente" : 115, "tipo" : "quiz", "score" : 96 }
{ "studente" : 149, "tipo" : "quiz", "score" : 97 }
{ "studente" : 152, "tipo" : "quiz", "score" : 96 }
{ "studente" : 225, "tipo" : "quiz", "score" : 96 }
{ "studente" : 230, "tipo" : "quiz", "score" : 97 }
{ "studente" : 258, "tipo" : "quiz", "score" : 97 }
{ "studente" : 300, "tipo" : "quiz", "score" : 97 }
{ "studente" : 338, "tipo" : "quiz", "score" : 97 }
{ "studente" : 399, "tipo" : "quiz", "score" : 96 }
{ "studente" : 431, "tipo" : "quiz", "score" : 96 }
{ "studente" : 444, "tipo" : "quiz", "score" : 96 }
{ "studente" : 453, "tipo" : "quiz", "score" : 96 }
{ "studente" : 568, "tipo" : "quiz", "score" : 96 }
{ "studente" : 636, "tipo" : "quiz", "score" : 96 }
{ "studente" : 696, "tipo" : "quiz", "score" : 96 }
{ "studente" : 788, "tipo" : "quiz", "score" : 97 }
{ "studente" : 951, "tipo" : "quiz", "score" : 96 }
Type "it" for more
Gli operatori $gt e $lt non considerano gli estermi, nel nostro caso i valori 95 e 98. Per includerli modifichiamo gli operatori aggiungengo una "e"
> db.punteggio.find( { "score":{$gte:95, $lte:98}, "tipo":"quiz"}, {"_id": false } )
{ "studente" : 40, "tipo" : "quiz", "score" : 97 }
{ "studente" : 78, "tipo" : "quiz", "score" : 95 }
{ "studente" : 86, "tipo" : "quiz", "score" : 95 }
{ "studente" : 92, "tipo" : "quiz", "score" : 96 }
{ "studente" : 99, "tipo" : "quiz", "score" : 98 }
{ "studente" : 115, "tipo" : "quiz", "score" : 96 }
{ "studente" : 122, "tipo" : "quiz", "score" : 95 }
{ "studente" : 149, "tipo" : "quiz", "score" : 97 }
{ "studente" : 152, "tipo" : "quiz", "score" : 96 }
{ "studente" : 201, "tipo" : "quiz", "score" : 98 }
{ "studente" : 225, "tipo" : "quiz", "score" : 96 }
{ "studente" : 230, "tipo" : "quiz", "score" : 97 }
{ "studente" : 235, "tipo" : "quiz", "score" : 95 }
{ "studente" : 258, "tipo" : "quiz", "score" : 97 }
{ "studente" : 300, "tipo" : "quiz", "score" : 97 }
{ "studente" : 303, "tipo" : "quiz", "score" : 98 }
{ "studente" : 322, "tipo" : "quiz", "score" : 98 }
{ "studente" : 338, "tipo" : "quiz", "score" : 97 }
{ "studente" : 383, "tipo" : "quiz", "score" : 95 }
{ "studente" : 399, "tipo" : "quiz", "score" : 96 }
Type "it" for more
Abbiamo visto come operatore questi primi due operatori con i numeri.
E' possibile usarli anche con le stringhe, anche se non è raccomandato: vediamo come.
Torniamo all'esempio iniziale con i nomi e cognomi, dove avevamo caricato "Mario","Angelo" e "Maria", e quindi alla collection users, ed aggiungiamo altri nomi.
> db.users.insert( { "name" : "Roberto", "surname" : "Gialli", "city" : "Milano" } )
> db.users.insert( { "name" : "Sandra", "surname" : "Bullock", "city" : "Venezia" } )
> db.users.insert( { "name" : "Alberto", "surname" : "Rossi", "city" : "Roma" } )
Proviamo ad usare la condizione "maggiore di" applicato alla chiave "name"
> db.users.find( { "name":{$gt:"M"}} )
Ci aspettiamo tutti i nomi con lettera maggiore di M, invece il risultato è questo
{ "_id" : ObjectId("5f897bdbd04019ba8686e319"), "name" : "Mario", "surname" : "Rossi", "city" : "Torino" }
{ "_id" : ObjectId("5f897be3d04019ba8686e31b"), "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897d58d04019ba8686e31c"), "name" : "Roberto", "surname" : "Gialli", "city" : "Milano" }
{ "_id" : ObjectId("5f897d5dd04019ba8686e31d"), "name" : "Sandra", "surname" : "Bullock", "city" : "Venezia" }
Ha escludo i nomi con lettera minore di "M", ma ci sono anche i nomi che iniziano con la "M", anche se non ho indicato "$gte" !
Questo perchè, se utilizziamo questi operatori sulle stringhe, Mongo utilizza l'ordinamento in base alla rappresentazione dei byte dell'UTF8, per cui vediamo anche "Mario" perchè la parola "Mario" è considerata maggiore della parola "M", perchè contiene più lettere.
Utilizziamo assieme le condizioni "minore di" e "maggiore di"
> db.users.find( { "name":{$lt:"R", $gt:"B"}} )
{ "_id" : ObjectId("5f897bdbd04019ba8686e319"), "name" : "Mario", "surname" : "Rossi", "city" : "Torino" }
{ "_id" : ObjectId("5f897be3d04019ba8686e31b"), "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" }
L'operatore $exists
Questo operatore consente di verificare l'esistenza di una chiave nella collection.
Torniamo all'esempio della collection "users" e aggiungiamo un nuovo utente
> db.users.insert( { "name" : "Carla" } )
WriteResult({ "nInserted" : 1 })
Mongo ha accettato l'inserimento, anche se mancano i campi "surname" e "city". Questo perchè Mongo accetta documenti con strutture differenti.
Per estrarre tutti i documenti che hanno l'attributo "city" nella loro struttura, utilizziamo l'operatore $exists.
Come per il caso dei due operatori visti nel paragrafo precedente, anche adesso indichiamo la chiave sulla quale vogliamo applicare l'operatore, poi apriamo una graffa, ed inseriamo l'operatore come coppia chiave/valore. Utilizziamo il valore "true"
> db.users.find( { "city":{$exists:true}} )
{ "_id" : ObjectId("5f897bdbd04019ba8686e319"), "name" : "Mario", "surname" : "Rossi", "city" : "Torino" }
{ "_id" : ObjectId("5f897be0d04019ba8686e31a"), "name" : "Angelo", "surname" : "Bianchi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897be3d04019ba8686e31b"), "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897d58d04019ba8686e31c"), "name" : "Roberto", "surname" : "Gialli", "city" : "Milano" }
{ "_id" : ObjectId("5f897d5dd04019ba8686e31d"), "name" : "Sandra", "surname" : "Bullock", "city" : "Venezia" }
{ "_id" : ObjectId("5f897eb2d04019ba8686e31e"), "name" : "Alberto", "surname" : "Rossi", "city" : "Roma" }
Se indichiamo "false" avremo l'unico documento senza città
> db.users.find( { "city":{$exists:false}} )
{ "_id" : ObjectId("5f898375d04019ba8686e31f"), "name" : "Carla" }
L'operatore $type
Con questo operatore possiamo chiedere se un attributo (una chiave) sia di un tipo specifico.
Per indicare la tipologia occorre indicare una numerazione prevista da BSON ... se non sai cos'è BSON, l'abbiamo introdotto nell'articolo precedente.
Ad esempio:
- 2: indica le stringhe
- 4: indica un array
- 9: indica una data
- 10: per indicare un null
Per lista completa, a fondo articolo, ho postato il link alla documentazione ufficiale.
Aggiungiamo un nuovo documento, dove come nome mettiamo un numero... si, un numero.
> db.users.insert( { "name" : 20 } )
WriteResult({ "nInserted" : 1 })
MongoDB è una struttura dinamica per cui una chiave può assumere i valori che vogliamo all'interno della stessa collection.
Bene, adesso andiamo ad estrarre tutti i soli documenti in chi la chiave "name" sia una stringa
> db.users.find( { "name":{$type:2}} )
{ "_id" : ObjectId("5f897bdbd04019ba8686e319"), "name" : "Mario", "surname" : "Rossi", "city" : "Torino" }
{ "_id" : ObjectId("5f897be0d04019ba8686e31a"), "name" : "Angelo", "surname" : "Bianchi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897be3d04019ba8686e31b"), "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897d58d04019ba8686e31c"), "name" : "Roberto", "surname" : "Gialli", "city" : "Milano" }
{ "_id" : ObjectId("5f897d5dd04019ba8686e31d"), "name" : "Sandra", "surname" : "Bullock", "city" : "Venezia" }
{ "_id" : ObjectId("5f897eb2d04019ba8686e31e"), "name" : "Alberto", "surname" : "Rossi", "city" : "Roma" }
{ "_id" : ObjectId("5f898375d04019ba8686e31f"), "name" : "Carla" }
Sono stati estratti tutti i documenti, tranne l'ultimo caricato, in cui il "name" era un numero
L'operatore $regex
Questo operatore è utilizzato per le espressioni regolari. Come, forse, già sapete, le espressioni regolari consentono di effettuare controlli, ricerche, sulle stringhe, in base a regole o modelli.
Ad esempio, possiamo cercare tutte le stringhe che iniziano, o finiscono, con una determinata lettera.
Con le regular expression, è possibile creare regole molto complesse, così come un po complessa è la loro spiegazione, che esula dall'argomento di oggi. Oggi vedremo solo qualche esempio di utilizzo in Mongo.
Per cercare tutte le chiavi "name" che contengono la lettera "e" minuscola, utilizziamo $regex con una sintassi che sarà, oramai, chiara.
> db.users.find( { "name":{$regex:"e"}} )
{ "_id" : ObjectId("5f897be0d04019ba8686e31a"), "name" : "Angelo", "surname" : "Bianchi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897d58d04019ba8686e31c"), "name" : "Roberto", "surname" : "Gialli", "city" : "Milano" }
{ "_id" : ObjectId("5f897eb2d04019ba8686e31e"), "name" : "Alberto", "surname" : "Rossi", "city" : "Roma" }
Se voglio cercare i "name" che terminano con la lettera "a" aggiungiamo alla "a" il dollaro "$". Il dollaro fa parte della simbologia delle espressioni regolari, ed indica la fine della stringa.
> db.users.find( { "name":{$regex:"a$"}} )
{ "_id" : ObjectId("5f897be3d04019ba8686e31b"), "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897d5dd04019ba8686e31d"), "name" : "Sandra", "surname" : "Bullock", "city" : "Venezia" }
{ "_id" : ObjectId("5f898375d04019ba8686e31f"), "name" : "Carla" }
Il simbolo dell'accento circonflesso "^" nelle espressioni regolari indica l'inizio della stringa.
Per cercare tutti i "name" che iniziano per la lettera "M" utilizziamo $regex come segue
> db.users.find( { "name":{$regex:"^M"}} )
{ "_id" : ObjectId("5f897bdbd04019ba8686e319"), "name" : "Mario", "surname" : "Rossi", "city" : "Torino" }
{ "_id" : ObjectId("5f897be3d04019ba8686e31b"), "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" }
Gli operatori $and e $or
Questi operatori consentono di combinare più condizioni tra loro nella query.
- Con l'operatore $or è sufficiente che una sola delle condizioni sia verificata
- Con l'operatore $and tutte le condizioni devono essere verificate
Finora abbiamo visto gli operatori utilizzati come sottodocumenti dell'attributo. Questi due nuovi attributi invece si utilizzano, e scrivono, in modo diverso.
Inziamo dall'operatore "or": la struttura della query sarà questa.
> db.users.find( { $or:[{...},{...},{...}] } )
Dopo aver aperto la parentesi graffa dell'oeggtto json, indichiamo subito l'operatore $or, che sarà un array, quindi con parentesi quadre, contenente le varie condizioni da combinare, queste ultime come sempre tra graffe.
Ad esempio vogliamo estrarre tutti i documenti il cui "name" finisce con la lettera "a" e con la chiave "city" esistente. Occhio alle graffe!
> db.users.find( { $or:[{"name":{$regex:"a$"}},{ "city":{$exists:true}}] } )
{ "_id" : ObjectId("5f897bdbd04019ba8686e319"), "name" : "Mario", "surname" : "Rossi", "city" : "Torino" }
{ "_id" : ObjectId("5f897be0d04019ba8686e31a"), "name" : "Angelo", "surname" : "Bianchi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897be3d04019ba8686e31b"), "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897d58d04019ba8686e31c"), "name" : "Roberto", "surname" : "Gialli", "city" : "Milano" }
{ "_id" : ObjectId("5f897d5dd04019ba8686e31d"), "name" : "Sandra", "surname" : "Bullock", "city" : "Venezia" }
{ "_id" : ObjectId("5f897eb2d04019ba8686e31e"), "name" : "Alberto", "surname" : "Rossi", "city" : "Roma" }
{ "_id" : ObjectId("5f898375d04019ba8686e31f"), "name" : "Carla" }
>
Abbiamo detto che, con l'operatore "$or" è sufficiente che una sola delle condizioni sia verificata, ecco perchè, ad esempio, appare l'ultimo documento.
Con l'operatore "$and" tutte le condizioni sono verificate, per cui se ripetiamo la stessa query con questa condizione, il risultato sarà questo:
> db.users.find( { $and:[{"name":{$regex:"a$"}},{ "city":{$exists:true}}] } )
{ "_id" : ObjectId("5f897be3d04019ba8686e31b"), "name" : "Maria", "surname" : "Verdi", "city" : "Vicenza" }
{ "_id" : ObjectId("5f897d5dd04019ba8686e31d"), "name" : "Sandra", "surname" : "Bullock", "city" : "Venezia" }
Il medesimo risultato avrei potuto ottenerlo anche con le conoscenze precedenti all'introduzione di questo operatore, così:
> db.users.find( { "name":{$regex:"a$"}, "city":{$exists:true} } )
Di norma, per questioni di performance, si preferisce usare la seconda struttura.
Dati con valori di tipo array
Fino ad adesso abbiamo visto semplici strutture di documenti. Vediamo qualcosa di più complesso, inserendo dei valori di tipo array nei documenti.
Creiamo una nuova collezione chiamata "articoli" ed inseriamo alcuni documenti, con due chiavi, il "titolo" come stringa, e i "tags" come array.
> db.articoli.insert( { "titolo":"Primo articolo", "tags":["php","webservice","funzioni"] } )
> db.articoli.insert( { "titolo":"Secondo articolo", "tags":["mysql","database","query"] } )
> db.articoli.insert( { "titolo":"Terzo articolo", "tags":["nosql","mongodb","memcached"] } )
> db.articoli.insert( { "titolo":"Quarto articolo", "tags":["nosql","redis","php","database"] } )
Cerchiamo tutti i documenti che contengono "nosql" nei "tags", ed effettuiamo la ricerca come se i valori dei tags fossero comuni stringhe e non array
> db.articoli.find( { "tags":"nosql" } )
{ "_id" : ObjectId("5f89c2a79f4a292dbe4863e6"), "titolo" : "Terzo articolo", "tags" : [ "nosql", "mongodb", "memcached" ] }
{ "_id" : ObjectId("5f89c61f9f4a292dbe4863e7"), "titolo" : "Quarto articolo", "tags" : [ "nosql", "redis", "php", "database" ] }
MongoDB come prima cosa, verifica se esiste un valore "nosql" come stringa, e se non la trova, cerca tra i singoli elementi dell'array.
Tuttavia in questa ricerca c'è un limite: se avessimo un array dentro un array, Mongo si fermerebbe solo al primo livello di array. Mongo infatti non offre nessun operatore per effettuare la ricerca ricorsiva oltre al primo livello di profondità, per una questione di performance.
Adesso vediamo tre operatori che consentono ricerce negli array: $all, $in e $nin
Gli operatori $all, $in, $nin
Vediamo come effettuare ricerce più complesse tra valori di tipo array.
Vogliamo cercare tutti i documenti che hanno come tag sia "database" sia "query": utilizziamo l'operatore $all seguito dalla lista di elementi da cercare
> db.articoli.find( { "tags":{$all:["database","query"]} } )
{ "_id" : ObjectId("5f89c2a49f4a292dbe4863e5"), "titolo" : "Secondo articolo", "tags" : [ "mysql", "database", "query" ] }
La ricerca va a buon fine se all'interno dei tag almeno quelli cercati, tutti, siano presenti.
Se invece ci basta verificare la presenza di almeno un elemento, dobbiamo utilizzare l'operatore $in seguito dalla lista di elementi da cercare
> db.articoli.find( { "tags":{$in:["webservice","router"]} } )
{ "_id" : ObjectId("5f89c29e9f4a292dbe4863e4"), "titolo" : "Primo articolo", "tags" : [ "php", "webservice", "funzioni" ] }
Come vediamo in questo documento è presente "webservice", ma non "router": per l'operatore $in è sufficiente la presenza di un elemento.
Questo operatore, lo possiamo usare anche per ricerche nelle chiavi di tipo stringa, ad esempio effettuiamo una ricerca nella chiave "titolo"
> db.articoli.find( { "titolo":{$in:["Primo articolo","Ultimo"]} } )
{ "_id" : ObjectId("5f89c29e9f4a292dbe4863e4"), "titolo" : "Primo articolo", "tags" : [ "php", "webservice", "funzioni" ] }
Mentre l'operatore $in cerca almeno un valore nella chiave di ricerca, l'operatore $nin è la negazione di $in, per cui andiamo ad escludere dalla ricerca quei documenti che verificano certe condizioni.
Ad esempio se NON voglio estrarre i documenti che come tags abbiamo o "php" o "database":
> db.articoli.find( { "tags":{$nin:["php","database"]} } )
{ "_id" : ObjectId("5f89c2a79f4a292dbe4863e6"), "titolo" : "Terzo articolo", "tags" : [ "nosql", "mongodb", "memcached" ] }
Interagire con i sotto documenti
Fino ad adesso abbiamo visto come effettuare le query con vari tipo di dati: numerici, stringhe o array. Vediamo adesso come gestire i sottodocumenti.
Creiamo una nuova collection chiamata "contatti" dove, oltre al nome della persona, salviamo i contatti skype ed email.
> db.contatti.insert( { "name":"Mario", "contacts":{"email":"mario@libero.it", "skype":"mario.rossi"} } )
> db.contatti.insert( { "name":"Luca", "contacts":{"email":"luca@gmail.com", "skype":"luca.verdi"} } )
> db.contatti.insert( { "name":"Stefania", "contacts":{"email":"stefy@libero.it", "skype":"stefy.andrione"} } )
La chiave "contacts" è un sottodocumento, e quindi è racchiuso tra parentesi graffe.
Adesso vogliamo cercare nel sottodocumento "contacts" chi ha come account "skype" il valore "luca.verdi"
Se nella query scrivessi il sottodocumento completo, esattamente come l'abbiamo scritto sopra, la query avrebbe successo
> db.contatti.find({ "contacts":{"email":"luca@gmail.com", "skype":"luca.verdi"} })
{ "_id" : ObjectId("5f89cb3e9f4a292dbe4863e9"), "nome" : "Luca", "contacts" : { "email" : "luca@gmail.com", "skype" : "luca.verdi" } }
Chiaramente non faremo mai una ricerca di questo tipo.
Ma proviamo adesso ad invertire "skype" ed "email"
> db.contatti.find({ "contacts":{"skype":"luca.verdi","email":"luca@gmail.com"} })
La ricerca non produrrebbe alcun risultato perchè cercherebbe tutti i documenti che contengono l'attributo "contacts" che è un sottodocumento con il valore esatto
"skype":"luca.verdi","email":"luca@gmail.com"
Idem se cerchiamo solo "skype"
> db.contatti.find({ "contacts":{"skype":"luca.verdi"} })
Mongo infatti cercherebbe tutti i documenti che contengono l'attributo "contacts" che è un sottodocumento che contiene esattamente
{"skype":"luca.verdi"}
Come possiamo quindi accedere agli attributi di un sottodocumento?
Va utilizzata una sintassi speciale, con il punto. La ricerca diventa la seguente, ed estrare il risultato atteso
> db.contatti.find({ "contacts.skype":"luca.verdi" })
{ "_id" : ObjectId("5f89cb3e9f4a292dbe4863e9"), "nome" : "Luca", "contacts" : { "email" : "luca@gmail.com", "skype" : "luca.verdi" } }
Considerando che i documenti sono oggetti, come visto nell'articolo in cui abbiamo introdotto l'argomento MongoDB, il punto risulta adesso comprensibile!
L'oggetto cursore: limit, sort, skip, count
Quando lanciamo il comando find, viene costruito un oggetto di tipo cursore, che contatta il database e immagazzina la risposta (la lista di documenti).
> db.contatti.find()
{ "_id" : ObjectId("5f8a96d1b86fba648726304b"), "name" : "Mario", "contacts" : { "email" : "mario@libero.it", "skype" : "mario.rossi" } }
{ "_id" : ObjectId("5f8a96d6b86fba648726304c"), "name" : "Luca", "contacts" : { "email" : "luca@gmail.com", "skype" : "luca.verdi" } }
{ "_id" : ObjectId("5f8a96dbb86fba648726304d"), "name" : "Stefania", "contacts" : { "email" : "stefy@libero.it", "skype" : "stefy.andrione" } }
La shell è impostata per iterare il cursorse e mostrare quindi tutti gli elementi. E' tuttavia possibile scorrere il cursore manualmente, scrivendo questo:
> var cur = db.contatti.find(); null;
Creiamo la variabile cur, che è un oggetto di tipo cursore. Il null posto alla fine evita che vengano mostrati tutti gli elementi.
L'oggetto cursore ha vari metodi.
I primi due che vediamo sono i seguenti
- hasNext(): verifica se è presente l'elemento successivo al cursore, e nel caso restiuisce true, in caso contrario false
- next(): funziona in modo analogo ma resituisce l'elemento successivo spostando il cursore in avanti
Vediamoli in azione
> var cur = db.contatti.find(); null;
null
> cur.hasNext()
true
> cur.next()
{
"_id" : ObjectId("5f8a96d1b86fba648726304b"),
"name" : "Mario",
"contacts" : {
"email" : "mario@libero.it",
"skype" : "mario.rossi"
}
}
Per visualizzare l'elemento successivo, utilizzo nuovamente next
> cur.next()
{
"_id" : ObjectId("5f8a96d6b86fba648726304c"),
"name" : "Luca",
"contacts" : {
"email" : "luca@gmail.com",
"skype" : "luca.verdi"
}
}
E così in avanti.
Potremmo scrivere un ciclo per scorrere completamente il nostro curore, ed ottenere il risultato della shell
> var cur = db.contatti.find(); null;
null
> while(cur.hasNext()) printjson( cur.next() );
{
"_id" : ObjectId("5f8a96d1b86fba648726304b"),
"name" : "Mario",
"contacts" : {
"email" : "mario@libero.it",
"skype" : "mario.rossi"
}
}
{
"_id" : ObjectId("5f8a96d6b86fba648726304c"),
"name" : "Luca",
"contacts" : {
"email" : "luca@gmail.com",
"skype" : "luca.verdi"
}
}
{
"_id" : ObjectId("5f8a96dbb86fba648726304d"),
"name" : "Stefania",
"contacts" : {
"email" : "stefy@libero.it",
"skype" : "stefy.andrione"
}
}
Vediamo adesso altri metodi dell'oggetto cursore.
Possiamo utilizzare il metodo limit per limitare i risultati del cursore, il modo simile al limit utilizzato nelle query SQL.
> var cur = db.contatti.find(); null;
null
> cur.limit(2); null;
null
> while(cur.hasNext()) printjson( cur.next() );
{
"_id" : ObjectId("5f8a96d1b86fba648726304b"),
"name" : "Mario",
"contacts" : {
"email" : "mario@libero.it",
"skype" : "mario.rossi"
}
}
{
"_id" : ObjectId("5f8a96d6b86fba648726304c"),
"name" : "Luca",
"contacts" : {
"email" : "luca@gmail.com",
"skype" : "luca.verdi"
}
}
Se vogliamo mostrare il risulato senza dover lanciare il ciclo while, basta togliere il null dopo il limit
> var cur = db.contatti.find(); null;
null
> cur.limit(2)
{ "_id" : ObjectId("5f8a96d1b86fba648726304b"), "name" : "Mario", "contacts" : { "email" : "mario@libero.it", "skype" : "mario.rossi" } }
{ "_id" : ObjectId("5f8a96d6b86fba648726304c"), "name" : "Luca", "contacts" : { "email" : "luca@gmail.com", "skype" : "luca.verdi" } }
Quindi, null serve a impedire che venga immediatamente visualizzata la lista dei risulati.
Il metodo sort è simile all'order by in SQL, e necessita dell'attributo (chiave) in base alla quale vogliamo ordinare i risultati della ricerca, e il tipo di ordinamento: 1 è inteso come crescente, -1 decrescente
Ad esempio ordiniamo i nomi per ordine crescente:
> var cur = db.contatti.find(); null;
null
> cur.sort({name:1})
{ "_id" : ObjectId("5f8a96d6b86fba648726304c"), "name" : "Luca", "contacts" : { "email" : "luca@gmail.com", "skype" : "luca.verdi" } }
{ "_id" : ObjectId("5f8a96d1b86fba648726304b"), "name" : "Mario", "contacts" : { "email" : "mario@libero.it", "skype" : "mario.rossi" } }
{ "_id" : ObjectId("5f8a96dbb86fba648726304d"), "name" : "Stefania", "contacts" : { "email" : "stefy@libero.it", "skype" : "stefy.andrione" } }
Possiamo aggiungere altri attributi di ordinamento, in ordine di priorità, come avviene in SQL
> cur.sort({"name":1,"surname":-1})
Possiamo combinare ovviamente i metodi sort e limit
> var cur = db.contatti.find(); null;
null
> cur.sort({name:1}).limit(2)
{ "_id" : ObjectId("5f8a96d6b86fba648726304c"), "name" : "Luca", "contacts" : { "email" : "luca@gmail.com", "skype" : "luca.verdi" } }
{ "_id" : ObjectId("5f8a96d1b86fba648726304b"), "name" : "Mario", "contacts" : { "email" : "mario@libero.it", "skype" : "mario.rossi" } }
Esiste poi il metodo skip che permette di salvare "n" documenti.
Ad esempio, voglio ordinare i nsotri contatti per nome, crescente, impostando un limit di 3, e vogliamo saltare i primi 2 documenti. Combinando tutti i metodi visti otteniamo:
> var cur = db.contatti.find(); null;
null
> cur.sort({name:1}).limit(3).skip(2)
{ "_id" : ObjectId("5f8a96dbb86fba648726304d"), "name" : "Stefania", "contacts" : { "email" : "stefy@libero.it", "skype" : "stefy.andrione" } }
Nella nostra collection erano presenti 3 documenti, saltando i primi due, ne resta solo uno.
Attenzione: Mongo non segue l'ordine in cui indichiamo i vari metodi del corsore, bensì in questo ordine rigoroso
- sort
- limit
- skip
Il metodo count: contare il risultato della ricerca
Con il metodo count, in modo simile a quanto avviene in SQL, possiamo contare i risultati (i documenti) della ricerca
Se non passiamo argomenti otteniamo il numero totale di documenti presenti nella collection
> db.contatti.count()
3
Oppure possiamo inserire nelle condizioni di ricerca, esattamente come abbiamo fatto utilizzando il metodo find.
> db.contatti.count({ "name":"Luca" })
1
L'update di un documento
Esistono 4 approcci differenti per effettuare un update in Mongo.
Possiamo effettuare:
- una sostituzione completa del documento
- una aggiunta o modifica di un attributo di un documento
- un UPSERT, ovvero la modifica di un documento se esiste, e la sua creazione se non esiste, il tutto in una unica operazione
- il multiupdate
Tranne che nell'ultimo caso, negli altri l'update ha effetto SOLO su un unico documento. L'ultima tipologia invece ci consente di aggiornare più documenti.
Update con sostituzione completa del documento
Il metodo update deve ricevere due argomenti obbligatori:
- una condizione, simile a quella usata nel metodo find, per identificare il documento o i documenti da aggiornare
- il dato da aggiornare, o meglio, il nuovo documento, perchè questo prenderà il posto del documento originario, ad eccezzione della chiave "_id" che reseterà immutata
Ad esempio aggiorniamo il documento che ha come nome "Luca" e li modifichiamo in "Antonio"
> db.contatti.update({ "name":"Luca" },{ "name":"Antonio"})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Mongo cercherà il primo documento che verifica la condizione, e lo sostutirà con il nuovo documento indicato
Infatti, se provi ad estrarre i nostri documenti, adesso il documento che conteneva il nome "Luca" sarà diventato questo
{ "_id" : ObjectId("5f8a96d6b86fba648726304c"), "name" : "Antonio" }
Come vediamo, il nome è cambiato in Antonio, manca l'attributo "contacts", mentre la chiave "_id" è l'unica rimasta dal documento originario.
Possiamo quindi capire che questo tipo di update è pericoloso, perchè se ci dimenticassimo di specificare un attributo nell'update, li perderemmo per sempre!
Per farti capire che non abbiamo solo aggiornato il "name" ma abbiamo sovrascritto integralmente il documento, facciamo un nuovo update su questo documento
> db.contatti.update({ "name":"Antonio" },{ "name":"Giovanni", "contacts":{"email":"gio@gmail.com", "skype":"giovanni.verdi"}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Il documento sarà stato interamente modificato, tranne la chiave "_id"
{ "_id" : ObjectId("5f8a96d6b86fba648726304c"), "name" : "Giovanni", "contacts" : { "email" : "gio@gmail.com", "skype" : "giovanni.verdi" } }
Questo update agisce solo sul primo documento che soddisfa la condizione e non su tutti, per cui se volessimo aggiornare più documenti, dovremmo fare un update ad uno ad uno.
Visto il rischio di cancellazione involontaria di attributi, questo approccio non è molto consigliato
Update con aggiunta o modifica di uno specifico attributo di un documento: gli attributi $set, $unset, $incr
Se vogliamo modificare solo un attributo, ad esempio, il "name", senza sovrascrivere tutto il resto, si utilizza un apposito operatore chiamato $set seguito dalla chiave da aggiornare.
> db.contatti.update({ "name":"Giovanni" },{ $set:{"name":"Giuseppe"} })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Il documento diventa questo, e come possiamo vedere, è stato modificato solo il valore dell'attributo "name"
{ "_id" : ObjectId("5f8a96d6b86fba648726304c"), "name" : "Giuseppe", "contacts" : { "email" : "gio@gmail.com", "skype" : "giovanni.verdi" } }
Inoltre, se la chiave non esiste, viene creata
Ad esempio, facciamo il seguente update
> db.contatti.update({ "name":"Giuseppe" },{ $set:{"age": 25} })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Il documento diventerà il seguente
{ "_id" : ObjectId("5f8a96d6b86fba648726304c"), "name" : "Giuseppe", "contacts" : { "email" : "gio@gmail.com", "skype" : "giovanni.verdi" }, "age" : 25 }
Per cui l'attributo $set serve a modificare un attributo, se presente, o ad aggiungerlo se non esiste.
Un altro attributo utilizzando in questo approccio di update è $inc che serve ad incrementare il valore di un attributo di x unità.
Ad esempio se volessi aumentare l'età di Giuseppe di 2 anni, scriverò
> db.contatti.update({ "name":"Giuseppe" },{ $inc:{"age": 2} })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Il documento sarà quindi aggiornato a 27 anni
{ "_id" : ObjectId("5f8a96d6b86fba648726304c"), "name" : "Giuseppe", "age" : 27, "contacts" : { "email" : "gio@gmail.com", "skype" : "giovanni.verdi" } }
Come $set, anche $inc, se non esiste l'attributo che stiamo incrementanto, lo crea assegnandogli come valore l'incremento indicato.
Ad esempio, "aggiorniamo" il documento con nome "Mario", che non ha un attributo "age", in questo modo
> db.contatti.update({ "name":"Mario" },{ $inc:{"age": 2} })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Adesso il documento di "Mario" presenterà la chiave "age" con valore "2"
{ "_id" : ObjectId("5f8a96d1b86fba648726304b"), "name" : "Mario", "contacts" : { "email" : "mario@libero.it", "skype" : "mario.rossi" }, "age" : 2 }
Per eliminare un attributo utilizziamo l'operatore $unset, in questo modo
> db.contatti.update({ "name":"Mario" },{ $unset:{"contacts":1} })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
In questo caso, come valore di "age" abbiamo indicato 1, ma in realtà potevamo indicare qualcunque cosa, infatti quel valore è considerato sempre l'equivalente di "true"
Il documento adesso non avrà più l'attributo "contacts"
{ "_id" : ObjectId("5f8a96d1b86fba648726304b"), "name" : "Mario", "age" : 2 }
Update in valori di tipo array: gli operatori $push, $pop, $pull, $pullALL, $addToSet
Per questa spiegazione utilizziamo una nuova collections chiamata "esempio".
Creiamo un documento a cui assegnamo, solo per nostra comodità di spiegazione, un "_id" con valore 0, e un attributo "a" di tipo array che contiene i valori 5,7,1,3 (usiamo valori numerici sempre per comodità)
Assegnamo anche come "_id" il valore 0 sempre per comodità, ma potevate assegnargli il valore che volevate.
> db.esempio.insert( { "_id":0, "a":[5,7,1,3,7] } )
WriteResult({ "nInserted" : 1 })
L'attributo "a" è di tipo array, e gli elementi dell' array, come certamente saprete, hanno un loro indice:
- il primo elemento è lo 0 (e nel nostro esempio ha valore 5)
- il secondo elemento è l'1 (e nel nostro esempio ha valore 7)
- il terzo elemento è il 2 (e nel nostro esempio ha valore 1)
- e così via....
Per cui, se volessi assegnare alla posizione 2 un nuovo valore, utilizzo questa query, in cui utilizzo la chiave "_id" per la ricerca
> db.esempio.update( { "_id":0}, { $set: {"a.2":9} } )
WriteResult({ "nInserted" : 1 })
Abbiamo quindi indicato l'attributo "a" e, dopo il punto, indentifichiamo la posizione dell'elemento da modificare (in modo simile a quando avevamo visto come accedere agli attriubuti dei sottodocumenti)
Il documento diventerà questo: il terzo elemento è stato modificato, perchè come abbiamo detto gli indici partono dallo 0
> db.esempio.findOne()
{ "_id" : 0, "a" : [ 5, 7, 9, 3, 7 ] }
Adesso vediamo alcuni operatori specifici che Mongo mette a disposizione per manipolare un array.
L'operatore $push consente di inserire un nuovo elemento in coda all'array.
> db.esempio.update( { "_id":0}, { $push: {"a":6} } )
WriteResult({ "nInserted" : 1 })
Il documento adesso sarà questo
{ "_id" : 0, "a" : [ 5, 7, 9, 3, 7, 6 ] }
L'operatore $pull consente di eliminare un elemento dell'array indipendentemente dalla sua posizione, ma basandoci sul suo valore
> db.esempio.update( { "_id":0}, { $pull: {"a":7} } )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Il documento adesso sarà questo: ha eliminato tutti i valori 7 presenti nell'array, quindi in qualunque posizione.
{ "_id" : 0, "a" : [ 5, 9, 3, 6 ] }
Per eliminare un elemento non in base al valore, ma alla posizione, utilizziamo l'operatore $pop che consente di eliminare il primo, se indico -1, o l'ultimo elemento dell'array, se indico 1.
Ad esempio eliminiamo l'ultimo elemento dell'array con questa query
> db.esempio.update( { "_id":0}, { $pop: {"a":1} } )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Il documento adesso sarà questo, e l'array non conterrà più l'ultimo elemento
{ "_id" : 0, "a" : [ 5, 9, 3 ] }
L'operatore $pullAll consente di eliminare un lista di valori presenti nell'array, a prescindere dalla posione in cui si trovano.
Ad esempio per eliminare i valori 5 e 3 utilizziamo questa query
> db.esempio.update( { "_id":0}, { $pull: {"a":[5,3]} } )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Il nostro documento adesso risulta il seguente
{ "_id" : 0, "a" : [ 9 ] }
Il nostro array è diventato misero... rimpolpiamolo inserendo più valori in un solo colpo. Per questo si utilizza l'operatore $push, combinato con il modificatore $each: i nuovi elementi verranno inserito al fondo dell'array come in questo esempio.
> db.esempio.update( { "_id":0}, { $push: { "a": { $each: [12,7,3,24,2] } } } )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
Ecco il nostro nuovo documento
{ "_id" : 0, "a" : [ 9, 12, 7, 3, 24, 2 ] }
Esiste poi l'operatore $addToSet che consente di aggiungere elementi SOLO se non sono già presenti, e quindi evita duplicati.
Se il valore NON è presente nell'array lo aggiunge co