Uno dei punti di forza dei database orientati ai documenti è proprio la velocità in scrittura. Perciò è importante conoscere bene le funzioni per la
modifica dei dati. Le vedremo in azione nella shell mongo, ma in generale ogni Driver dispone di una sintassi corrispondente ad ognuna di esse.
Prima però è necessaria una premessa.
Write Concern
Quando viene inviato un comando di scrittura su un database relazionale, per le proprietà ACID, viene garantito che, se non si sono verificati errori, il
database ha recepito e memorizzato in modo persistente la modifica. Con MongoDB, invece, si possono configurare diversi scenari, che dipendono dalle nostre
preferenze o dalle necessità applicative. In generale queste strategie (denominate Write Concern Level) rappresentano diversi livelli di
compromesso tra velocità e consistenza dei dati.
Aknowledged
(ricevuto): indica che il client riceverà risposta quando il comando sarà stato recepito e il server lo avrà memorizzato. Quindi, per esempio, non c’è
garanzia che sia possibile ripristinare i dati appena salvati se il server andasse in crash a causa di problema alla memoria o per un errore del sistema
operativo. Però le prestazioni di esecuzione del comando saranno sicuramente buone, perché la scrittura in memoria è molto veloce, ed il client può gestire
l’evenienza di un errore di rete oppure di un errore logico nel comando.
Unaknowledged: è la strategia più rapida, detta anche “fire-and-forget”, perché il client invia il comando e non attende nessuna risposta. Sul server può succedere
qualsiasi cosa (l’esecuzione può andare a buon fine o il comando può non arrivare, oppure può esserci un errore nel documento o un crash del server) ma il
client, una volta lanciato il comando, non otterrà alcuna risposta. Questa strategia è rilevante quando vengono generati molti dati, ed è di primaria
importanza la loro “freschezza” piuttosto che la consistenza, oppure quando si importa una grande mole di dati (operazioni bulk) e si è
già sicuri della consistenza degli stessi. Si specifica impostando l’attributo w a 1, come vedremo.
Journaled
(annotato): significa che il client riceverà la risposta dal server solo quando il comando sarà stato salvato in memoria e scritto nel disco fisso sul transaction log, un file di log che permette al database di ripristinare la modifica in caso di crash o di interruzione
dell’alimentazione. Ovviamente questa strategia è più lenta delle altre due, ma da un po’ di sicurezza in più. Si specifica impostando l’attributo j a true.
Replica Acknowledged: uno dei punti di forza di MongoDB è il Replica Set, un insieme di repliche del database primario che permette il failover automatico,
cioè la capacità di caricare una replica in tempo reale se il database primario andasse in crash o fosse irraggiungibile. Supponiamo di utilizzare il Write
Concern Journaled per i nostri comandi; cosa succederebbe se il server primario andasse in crash dopo aver inviato la risposta al client, ma prima di aver
replicato il comando sulle repliche? Andando in funzione una delle repliche, la modifica non sarebbe presente. Con Replica Akwnowledged si ha la garanzia
che la modifica è stata propagata sul numero di repliche indicato nell’attributo w.
Majority: simile a Replica Aknowledged, ma si aspetta che la modifica sia propagata sulla maggioranza delle repliche.
I Write Concern possono essere indicati in ogni operazione, quindi si può decidere il livello in base allo specifico comando che si sta inviando.
Ora apriamo una shell di MongoDB (mongo) e colleghiamoci ad un server, come abbiamo visto nella lezione 2.
Inserimento di dati: insert
Il metodo insert
permette di aggiungere un elemento ad una collezione. Consideriamo l’esempio di un sito che permette di inserire commenti ai prodotti in vendita, e che gli utenti possano commentare anche i commenti degli altri utenti. Nella fase iniziale di importazione dei dati, possiamo effettuare un inserimento di massa utilizzando il comando:
db.users.insert([
{_id: "user1@html.it" , name: "User 1", logins: 31, lastAccess: new Date(2014, 10, 7) },
{_id: "user2@html.it" , name: "User 2" }
],
{ writeConcern: { w: 0 }
}
Il primo parametro è un array di documenti. Nell’esempio abbiamo specificato un _id, che quindi non verrà generato da MongoDB. Ovviamente questo
script potrebbe essere generato da un altro strumento software, per esempio una stored procedure di un database SQL, e contenere una grande mole di
inserimenti. Il campo lastAccess viene inizializzato con una data, il 7 novembre (infatti gennaio corrisponde a 0, e quindi novembre è
rappresentato dal numero 10) dell’anno 2014. La data verrà memorizzata in UTC nel database, e si perderà il dato sull’ora locale.
È compito dell’applicazione gestire la data ed il time zone.
Tramite il parametro { writeConcern: { w: 0 } }
abbiamo richiesto di usare una scrittura con il Write Concern Unacknowledged. Quindi non avremo evidenza da MongoDB dei risultati nell’inserimento, ma sapremo solo che il client li avrà inviati al server.
Dopo aver eseguito il comando sopra, se interroghiamo la collection tramite db.users.find()
troveremo che i dati sono stati inseriti, a meno che nel frattempo non si siano verificati imprevisti sul server (per esempio non era momentaneamente raggiungibile, oppure si fosse riavviato): in tal caso non vedremo alcun dato inserito, o mancheranno alcune entry.
Modifica di dati: update
Ora immaginiamo di modificare un utente per aggiornare la data dell’ultimo accesso e il numero di login effettuati:
db.users.update(
{ name: "User 1" },
{
$inc: { logins: 1},
$set: { lastAccess: new Date(2014, 10, 8) }
}
)
Tramite l’operatore $inc
abbiamo richiesto di incrementare i campi indicati, mentre con $set
abbiamo
voluto modificare i campi specificati successivamente. Non avendo aggiunto altre opzioni, abbiamo modificato solo il primo documento, che corrisponde alla
query indicata: l’utente con nome User 1. Con la stessa sintassi si può usare l’operatore $unset
, che elimina il campo
dal documento. La lista completa degli operatori che possono essere usati è consultabile sul sito ufficiale.
Si noti, inoltre, che l’operazione è atomica: ciò implica che, ad esempio, l’incremento non può essere falsato da scritture contemporanee.
Il comando update
permette anche di rimpiazzare totalmente un documento con un altro espressamente specificato. Ciò può essere
utile quando non si sa il documento è presente o no nel database (upsert):
db.users.update(
{ name: "User 3" },
{ _id: "user3@html.it", name: "User 3", lastAccess : new Date() },
{upsert: true }
)
Se questo comando trova un utente con nome User 3, lo rimpiazza con il documento specificato nel secondo parametro; altrimenti lo crea ex-novo.
Da qui si vede che il comando update permette di specificare un terzo parametro, chiamato options, con i seguenti campi:
Campo | Descrizione |
---|---|
upsert |
Se valorizzato a true, consente di effettuare l’inserimento se nessun documento è stato trovato con le caratteristiche indicate. Di default è false |
multi |
Se valorizzato a true, consente di specificare se l’update è massivo, o va fatto solo sul primo documento trovato. Di default è false |
writeConcern |
Per specificare il Write Concern, come abbiamo visto con il comando insert |
Eliminazione dei dati: remove
Il comando remove
è molto semplice da utilizzare. Esso accetta come parametri una query, intesa come oggetto BSON, ed alcune opzioni facoltative:
Campo | Descrizione |
---|---|
justOne |
Indica se vogliamo eliminare un solo elemento. Di default è false, quindi tutti i documenti che soddisfano la query verranno eliminati |
writeConcern |
per specificare il Write Concern |
Ad esempio, per essere sicuri di eliminare solo un utente con nome User 3:
db.users.remove( { name: "User 3" }, { justOne: true } )
Un’alternativa a update e a remove
Il comando findAndModify
permette di effettuare all’incirca le stesse operazioni di update e remove, ma con una
sintassi più compatta e con la possibilità di ottenere, come risultato dell’operazione, il documento modificato oppure il documento come è stato trovato
prima della modifica. Tuttavia non sono permesse operazioni di massa sui documenti, né è quindi possibile specificare il Write Concern.
db.users.findAndModify({
query: { name: "User 4" },
update: { $currentDate: { lastAccess: true } } ,
upsert: true,
new: true
})
Questo comando modificherà l’utente con nome User 4, impostando il campo lastAccess alla data corrente. Se non verrà trovato, l’utente
verrà creato ex-novo (upsert). Di default il comando restituisce il documento modificato (o null se nessun documento è stato trovato
inizialmente); tuttavia, specificando il campo new valorizzato a true, si ottengono invece i documenti nello stato finale, come sono
stati salvati nel database. Tramite l’opzione fields si possono specificare i singoli campi, altrimenti di default viene restituito l’intero
documento, come vediamo in questo esempio di eliminazione:
db.users.findAndModify({
query: { name: "User 4" },
remove: true,
fields: { _id: true }
})
In questo caso il valore di ritorno sarà null se l’utente con nome User 4 non è stato trovato, altrimenti verrà restituito solo il campo _id dell’utente trovato ed eliminato.