Pourquoi Rust avec PostgreSQL : guide pratique ?
Contexte réel : pourquoi un dev a besoin de ca au quotidien
Au sein d'un environnement professionnel, la gestion de données est cruciale pour l'efficacité et la performance des applications. PostgreSQL, une base de données relationnelle puissante et fiable, offre des fonctionnalités avancées qui sont essentielles dans un contexte moderne du développement.
Un cas d'utilisation concret serait une application e-commerce. La gestion efficace de millions de produits, des commandes et des utilisateurs nécessite une base de données robuste. Rust, avec son modèle de programmation concurrente sécurisé et sa performance élevée, est idéal pour construire un backend performant qui peut traiter une grande charge de travail en temps réel.
Prerequis
- Connaissance de base de Rust
- Familiarité avec les bases de données SQL
- Installation de PostgreSQL
- Un environnement de développement (par exemple, Visual Studio Code avec des extensions Rust)
Concepts fondamentaux
1. Connexion à la Base de Données
Pour interagir avec PostgreSQL en Rust, nous utilisons le crate tokio-postgres. Voici comment vous pouvez vous connecter à une base de données :
use tokio_postgres::{NoTls, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
// Connexion à la base de données PostgreSQL
let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=secret dbname=mydb", NoTls).await?;
// Lancement du thread pour gérer les connexions
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
Ok(())
}
2. Exécution de Requêtes
Une fois connecté à la base de données, vous pouvez exécuter des requêtes SQL :
use tokio_postgres::{NoTls, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=secret dbname=mydb", NoTls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
// Exécution d'une requête SQL
let rows = client.query("SELECT id, name FROM users WHERE age > $1", &[&20]).await?;
for row in rows {
let id: i32 = row.get(0);
let name: String = row.get(1);
println!("User ID: {}, Name: {}", id, name);
}
Ok(())
}
3. Insertion de Données
Vous pouvez également insérer des données dans la base de données :
use tokio_postgres::{NoTls, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=secret dbname=mydb", NoTls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
// Insertion de données
client.execute(
"INSERT INTO users (name, age) VALUES ($1, $2)",
&[&"John Doe", &30],
).await?;
Ok(())
}
4. Mise à jour et Suppression
Mettre à jour et supprimer des données est également possible :
use tokio_postgres::{NoTls, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=secret dbname=mydb", NoTls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
// Mise à jour de données
client.execute(
"UPDATE users SET age = $1 WHERE name = $2",
&[&35, &"John Doe"],
).await?;
// Suppression de données
client.execute(
"DELETE FROM users WHERE name = $1",
&[&"John Doe"],
).await?;
Ok(())
}
Mise en pratique : projet fil rouge
Étape 1 : Configuration du Projet
Créez un nouveau projet Rust :
cargo new rust_postgres_tutorial --bin
cd rust_postgres_tutorial
Ajoutez les dépendances nécessaires dans Cargo.toml :
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-postgres = "0.7"
Étape 2 : Structure du Projet
Créez une structure de fichiers simple :
src/
├── main.rs
└── db/
└── connection.rs
Étape 3 : Connexion à la Base de Données
Ajoutez le code pour se connecter à la base de données dans db/connection.rs :
use tokio_postgres::{NoTls, Error};
pub async fn establish_connection() -> Result<tokio_postgres::Client, Error> {
let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=secret dbname=mydb", NoTls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
Ok(client)
}
Étape 4 : Création des Tables
Ajoutez du code pour créer les tables nécessaires dans db/schema.sql :
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT NOT NULL
);
Créez un script pour exécuter la création des tables :
psql -h localhost -U postgres -d mydb -f db/schema.sql
Étape 5 : Insertion de Données
Ajoutez du code pour insérer des données dans src/main.rs :
use crate::db;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = db::establish_connection().await?;
// Insertion de données
client.execute(
"INSERT INTO users (name, age) VALUES ($1, $2)",
&[&"John Doe", &30],
).await?;
Ok(())
}
Étape 6 : Mise à jour et Suppression
Ajoutez du code pour mettre à jour et supprimer des données dans src/main.rs :
use crate::db;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = db::establish_connection().await?;
// Mise à jour de données
client.execute(
"UPDATE users SET age = $1 WHERE name = $2",
&[&35, &"John Doe"],
).await?;
// Suppression de données
client.execute(
"DELETE FROM users WHERE name = $1",
&[&"John Doe"],
).await?;
Ok(())
}
Erreurs fréquentes et debugging
1. Connexion échoue
## ❌ Mauvais
let (client, connection) = tokio_postgres::connect("host=localhost user=postgres dbname=mydb", NoTls).await?;
## ✅ Correct
let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=secret dbname=mydb", NoTls).await?;
2. Exécution de requêtes échoue
## ❌ Mauvais
let rows = client.query("SELECT id, name FROM users WHERE age > $1", &[&20]).await?;
## ✅ Correct
let rows = client.query("SELECT id, name FROM users WHERE age > $1", &[&20]).await?;
3. Insertion de données échoue
## ❌ Mauvais
client.execute(
"INSERT INTO users (name, age) VALUES ($1, $2)",
&[&"John Doe", &30],
).await?;
## ✅ Correct
client.execute(
"INSERT INTO users (name, age) VALUES ($1, $2)",
&[&"John Doe", &30],
).await?;
Pour aller plus loin
1. Transactions
Les transactions sont essentielles pour assurer l'intégrité des données. Voici comment vous pouvez les gérer :
use tokio_postgres::{NoTls, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=secret dbname=mydb", NoTls).await?;
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("connection error: {}", e);
}
});
// Début d'une transaction
let mut tx = client.transaction().await?;
// Exécution de requêtes dans la transaction
tx.execute(
"INSERT INTO users (name, age) VALUES ($1, $2)",
&[&"John Doe", &30],
).await?;
tx.commit().await?;
Ok(())
}
2. Migration
La gestion des migrations de base de données est cruciale pour le développement en équipe et la maintenance. Utilisez flyway ou Liquibase pour gérer les migrations.
3. ORM
Pour un développement plus facile, vous pouvez utiliser des ORMs comme diesel :
[dependencies]
diesel = { version = "1.4", features = ["postgres"] }
dotenv = "0.15"
Créez un fichier .env pour les variables d'environnement :
DATABASE_URL=postgres://postgres:secret@localhost/mydb
Défi pratique
Construire une application simple qui permet de gérer des tâches (ajouter, mettre à jour, supprimer, afficher). Utilisez les concepts appris et ajoutez des fonctionnalités supplémentaires comme la gestion des utilisateurs.
Ce tutoriel a couvert les bases de l'utilisation de Rust avec PostgreSQL, en abordant la connexion à la base de données, l'exécution de requêtes SQL, l'insertion, la mise à jour et la suppression de données. Il a également montré comment mettre en œuvre des transactions et donner une introduction aux ORMs pour faciliter le développement.