generic_relation_helpers
Package Rust pour construire facilement des objets enrichis à partir de données liées entre elles — avec ou sans base de données.
À qui s'adresse ce package ?
Ce package s'adresse à tout développeur Rust qui travaille avec des données réparties dans plusieurs tables ou plusieurs listes, et qui souhaite les combiner proprement pour les utiliser ou les retourner via une API.
Le problème de départ — comprendre avant de coder
Imagine une plateforme scolaire
Tu développes une application pour une école. Les élèves s'inscrivent à des cours. Ces informations sont stockées dans une base de données, réparties dans 3 tables séparées :
Table Eleve
┌─────────────────┬───────────────────────┐
│ nom_complet │ email │
├─────────────────┼───────────────────────┤
│ Lucas Martin │ lucas@ecole.fr │
│ Emma Dubois │ emma@ecole.fr │
│ Noah Bernard │ noah@ecole.fr │
└─────────────────┴───────────────────────┘
Table Cours
┌──────────────────┬──────────────────────────────┐
│ intitule │ description │
├──────────────────┼──────────────────────────────┤
│ Mathématiques │ Algèbre et géométrie │
│ Informatique │ Bases de la programmation │
│ Anglais │ Langue et littérature │
└──────────────────┴──────────────────────────────┘
Table Inscription ← table de liaison (qui suit quel cours ?)
┌─────────────────┬──────────────────┐
│ nom_eleve │ intitule_cours │
├─────────────────┼──────────────────┤
│ Lucas Martin │ Mathématiques │
│ Lucas Martin │ Informatique │
│ Emma Dubois │ Informatique │
│ Emma Dubois │ Anglais │
│ Noah Bernard │ Mathématiques │
└─────────────────┴──────────────────┘
Ton objectif : retourner à l'application ou à l'API un objet qui ressemble à ça :
Chaque élève est accompagné de la liste détaillée de ses cours — pas juste leurs noms, mais toutes leurs informations complètes.
Sans le package — ce que tu dois écrire toi-même
Sans outil dédié, tu dois écrire manuellement le code qui croise les 3 listes entre elles. Voici à quoi ça ressemble :
// ── Étape 1 : charger les 3 tables séparément ──
let eleves = vec!;
let cours = vec!;
let inscriptions = vec!;
// ── Étape 2 : construire manuellement les objets combinés ──
let mut resultat: = Vecnew;
for eleve in &eleves
Les problèmes de cette approche
Problème 1 — C'est long à écrire. 3 boucles imbriquées pour une simple combinaison. Si ton application a 10 types de relations (élèves/cours, professeurs/classes, classes/salles...), tu écris ce code 10 fois.
Problème 2 — C'est fragile.
Si tu renommes le champ nom_eleve en identifiant_eleve, tu dois retrouver et corriger ce code à la main — et espérer ne rien oublier.
Problème 3 — Ce n'est pas réutilisable.
Ce code ne fonctionne que pour Eleve et Cours. Pour Professeur et Matiere, tu réécriras exactement les mêmes 3 boucles avec des noms différents.
Problème 4 — Difficile à lire. Un développeur qui découvre ce code doit décortiquer les 3 boucles imbriquées avant de comprendre ce que fait la fonction. L'intention — "combiner des élèves avec leurs cours" — est noyée dans les détails techniques.
Avec le package — ce que tu écris à la place
use get_parents_with_their_childrens;
// ── Étape 1 : charger les 3 tables séparément (identique) ──
let eleves = vec!;
let cours = vec!;
let inscriptions = vec!;
// ── Étape 2 : une seule ligne pour tout combiner ──
let resultat: = get_parents_with_their_childrens;
C'est tout. Le package fait les 3 boucles à ta place, de manière générique, pour n'importe quel type de données.
La différence en un coup d'œil
Sans le package Avec le package
───────────────────────────────── ──────────────────────────────────
~20 lignes de code par relation 1 ligne par relation
À réécrire pour chaque entité Réutilisable pour tous les types
Logique mêlée au métier Logique séparée et invisible
Risque d'oubli ou d'erreur Testé et fiable
Comment le package sait quoi combiner ?
Le package ne connaît pas tes structs à l'avance. Tu lui expliques une seule fois, via de petits traits (des fiches d'identité), comment accéder aux informations dont il a besoin.
C'est quoi un trait ? En Rust, un trait est un contrat que tu fais signer à une struct. Tu dis : "cette struct sait répondre à ces questions". Une fois le contrat rempli, le package peut utiliser ta struct sans la connaître à l'avance.
Voici les fiches à remplir pour notre exemple école :
// ── Fiche 1 : "Comment identifier un élève ?" ──
// ── Fiche 2 : "Quels champs exposer pour l'élève ?" ──
// ── Fiche 3 : "Comment appeler la liste des cours dans l'objet final ?" ──
// ── Fiche 4 : "Comment relier une inscription à un cours ?" ──
// ── Fiche 5 : "Quelle colonne de la table Inscription pointe vers l'élève,
// et laquelle pointe vers le cours ?" ──
// ── Fiche 6 : "Comment construire un objet Cours depuis une HashMap ?" ──
// ── Fiche 7 : "Comment construire l'objet final EleveAvecCours ?" ──
Tu remplis ces fiches une seule fois par type de relation.
Ensuite, tu appelles get_parents_with_their_childrens autant de fois que tu veux, pour autant de relations que tu as dans ton application.
Les types de relations supportées ✅
Type 1 — Relation N:N via table de liaison
Un élève suit plusieurs cours, un cours accueille plusieurs élèves.
Eleve ──── Inscription ──── Cours
(table de liaison)
get_parents_with_their_childrens
Type 2 — Relation 1:N avec FK directe dans l'enfant
Un professeur publie plusieurs articles, chaque article appartient à un seul professeur.
Professeur ──── Article
(champ nom_professeur dans Article)
Il n'y a pas de table de liaison séparée. Article contient directement le nom du professeur. On passe donc Article deux fois :
let articles = /* charger Article */;
let articles_clone = articles.clone; // ← copie nécessaire
get_parents_with_their_childrens
// ↑ filtre ↑ données enrichies
Type 3 — Relation N:N avec attributs sur la liaison
Un élève obtient une note et une date d'examen pour chaque cours — ces informations sont des attributs de la liaison, pas du cours lui-même.
┌─── Cours ───────────────────────────┐
│ intitule: "Mathématiques" │
Eleve ──── Inscription ──────────────>│ description: "Algèbre et géométrie" │
note: 17 └─────────────────────────────────────┘
date_examen: 2025-06-15
noteetdate_examenappartiennent àInscription.intituleetdescriptionappartiennent àCours.
Les attributs de Inscription (note, date_examen) sont fusionnés automatiquement dans l'objet Cours retourné — sans code supplémentaire de ta part. Le résultat final combine les champs des deux tables :
Les types de relations non supportées ❌
Certaines configurations de relations entre tables sont en dehors du périmètre de ce package. Voici lesquelles, et pourquoi.
❌ Relation 1:1 — deux tables liées par une seule correspondance
Exemple : Un utilisateur a exactement un profil, un profil appartient à exactement un utilisateur.
Utilisateur ──── Profil
(un seul profil par utilisateur)
Pourquoi non supporté ?
Le package est conçu pour associer un parent à une liste d'enfants (Vec<TEnfant>). Dans une relation 1:1, il n'y a pas de liste — juste un objet unique en face. Utiliser le package pour ça fonctionnerait techniquement mais retournerait un Vec avec un seul élément, ce qui est sémantiquement incorrect et inutilement complexe.
Contournement : Une simple requête avec jointure ou un accès direct suffit.
❌ Relation récursive — une table liée à elle-même
Exemple : Un employé a un manager, qui est lui-même un employé de la même table.
Employe ──── Employe
id: 1 id: 2 (manager de l'employé 1)
manager_id: 2
Pourquoi non supporté ?
Le package distingue clairement un type "parent" et un type "enfant". Ici, Employe joue les deux rôles simultanément — parent et enfant sont la même struct dans la même table. Le système de traits génériques ne peut pas résoudre cette ambiguïté sans conflit de types.
Contournement : Une requête SQL récursive (WITH RECURSIVE) ou une fonction Rust dédiée qui parcourt l'arborescence manuellement.
❌ Relation ternaire — 3 tables liées simultanément par une seule liaison
Exemple : Un professeur enseigne un cours dans une salle spécifique — les trois sont liés en même temps dans une seule table de liaison.
Professeur ──── Planning ──── Cours
│
Salle
Table Planning
┌──────────────────┬──────────────────┬───────────┐
│ nom_professeur │ intitule_cours │ nom_salle │
├──────────────────┼──────────────────┼───────────┤
│ M. Durand │ Mathématiques │ Salle A │
└──────────────────┴──────────────────┴───────────┘
Pourquoi non supporté ?
Le package est conçu pour relier deux entités entre elles (un parent et ses enfants). Une relation ternaire implique 3 entités liées simultanément dans la même ligne de jointure. La fonction get_parents_with_their_childrens n'accepte qu'un seul type de parent et un seul type d'enfant — pas trois.
Contournement : Décomposer en deux appels successifs — d'abord lier Professeur à Cours, puis enrichir chaque Cours avec sa Salle.
❌ Relation polymorphique — une liaison qui pointe vers plusieurs types différents
Exemple : Un commentaire peut appartenir soit à un article, soit à une vidéo, soit à une photo — selon un champ
type_cible.
Commentaire
cible_id: 42
type_cible: "Article" ← ou "Video", ou "Photo"
Pourquoi non supporté ? Rust est un langage à typage statique fort. Le package doit connaître à la compilation le type exact du parent et de l'enfant. Une relation polymorphique implique que le type de la cible varie à l'exécution selon la valeur d'un champ — ce que le système de types de Rust ne peut pas résoudre avec des génériques simples.
Contournement : Gérer chaque type de cible séparément avec trois appels distincts, filtrés par type_cible.
❌ Relation avec héritage de table — parent et enfants dans des tables séparées par sous-type
Exemple : Une table
Personneavec des sous-typesEtudiantetEnseignantdans des tables séparées.
Personne ──── Etudiant (données spécifiques à l'étudiant)
└─── Enseignant (données spécifiques à l'enseignant)
Pourquoi non supporté ? Ce pattern (appelé "héritage de table" ou table inheritance) suppose qu'une même entité est décrite par plusieurs tables selon son sous-type. Le package ne gère qu'une seule table source par rôle (parent, liaison, enfant) — il ne sait pas fusionner deux tables pour reconstituer un objet complet avant de l'utiliser.
Contournement : Reconstituer manuellement les structs complètes avant de les passer au package, ou utiliser une vue SQL qui fusionne les tables.
Résumé des cas supportés et non supportés
| Type de relation | Supporté |
|---|---|
| N:N via table de liaison | ✅ |
| 1:N avec FK directe dans l'enfant | ✅ |
| N:N avec attributs sur la liaison | ✅ |
| 1:1 (un parent, un enfant unique) | ❌ |
| Récursive (une table liée à elle-même) | ❌ |
| Ternaire (3 entités liées en même temps) | ❌ |
| Polymorphique (type de cible variable) | ❌ |
| Héritage de table (sous-types séparés) | ❌ |
Le filtrage intégré
Chaque appel peut être filtré directement, sans requête supplémentaire vers la base de données :
// Retourner uniquement Lucas Martin
get_parents_with_their_childrens
// Retourner tous les élèves dont le nom commence par "E"
get_parents_with_their_childrens
| Mode | Comportement | Exemple |
|---|---|---|
None |
Retourne tout | — |
"equal" |
Nom exactement égal | "Lucas Martin" → Lucas Martin |
"begin" |
Nom commence par... | "Em" → Emma Dubois |
"contains" |
Nom contient... | "oa" → Noah Bernard |
"finished" |
Nom se termine par... | "Dubois" → Emma Dubois |
Tous les filtres sont insensibles à la casse — "lucas martin" trouve "Lucas Martin".
Utilisation sans base de données
Le package fonctionne avec n'importe quelle source de données — pas seulement une base de données relationnelle.
// ── Depuis une API externe ──
let eleves: = appel_api.await;
let cours: = appel_api.await;
let inscriptions: = appel_api.await;
let resultat = get_parents_with_their_childrens;
// ── Depuis des données en mémoire (tests unitaires) ──
let eleves = vec!;
let cours = vec!;
let inscriptions = vec!;
let resultat = get_parents_with_their_childrens;
// ── Depuis Diesel + PostgreSQL ──
let eleves = eleve..unwrap;
let cours = cours_table..unwrap;
let inscriptions = inscription..unwrap;
let resultat = get_parents_with_their_childrens;
Dans les trois cas, l'appel get_parents_with_their_childrens est identique — seule la source des données change.
Fonctions disponibles
// ── Combiner une liste de parents avec leurs enfants ──
Helpers de conversion inclus
Pour lire les valeurs depuis une HashMap dans tes implémentations de build() :
get_string // → String (chaîne vide si absent)
get_string_opt // → Option<String> (None si absent)
get_bool // → bool (false si absent)
get_i64 // → i64 (0 si absent)
get_f64 // → f64 (0.0 si absent)
get_time // → Option<NaiveTime>
get_date // → Option<NaiveDate>
get_datetime // → Option<NaiveDateTime>
Installation
[]
= "0.1.0"
Compatibilité testée
| Source de données | Statut |
|---|---|
| Diesel + PostgreSQL | ✅ Testé |
| Données en mémoire (tests) | ✅ Testé |
| API externe (JSON désérialisé) | ✅ Compatible |
| SQLx | ✅ Compatible |
| Fichiers CSV / JSON | ✅ Compatible |
Licence
MIT