Quasi ogni progetto, anche uno basato su Rust, ha necessità di memorizzare dei dati. In molti casi, si ricorre al salvataggio su file, in tanti altri in cui è importante fondarsi su una struttura di memorizzazione ben articolata si integra nei progetti un database.
In questa lezione, vedremo come fare in modo che il linguaggio Rust riesca a dialogare con un database relazionale e nello specifico un database di tipo MySQL. Nel caso di questo esempio lo avvieremo tramite Docker ma qualora si preferissero altre modalità o si avesse a disposizione una propria installazione già pronta ci si ritenga liberi di procedere altrimenti: la sostanza della lezione non cambierà minimamente.
Rust e SQL
Inutile dire che un linguaggio come Rust dispone di tante alternative per l'interazione con un database relazionale. Qui abbiamo usato una soluzione piuttosto tipica - il crate mysql - che permette di personalizzare il lavoro a proprio piacimento seguendo un flusso piuttosto tipico - non complesso da imparare se si hanno già avute esperienze simili in altri linguaggi - sfruttando anche la disponibilità di un connection pool. Esistono tuttavia tante altre alternative che non vedremo qui tra cui Diesel, un Object Relational Model (ORM) per PostgreSQL, Sqlite nonchè MySQL e sqlx che permette anch'esso l'interazione con diversi sistemi per database relazionali.
Prepariamo l'esempio: il database
Nel nostro esempio, procederemo con operazioni "scolastiche" ma molto utili di inserimento e lettura di dati da una tabella MySQL pertanto, per prima cosa, avremo bisogno del database.
Come anticipato, lo avvieremo con Docker in questo modo:
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=topolino mysql
e connettendoci tramite il client mysql
creiamo una tabella in cui aggregheremo dati a proposito di operazioni bancarie (una sorta di estratto conto):
$ mysql -h 172.17.0.2 -u root -p
...
mysql> CREATE DATABASE banca;
Query OK, 1 row affected (0.01 sec)
mysql> USE banca;
Database changed
mysql> SHOW TABLES;
Empty set (0.00 sec)
mysql> CREATE TABLE operazioni (
-> id int not null,
-> importo FLOAT not null,
-> operazione TEXT);
Query OK, 0 rows affected (0.03 sec)
mysql> SHOW TABLES;
+-----------------+
| Tables_in_banca |
+-----------------+
| operazioni |
+-----------------+
Collegandoci al sistema (nel nostro caso, indirizzo IP 172.17.0.2
) abbiamo creato un database di nome banca
e la tabella operazioni
: si noti in proposito il cambio di prompt tra il sistema operativo ($
) e l'ingresso in MySQL (mysql>
).
Ora che è tutto pronto passiamo al vero e proprio programma Rust.
Il codice
Usiamo cargo per la gestione del progetto e, nel file di configurazione Cargo.toml
, inseriamo l'annotazione per le dipendenze:
[dependencies]
mysql = "*"
Il programma che segue - puntualmente illustrato da commenti - crea un vettore di dati strutturati sulla struct OperazioneBancaria
composta da tre semplici campi (identificativo operazione, importo e descrizione dell'operazione). I dati di ognuno di questi elementi vengono iniettati nella tabella per poi, alla fine, essere letti con una query:
// importiamo le librerie di cui abbiamo bisogno
use mysql::*;
use mysql::prelude::Queryable;
// creazione di una Struct di base per organizzare le nostre unità informative
struct OperazioneBancaria {
id: i32,
importo: f32,
operazione: Option<String>,
}
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
// impostiamo la stringa di connessione
let conn_string = "mysql://root:topolino@172.17.0.2:3306/banca";
// apriamo un pool di connesstioni e ne estraiamo una
let conn_pool = Pool::new(conn_string)?;
let mut conn = conn_pool.get_conn()?;
// prepariamo una batteria di struct da salvare nel database
let operazioni = vec![
OperazioneBancaria { id: 1, importo: 230.00, operazione: Some("Accredito dal conto nr. 6789876".to_owned()) },
OperazioneBancaria { id: 2, importo: -150.00, operazione: Some("Pagobancomat presso Pizzeria Carmelo".to_owned()) },
OperazioneBancaria { id: 3, importo: -70.00, operazione: Some("Pagobancomat presso Abiti da sogno".to_owned()) },
OperazioneBancaria { id: 4, importo: 211.00, operazione: Some("Versamento in contanti".to_owned()) }
];
// programmiamo l'inserimento all'interno della tabella operazioni
conn.exec_batch(
r"INSERT INTO operazioni (id, importo, operazione)
VALUES (:id, :importo, :operazione)",
operazioni.iter().map(|p| params! {
"id" => p.id,
"importo" => p.importo,
"operazione" => &p.operazione,
})
)?;
// eseguiamo la lettura dei dati tramite query
let risultato = conn.query_map(
"SELECT id, importo, operazione from operazioni",
|(id, importo, operazione)| {
OperazioneBancaria { id, importo, operazione }
},
)?;
// iteriamo tra i risultati per stamparne il contenuto
for operazione in risultato.iter(){
println!("Id: {} importo: {} euro {}",
&operazione.id,
&operazione.importo,
&operazione.operazione.as_ref().unwrap()
);
}
Ok(())
}
L'output dell'applicazione Rust
L'output finale che dimostra sia l'effettivo inserimento sia il concreto recupero è il seguente (avviato con il comando cargo run
):
Id: 1 importo: 230 euro Accredito dal conto nr. 6789876
Id: 2 importo: -150 euro Pagobancomat presso Pizzeria Carmelo
Id: 3 importo: -70 euro Pagobancomat presso Abiti da sogno
Id: 4 importo: 211 euro Versamento in contanti
e anche accedendo al database MySQL direttamente si può notare l'effettiva esistenza dei dati memorizzati in maniera persistente:
mysql> SELECT * FROM operazioni;
+----+---------+--------------------------------------+
| id | importo | operazione |
+----+---------+--------------------------------------+
| 1 | 230 | Accredito dal conto nr. 6789876 |
| 2 | -150 | Pagobancomat presso Pizzeria Carmelo |
| 3 | -70 | Pagobancomat presso Abiti da sogno |
| 4 | 211 | Versamento in contanti |
+----+---------+--------------------------------------+
A questo punto, nonostante l'essenzialità dell'esempio, la strada per l'integrazione di un database relazionale è spianata. Con la conoscenza del linguaggio SQL potremo definire operazioni che tramite codice simile a questo potranno essere inoltrate direttamente al database.