Dopo aver visto come accedere a database relazionali con Rust, è il momento di trattare l'altra metà del cielo ovvero i database NoSQL. Rust offre molte opportunità anche per operare con questi prodotti, importantissimi nell'ambito del trattamento delle informazioni, ed in questo caso affronteremo quello che probabilmente ne è uno dei protagonisti assoluti: MongoDB. Impareremo in questa lezione come collegarci con un database di questa piattaforma ma anche come sfruttare appositi strumenti per la trasformazione delle strutture dati di MongoDB in quelle di Rust e viceversa.
Preparare il database MongoDB per Rust
Per prima cosa abbiamo bisogno di un database MongoDB. Per avere un'istanza operativa di tale piattaforma, possiamo procedere in vari modi partendo da un'installazione in locale o all'avvio di un container Docker o - alternativa più comoda di tutte - accedere alla versione fully managed Cloud MongoDB Atlas in cui si può attivare un database, gratuitamente, senza dover installare niente.
In ogni caso, la preparazione terminerà con una sorta di stringa di connessione che riassumerà tutti i dati necessari affinché MongoDB sia raggiunto e permetta l'accesso alla nostra applicazione: la stringa contemplerà infatti username, password e coordinate di rete.
Creare un nuovo documento
Mongo ragiona "a documenti". Costrutti che possono essere paragonati ad un oggetto della Programmazione Orientata agli Oggetti, ad una struct di Rust o ad un oggetto JSON. Insomma a qualsiasi architettura sintattica che permetta di aggregare più dati insieme per generare una sorta
di informazione articolata di livello superiore. I documenti corrispondono in un database Mongo a quello che sono le righe in una tabella relazionale, mentre alla tabella stessa viene fatto corrispondere il concetto di collection.
Un modo estremamente pratico per integrare la logica di Mongo in un programma Rust è quello di importare la libreria mongodb:
[dependencies]
mongodb = "3.2.1"
tokio = { version = "1", features = ["full"] }
bson = "2.13.0"
tokyo
risulta utile, come abbiamo già visto, per la programmazione asincrona. bson
per la gestione del formato BSON (codifica e decodifica) per il salvataggio ed il recupero dei dati nel formato interno di MongoDB.
Un esempio pratico in Rust
Il seguente codice, come spiegano i commenti, crea, nella collection dipendenti
, un nuovo documento che rappresenta i dati di un programmatore che fa parte di uno staff aziendale:
use mongodb::Client;
use std::error::Error;
use tokio;
use mongodb::bson::doc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// ci connettiamo al client con la stringa di connessione
let client=Client::with_uri_str("STRINGA DI CONNESSIONE PERSONALE").await?;
// definiamo la struttura dati Rust che diventerà un documento MongoDB
let emp = doc! {
"nome": "Enea Rossi",
"assunzione": 2020,
"ruolo": "sviluppatore"
};
println!("{}", emp);
// procediamo al salvataggio del documento
let risultato = client.database("azienda").collection("dipendenti").insert_one(emp.clone()).await?;
// stampa dell'identificativo ottenuto per il nuovo documento: inserimento riuscito!
println!("Id oggetto creato: {}", risultato.inserted_id);
Ok(())
}
Al termine ci verrà stampato l'identificativo che permetterà univocamente di accedere al nuovo documento collocato nel database azienda
, precisamente nella collection dipendenti
. Accedendo al database MongoDB troveremo i dati così salvati:
_id: ObjectId('67bcc1bcfcc1dd5ca8dcf347')
nome:"Enea Rossi"
assunzione: 2020
ruolo: "sviluppatore"
Il driver offre molti altri metodi per la gestione della scrittura di dati come insert_many
per l'inserimento di oggetti multipli in un colpo solo ma non li approfondiremo in questa lezione (non mancherà ancora comunque occasione). Essi possono essere approfonditi nella pagine ufficiali della documentazione del driver.
Lettura di una collection
Il driver non risulta troppo complicato da approcciare in quanto ripercorre i metodi che il database usa nel suo linguaggio. Ad esempio, interroghiamolo per ottenere tutti i dipendenti assunti nel 2020:
use mongodb::{Client, Collection};
use mongodb::bson::doc;
use futures::TryStreamExt;
use serde::{ Deserialize, Serialize };
// definiamo la struct che ospiterà di dati recuperati dai documenti
#[derive(Serialize, Deserialize, Debug)]
struct Dipendente {
nome: String,
ruolo: String,
assunzione: i32
}
#[tokio::main]
async fn main() -> mongodb::error::Result<()> {
let uri = "STRINGA DI CONNESSIONE PERSONALE";
let client = Client::with_uri_str(uri).await?;
// otteniamo un riferimento alla collection
let collection: Collection<Dipendente> = client
.database("azienda")
.collection("dipendenti");
// cerchiamo tutti i documenti dove il campo assunzione riporti 2020
let mut risultati = collection.find(
doc! { "assunzione": 2020 }
).await?;
// stampa di ogni documento recuperato
while let Some(documento) = risultati.try_next().await? {
println!("{:?}", documento);
}
Ok(())
}
Abbiamo gestito il tutto in maniera asincrona ma soprattutto usando il crate serde
abbiamo ottenuto uno strumento per la serializzazione e deserializzazione dei documenti. In questo caso infatti vediamo che è stata predisposta una struct che ha la stessa forma dei documenti nel database e di essa verranno create istanze ognuna popolata automaticamente con i dati di un singolo documento.