Rust et PostgreSQL
Pour utiliser Rust avec PostgreSQL, nous avons plusieurs bibliothèques disponibles.
Pour cette article, nous allons utiliser la crate postgres
en mode synchrone (ou plus souvent tokio-postgres
pour les applications asynchrones) en Rust pour interagir avec une base de données PostgreSQL.
Je ne détaille pas ici la création d'un nouveau projet, je vous laisse regarder la commande cargo new
que vous trouverez dans la documentation officielle
Configuration de base
Le premier point est de déclarer les dépendances dans le fichier Cargo.toml
.
Ajouter les dépendances
Dans votre Cargo.toml
:
[dependencies]
tokio = { version = "1.0", features = ["full"] }
tokio-postgres = { version = "0.7", features = ["with-uuid-1"] }
Astuces
La liste des features
activable est disponible dans la documentation.
Connexion à la base de données
ensuite dans le fichier main.rs
Exemple synchrone (avec postgres
)
use postgres::{Client, NoTls, Error};
fn main() -> Result<(), Error> {
// Chaîne de connexion
let conn_str = "host=localhost user=postgres dbname=mydb password=1234";
let mut client = Client::connect(conn_str, NoTls)?;
// Exécution d'une requête simple pour récupérer la PK et le nom de l'utilisateur
for row in client.query("SELECT id, name FROM users", &[])? {
let id: i32 = row.get(0);
let name: String = row.get(1);
println!("id: {}, name: {}", id, name);
}
Ok(())
}
Remarques
Ici les informations de connexion sont connectés en dur, vous pouvez par exemple les transmettre en variables d'environnement et utiliser le module standard std::env
Exemple asynchrone (avec tokio-postgres
)
use tokio_postgres::{NoTls, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
// Chaîne de connexion
let conn_str = "host=localhost user=postgres dbname=mydb password=1234";
let (client, connection) = tokio_postgres::connect(conn_str, NoTls).await?;
// Exécution de la connexion en arrière-plan
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Erreur de connexion: {}", e);
}
});
// Exécution d'une requête simple
let rows = client.query("SELECT id, name FROM users", &[]).await?;
for row in rows {
let id: i32 = row.get(0);
let name: String = row.get(1);
println!("id: {}, name: {}", id, name);
}
Ok(())
}
Requêtes paramétrées
Les requêtes paramétrés sont el moyen le plus sur d'éviter les failles d'injections SQL.
Insertion de données
let name = "Alice";
let age = 30;
client.execute(
"INSERT INTO users (name, age) VALUES ($1, $2)",
&[&name, &age],
).await?;
Sélection avec filtre
let min_age = 25;
let rows = client.query(
"SELECT name FROM users WHERE age > $1",
&[&min_age],
).await?;
Transactions
let transaction = client.transaction().await?;
transaction.execute("INSERT INTO users (name) VALUES ($1)", &[&"Bob"]).await?;
transaction.execute("UPDATE users SET age = $1 WHERE name = $2", &[&40, &"Bob"]).await?;
transaction.commit().await?;
// Ou utiliser transaction.rollback().await?; (ou savepoint)
Remarques
Pour plus d'informations concernant les transactions, vous pouvez consulter la documentation.
Gestion des erreurs
Il ne faut pas non plus oublier la gestion des erreurs.
match client.query_one("SELECT name FROM users WHERE id = $1", &[&1]).await {
Ok(row) => {
let name: String = row.get(0);
println!("Nom: {}", name);
}
Err(e) => eprintln!("Erreur: {}", e),
}
Utilisation de types personnalisés
Si vous utilisez des types PostgreSQL spécifiques (comme UUID
), assurez-vous d'activer les features appropriées dans Cargo.toml
:
tokio-postgres = { version = "0.7", features = ["with-uuid-1"] }
Exemple d'utilisation :
use uuid::Uuid;
let id = Uuid::new_v4();
client.execute(
"INSERT INTO users (id, name) VALUES ($1, $2)",
&[&id, &"Charlie"],
).await?;
Pool de connexions (avec bb8
)
Pour gérer un pool de connexions, nous allons ajouter les crates bb8
et bb8-postgres
à nos dépendances, ajouter ceci dans le fichier Cargo.toml
bb8 = "0.8"
bb8-postgres = "0.8"
Exemple :
use bb8_postgres::PostgresConnectionManager;
use bb8::Pool;
type PgPool = Pool<PostgresConnectionManager<NoTls>>;
#[tokio::main]
async fn main() -> Result<(), Error> {
let manager = PostgresConnectionManager::new_from_stringlike(
"host=localhost user=postgres dbname=mydb password=1234",
NoTls,
)?;
let pool = Pool::builder().build(manager).await?;
let conn = pool.get().await?;
let rows = conn.query("SELECT id, name FROM users", &[]).await?;
// ...
Ok(())
}
Exemple complet (asynchrone)
use tokio_postgres::{NoTls, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let (client, connection) = tokio_postgres::connect(
"host=localhost user=postgres dbname=mydb password=1234",
NoTls,
).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Erreur de connexion: {}", e);
}
});
// Création d'une table
client.execute(
"CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
age INTEGER
)",
&[],
).await?;
// Insertion
client.execute(
"INSERT INTO users (name, age) VALUES ($1, $2)",
&[&"Alice", &30],
).await?;
// Sélection
let rows = client.query("SELECT id, name, age FROM users", &[]).await?;
for row in rows {
let id: i32 = row.get(0);
let name: String = row.get(1);
let age: i32 = row.get(2);
println!("id: {}, name: {}, age: {}", id, name, age);
}
Ok(())
}
Correspondance
Ci dessous vous trouverez un tableau de correspondance des types entre PostgreSQL et Rust (avec les crates postgres
/tokio-postgres
)
WARNING
Si vous utiliser d'autres bibliothèques, il se peut que certains type puisse différé légèrement
Type PostgreSQL | Type Rust (crate postgres /tokio-postgres ) | Remarques |
---|---|---|
SMALLINT | i16 | Entier 16 bits |
INTEGER | i32 | Entier 32 bits |
BIGINT | i64 | Entier 64 bits |
SERIAL | i32 | Auto-incrément (entier 32 bits) |
BIGSERIAL | i64 | Auto-incrément (entier 64 bits) |
REAL | f32 | Nombre à virgule flottante 32 bits |
DOUBLE PRECISION | f64 | Nombre à virgule flottante 64 bits |
NUMERIC | rust_decimal::Decimal | Requiert la crate rust_decimal pour une précision arbitraire |
BOOLEAN | bool | Vrai/Faux |
CHAR(N) /VARCHAR(N) | String | Chaîne de caractères |
TEXT | String | Chaîne de caractères de taille illimitée |
BYTEA | Vec<u8> | Tableau d'octets |
DATE | chrono::NaiveDate | Requiert la crate chrono |
TIME | chrono::NaiveTime | Requiert la crate chrono |
TIMESTAMP | chrono::NaiveDateTime | Requiert la crate chrono |
TIMESTAMPTZ | chrono::DateTime<Utc> | Requiert la crate chrono |
INTERVAL | postgres_types::Interval | Type spécifique à PostgreSQL |
UUID | uuid::Uuid | Requiert la feature with-uuid-1 et la crate uuid |
JSON /JSONB | serde_json::Value | Requiert la feature with-serde_json-1 et la crate serde_json |
ARRAY | Vec<T> | Où T est le type des éléments du tableau |
INET /CIDR | std::net::IpAddr /std::net::IpNet | Pour les adresses IP et réseaux |
POINT | (f64, f64) ou structure personnalisée | Coordonnées géométriques |
ENUM | String ou enum Rust | Nécessite une conversion manuelle |
Remarques importantes :
- Types temporels : La crate
chrono
est couramment utilisée pour manipuler les dates et heures. - Types décimaux : Pour
NUMERIC
, utilisezrust_decimal::Decimal
pour éviter les problèmes de précision. - UUID : Activez la feature
with-uuid-1
danstokio-postgres
pour supporter les UUID. - JSON : Activez la feature
with-serde_json-1
pour manipuler les types JSON/JSONB. - Types géométriques : Les types comme
POINT
peuvent être mappés à des tuples ou des structures Rust.
Points clés à retenir
- Sécurité : Utiliser le plus fréquement possible des requêtes paramétrées pour éviter les injections SQL.
- Asynchrone : Préférez
tokio-postgres
pour les applications asynchrones. - Pool de connexions : Utilisez
bb8
pour gérer efficacement les connexions en production. - Types personnalisés : Activez les features nécessaires pour les types comme
UUID
.