Bibliothèque en Rust : egui + SQLite
Objectif de l'exercice
Reproduire, côté desktop natif, le même exercice CRUD que la bibliothèque PHP/PDO/MariaDB déjà réalisée. Même logique métier, même structure de validation — mais sans HTTP, sans rechargement de page, et avec le compilateur Rust comme garde-fou supplémentaire.
Cette page suppose que vous avez déjà suivi les deux exercices précédents : le compteur egui et le formulaire avec validation.
Comparaison avec l'exercice PHP/PDO
| Étape | PHP / MariaDB | Rust / SQLite (ici) |
|---|---|---|
| Connexion à la base | PDO (config.php) |
Connection::open() (ouvrir_connexion) |
| Création de la table | schema.sql |
init_db() |
| Lister les livres | SELECT * (index.php) |
charger_livres() |
| Ajouter un livre | INSERT (ajouter.php) |
inserer_livre() |
| Modifier un livre | UPDATE (modifier.php) |
mettre_a_jour_livre() |
| Supprimer un livre | DELETE (supprimer.php) |
supprimer_livre() |
| Validation avant écriture | fonctions PHP dédiées | fonctions valider_* renvoyant Result |
| Affichage tabulaire | <table> HTML |
TableBuilder (crate egui_extras) |
Différence structurelle à souligner en classe
En PHP, chaque action est une page séparée : cycle complet requête → réponse → rechargement de navigateur. Ici, tout vit dans une seule fenêtre, en mémoire, et l'on recharge simplement les données depuis SQLite après chaque écriture. Pas de HTTP, pas de session : c'est du desktop natif.
Prérequis
Installation
- Rust installé via
rustup(voir la page d'installation) - Aucun serveur de base de données à lancer : SQLite est un
simple fichier (
bibliotheque.db), créé automatiquement au premier lancement.
Cargo.toml :
[package]
name = "bibliotheque"
version = "0.1.0"
edition = "2021"
[dependencies]
eframe = "0.36.1"
egui_extras = "0.36.1"
rusqlite = { version = "0.32", features = ["bundled"] }
Le feature bundled
Il compile SQLite directement dans le binaire : pas besoin
d'installer libsqlite3-dev sur les machines des élèves. Un
seul cargo build suffit, quelle que soit la distribution
utilisée en classe.
Le code, par blocs logiques
use eframe::egui;
use egui_extras::{Column, TableBuilder};
use rusqlite::{Connection, Result as SqlResult};
struct MonApplication {
conn: Connection,
livres: Vec<Livre>,
titre_saisi: String,
auteur_saisi: String,
annee_saisie: String,
isbn_saisi: String,
id_en_edition: Option<i64>,
message_erreur: Option<String>,
}
impl Default for MonApplication {
fn default() -> Self {
let conn = ouvrir_connexion().expect("connexion SQLite impossible");
init_db(&conn).expect("création de la table impossible");
let livres = charger_livres(&conn).unwrap_or_default();
Self {
conn,
livres,
titre_saisi: String::new(),
auteur_saisi: String::new(),
annee_saisie: String::new(),
isbn_saisi: String::new(),
id_en_edition: None,
message_erreur: None,
}
}
}
Pour vos élèves
Faire remarquer que MonApplication regroupe tout l'état du
programme : la connexion à la base, la liste affichée, et les
champs du formulaire en cours de saisie. En PHP, cet état était
éclaté entre variables de requête ($_POST), session et base
de données ; ici, tout vit dans une seule structure Rust.
#[derive(Debug, Clone)]
struct Livre {
id: i64,
titre: String,
auteur: String,
annee: i32,
isbn: String,
}
fn ouvrir_connexion() -> SqlResult<Connection> {
Connection::open("bibliotheque.db")
}
fn init_db(conn: &Connection) -> SqlResult<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS livres (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titre TEXT NOT NULL,
auteur TEXT NOT NULL,
annee INTEGER NOT NULL,
isbn TEXT NOT NULL
)",
[],
)?;
Ok(())
}
fn charger_livres(conn: &Connection) -> SqlResult<Vec<Livre>> {
let mut stmt =
conn.prepare("SELECT id, titre, auteur, annee, isbn FROM livres ORDER BY titre")?;
let lignes = stmt.query_map([], |ligne| {
Ok(Livre {
id: ligne.get(0)?,
titre: ligne.get(1)?,
auteur: ligne.get(2)?,
annee: ligne.get(3)?,
isbn: ligne.get(4)?,
})
})?;
lignes.collect()
}
Pour vos élèves
Faire le lien avec la classe DAO PHP : ici, ces fonctions ne connaissent rien à l'interface graphique. On pourrait les réutiliser telles quelles dans une version en ligne de commande, exactement comme une classe DAO PHP resterait utilisable derrière une API plutôt qu'un formulaire HTML.
fn valider_titre(titre: &str) -> Result<String, String> {
let titre = titre.trim();
if titre.is_empty() {
Err("Le titre ne peut pas être vide.".to_string())
} else {
Ok(titre.to_string())
}
}
fn valider_annee(annee: &str) -> Result<i32, String> {
let annee = annee.trim();
if annee.is_empty() {
Err("L'année ne peut pas être vide.".to_string())
} else {
match annee.parse::<i32>() {
Ok(valeur) if (0..=2100).contains(&valeur) => Ok(valeur),
Ok(_) => Err("L'année doit être comprise entre 0 et 2100.".to_string()),
Err(_) => Err("L'année doit être un nombre entier.".to_string()),
}
}
}
Result<T, String> vs validation PHP
Le compilateur force à traiter le cas d'erreur : impossible
d'ignorer un Result sans avertissement. En PHP, un if de
validation oublié compile et s'exécute sans broncher. Bon point
de discussion sur la philosophie de sûreté de Rust.
impl eframe::App for MonApplication {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ui, |ui| {
// formulaire (Grid), validation, bouton Ajouter/Enregistrer
// -> à compléter par vos soins : reprendre le principe du
// formulaire avec validation vu dans l'exercice précédent,
// en appelant valider_titre() / valider_annee() avant
// d'insérer ou de mettre à jour un livre.
// Tableau "façon tableur"
let livres_affiches = self.livres.clone();
let mut id_a_modifier: Option<i64> = None;
let mut id_a_supprimer: Option<i64> = None;
TableBuilder::new(ui)
.striped(true)
.column(Column::auto().at_least(30.0))
.column(Column::remainder().at_least(120.0))
// ...
.body(|mut body| {
for livre in &livres_affiches {
body.row(24.0, |mut row| {
row.col(|ui| { ui.label(livre.id.to_string()); });
// ...
row.col(|ui| {
if ui.small_button("Modifier").clicked() {
id_a_modifier = Some(livre.id);
}
if ui.small_button("Supprimer").clicked() {
id_a_supprimer = Some(livre.id);
}
});
});
}
});
// Actions différées : voir encadré ci-dessous
});
}
}
Pourquoi cloner self.livres avant le tableau ?
Le compilateur refuse de modifier self (via les boutons
Modifier/Supprimer) pendant qu'on itère sur self.livres :
c'est le borrow checker qui protège contre un accès
concurrent aux mêmes données. Solution : on clone la liste
pour l'affichage, on note l'action demandée dans une
variable (id_a_modifier, id_a_supprimer), et on l'applique
après la boucle. Excellent cas concret pour faire toucher
du doigt ce que "ownership" et "emprunt" signifient en pratique.
Assembler son fichier complet
À vous de jouer
Il n'y a pas de fichier tout prêt à télécharger : c'est volontaire.
Le but est de reconstituer vous-même bibliotheque_egui_sqlite.rs
en assemblant, dans cet ordre, les blocs de la section précédente
dans un seul fichier src/main.rs :
- Imports & structure (
use ...,struct MonApplication,impl Default) - Modèle & accès aux données (
struct Livre,ouvrir_connexion,init_db,charger_livres— ajoutez aussi vos fonctionsinserer_livre,mettre_a_jour_livre,supprimer_livresur le même principe) - Validation (
valider_titre,valider_annee— et une éventuellevalider_isbn) - Interface (
impl eframe::App for MonApplication, avec la méthodeui) main()
Les parties marquées // ... ou // à compléter par vos soins dans
le bloc « Interface » sont à écrire vous-même : c'est l'exercice.
Elles reprennent des principes déjà vus (formulaire avec egui::Grid,
validation avant écriture) dans les exercices précédents.
Vérifier au fur et à mesure
Compilez après chaque bloc ajouté (cargo check) plutôt que d'attendre
d'avoir tout collé : le compilateur Rust signale immédiatement une
fonction ou un type manquant, ce qui aide à repérer l'ordre logique
des dépendances entre les blocs.
Piège rencontré : changement d'API eframe
update devenu ui depuis eframe 0.34
Un tutoriel trouvé en ligne peut viser une ancienne version
d'eframe. Depuis la version 0.34, le trait App exige une
méthode ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame)
et non plus update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame).
Symptôme : erreurs E0407 / E0046 / E0308 à la compilation.
Leçon pour les élèves : toujours vérifier la version installée
(cargo tree | grep eframe) et se référer au CHANGELOG de la
crate plutôt qu'à un tutoriel daté, surtout dans un écosystème
aussi actif que Rust.
Pour aller plus loin (exercices élèves)
Pistes d'extension
- Ajouter une confirmation avant suppression (
egui::WindowouModal) - Ajouter un champ de recherche filtrant le tableau par titre ou auteur
- Trier le tableau par colonne (cliquer sur l'en-tête)
- Comparer avec la version MariaDB : remplacer
rusqliteparsqlxoumysqlet discuter des différences (serveur à lancer, connexion réseau, gestion des identifiants)
Persistance des données
Vérification à faire en classe
Contrairement au formulaire seul (exercice précédent) qui perdait
tout en mémoire à la fermeture, ici les données survivent : fermer
la fenêtre, relancer cargo run, et constater que le catalogue
est toujours là — preuve que bibliotheque.db a bien persisté sur
le disque entre les deux exécutions.