Voici une proposition de traduction complète de votre base de données relationnelle vers une architecture hybride EAV (Entity-Attribute-Value) / JSONB / Graph.
Dans cette approche moderne (très performante sur PostgreSQL, mais aussi sur Sqlite), nous utilisons :
- Une table Graph (Arêtes / Relations) pour lier les entités entre elles (ex: un utilisateur écrit un message, un hôpital possède une UF).
- Une table Centrale EAV / JSONB où chaque ligne est une entité générique, ses métadonnées fixes sont stockées en colonnes, et ses attributs variables/métiers sont encapsulés dans un objet
JSONB.
1. Structure du Modèle Cible (Le Moteur)
Ces trois tables suffisent à absorber l'intégralité de vos tables actuelles.
-- 1. Le dictionnaire des types d'entités (Hôpital, Utilisateur, Incident/Sitrep, Tâche, etc.)
CREATE TABLE entity_types (
code VARCHAR(50) PRIMARY KEY,
libelle VARCHAR(100) NOT NULL
);
-- 2. La table centrale EAV + JSONB (Les Noeuds du Graph)
CREATE TABLE entities (
id SERIAL PRIMARY KEY,
entity_type VARCHAR(50) NOT NULL REFERENCES entity_types(code),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
-- Le coeur JSONB qui remplace les centaines de colonnes spécifiques
attributes JSONB NOT NULL DEFAULT '{}',
-- Index GIN pour des recherches ultra-rapides à l'intérieur du JSONB
CONSTRAINT chk_attributes_obj CHECK (jsonb_typeof(attributes) = 'object')
);
CREATE INDEX ix_entities_type ON entities(entity_type);
CREATE INDEX ix_entities_attributes ON entities USING gin (attributes);
-- 3. La table des relations (Les Arêtes / Edges du Graph)
CREATE TABLE entity_relations (
id SERIAL PRIMARY KEY,
source_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
target_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
relation_type VARCHAR(50) NOT NULL, -- 'MEMBRE_DE', 'A_ECRIT', 'CONCERNE_INCIDENT', 'CONTIENT_UF'
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
metadata JSONB DEFAULT '{}', -- Optionnel, pour qualifier la relation (ex: rôles, dates)
UNIQUE(source_id, target_id, relation_type)
);
CREATE INDEX ix_rel_source ON entity_relations(source_id);
CREATE INDEX ix_rel_target ON entity_relations(target_id);
CREATE INDEX ix_rel_type ON entity_relations(relation_type);
2. Initialisation des Types d'Entités
Avant d'insérer, on déclare les types d'objets qui composent votre application :
INSERT INTO entity_types (code, libelle) VALUES
('HOSPITAL', 'Établissement Hospitalier'),
('UNITE_FONCTIONNELLE', 'Unité Fonctionnelle'),
('USER', 'Utilisateur de la plateforme'),
('LOG', 'Main courante / Journal d''événement'),
('SITREP', 'Situation d''urgence / Incident'),
('DECISION', 'Décision de crise'),
('PRESENCE', 'Registre de présence'),
('CONSIGNE', 'Consigne d''équipe'),
('SERVICE_STATUS', 'Statut d''un service'),
('CAPACITE_REF', 'Référentiel de capacité'),
('CAPACITE_DECLARATION', 'Déclaration de lits'),
('CAPACITE_MEDICOTECH', 'Capacité Médico-technique'),
('TRANSFERT_PATIENT', 'Demande de transfert patient'),
('DEMANDE_INTERGHT', 'Demande Inter-GHT'),
('NOTIFICATION', 'Notification système'),
('CHAT_SALON', 'Salon de discussion'),
('CHAT_MESSAGE', 'Message de chat'),
('MISSION_BRC', 'Mission Brancardage'),
('TUTEUR_SESSION', 'Session de formation Tuteur AI');
3. Dictionnaire de Traduction (Mapping Relationnel ➔ JSONB/Graph)
Voici comment vos données textuelles et numériques se restructurent concrètement. Au lieu de créer des tables, vous insérez dans entities avec la structure JSON suivante :
Écosystème Structure & Capacités
-
HOSPITAL
-
Attributes JSONB : {"nom": "...", "code_finess": "...", "latitude": 48.8, "longitude": 2.3, "adresse": "...", "telephone_garde": "..."}
-
UNITE_FONCTIONNELLE
-
Attributes JSONB : {"code_uf": "...", "libelle": "...", "pole": "...", "actif": true}
-
Graph Relation : (UNITE_FONCTIONNELLE) -[APPARTIENT_A]-> (HOSPITAL)
-
CAPACITE_REFERENTIEL
-
Attributes JSONB : {"service_nom": "...", "uf_code": "...", "pole": "...", "site": "...", "capacite_totale": 20, "config_genre": {"homme": true, "femme": true, "indiffer": true}, "telephone_cadre": "...", "ordre_affichage": 1}
Utilisateurs & Communications
-
USER
-
Attributes JSONB : {"username": "...", "display_name": "...", "role": "...", "password_hash": "...", "perimetre": "...", "security": {"mfa_enabled": true, "mfa_secret": "...", "backup_codes": [...]}}
-
*Note pour user_online_status* : À intégrer directement en attribut dynamique du User : {"status": {"online": true, "last_seen": "2026-06-06T..."}}
-
CHAT_MESSAGE
-
Attributes JSONB : {"contenu": "...", "auteur_nom_copie": "...", "pieces_jointes": [{"nom": "file.pdf", "taille": 1024, "path": "..."}]}
-
Graph Relation :
-
(CHAT_MESSAGE) -[POSTE_DANS]-> (CHAT_SALON)
-
(USER) -[A_ECRIT]-> (CHAT_MESSAGE)
-
(CHAT_MESSAGE) -[REPOND_A]-> (CHAT_MESSAGE) (remplace reply_to_id)
Gestion de Crise (Sitrep, Tâches, Rex)
-
SITREP (Incident)
-
Attributes JSONB : {"type_crise": "...", "urgency": 3, "faits": "...", "analyse": "...", "actions": {"remediation": "...", "moyens_engages": "..."}, "statut": "En cours", "completion_percent": 45, "dates": {"estimated_resolution": "...", "resolved_at": "..."}}
-
Graph Relation : (USER/Declarant) -[A_DECLARE]-> (SITREP)
-
TASK
-
Attributes JSONB : {"titre": "...", "description": "...", "assignee": "...", "priorite": 1, "colonne": "TODO", "due_at": "..."}
-
Graph Relation : (TASK) -[AFFECTE_A]-> (SITREP)
-
REX_ENTRY
-
Attributes JSONB : {"kpis": {"duree_minutes": 120, "mttd": 15, "mttr": 105}, "analyse": {"points_positifs": "...", "points_amelio": "...", "lecons": "..."}}
-
Graph Relation : (REX_ENTRY) -[EVALUE]-> (SITREP)
4. Exemple Concret de Requêtage (Avantages du modèle)
Exemple 1 : Insérer un Sitrep lié à un Hôpital et un Déclarant
Plus besoin de jointures complexes basées sur des IDs de tables éparpillées.
-- 1. On crée le Sitrep
INSERT INTO entities (entity_type, attributes)
VALUES ('SITREP', '{"type_crise": "Cyberattaque", "urgency": 5, "faits": "Ransomware sur le réseau principal"}')
RETURNING id; -- Disons qu'il génère l'ID 42
-- 2. On crée le lien Graph avec l'hôpital (ID 10) et l'utilisateur (ID 5)
INSERT INTO entity_relations (source_id, target_id, relation_type) VALUES
(42, 10, 'IMPACTE_HOPITAL'),
(5, 42, 'A_DECLARE');
Exemple 2 : Requête JSONB (Trouver toutes les UFs d'un pôle spécifique)
SELECT id, attributes->>'libelle' as nom_uf
FROM entities
WHERE entity_type = 'UNITE_FONCTIONNELLE'
AND attributes->>'pole' = 'Urgences';
Exemple 3 : Requête Graph (Trouver toutes les tâches liées à un Sitrep spécifique)
SELECT e_task.id, e_task.attributes->>'titre' AS tache
FROM entities e_sitrep
JOIN entity_relations r ON e_sitrep.id = r.target_id
JOIN entities e_task ON r.source_id = e_task.id
WHERE e_sitrep.id = 42
AND r.relation_type = 'AFFECTE_A';
💡 Pourquoi ce choix est excellent pour votre projet :
- Évolutivité sans douleur (No-migration) : Si demain le module
plugin_tuteur ou l'IA (ia_sessions) a besoin de stocker 15 nouveaux indicateurs, vous n'avez aucun ALTER TABLE à faire. Vous injectez simplement les nouvelles clés dans l'objet attributes en JSONB.
- Modularité des plugins : Vos tables
plugin_brc_* et plugin_tuteur_* se fondent naturellement dans le décor sous forme d'entités avec leurs propres types. Les relations historiques (plugin_brc_historique) deviennent des arêtes graph ou des tableaux d'historiques embarqués dans le JSONB.
Voici une proposition de traduction complète de votre base de données relationnelle vers une architecture hybride EAV (Entity-Attribute-Value) / JSONB / Graph.
Dans cette approche moderne (très performante sur PostgreSQL, mais aussi sur Sqlite), nous utilisons :
JSONB.1. Structure du Modèle Cible (Le Moteur)
Ces trois tables suffisent à absorber l'intégralité de vos tables actuelles.
2. Initialisation des Types d'Entités
Avant d'insérer, on déclare les types d'objets qui composent votre application :
3. Dictionnaire de Traduction (Mapping Relationnel ➔ JSONB/Graph)
Voici comment vos données textuelles et numériques se restructurent concrètement. Au lieu de créer des tables, vous insérez dans
entitiesavec la structure JSON suivante :Écosystème Structure & Capacités
HOSPITAL
Attributes JSONB :
{"nom": "...", "code_finess": "...", "latitude": 48.8, "longitude": 2.3, "adresse": "...", "telephone_garde": "..."}UNITE_FONCTIONNELLE
Attributes JSONB :
{"code_uf": "...", "libelle": "...", "pole": "...", "actif": true}Graph Relation :
(UNITE_FONCTIONNELLE) -[APPARTIENT_A]-> (HOSPITAL)CAPACITE_REFERENTIEL
Attributes JSONB :
{"service_nom": "...", "uf_code": "...", "pole": "...", "site": "...", "capacite_totale": 20, "config_genre": {"homme": true, "femme": true, "indiffer": true}, "telephone_cadre": "...", "ordre_affichage": 1}Utilisateurs & Communications
USER
Attributes JSONB :
{"username": "...", "display_name": "...", "role": "...", "password_hash": "...", "perimetre": "...", "security": {"mfa_enabled": true, "mfa_secret": "...", "backup_codes": [...]}}*Note pour
user_online_status*: À intégrer directement en attribut dynamique du User :{"status": {"online": true, "last_seen": "2026-06-06T..."}}CHAT_MESSAGE
Attributes JSONB :
{"contenu": "...", "auteur_nom_copie": "...", "pieces_jointes": [{"nom": "file.pdf", "taille": 1024, "path": "..."}]}Graph Relation :
(CHAT_MESSAGE) -[POSTE_DANS]-> (CHAT_SALON)(USER) -[A_ECRIT]-> (CHAT_MESSAGE)(CHAT_MESSAGE) -[REPOND_A]-> (CHAT_MESSAGE)(remplacereply_to_id)Gestion de Crise (Sitrep, Tâches, Rex)
SITREP (Incident)
Attributes JSONB :
{"type_crise": "...", "urgency": 3, "faits": "...", "analyse": "...", "actions": {"remediation": "...", "moyens_engages": "..."}, "statut": "En cours", "completion_percent": 45, "dates": {"estimated_resolution": "...", "resolved_at": "..."}}Graph Relation :
(USER/Declarant) -[A_DECLARE]-> (SITREP)TASK
Attributes JSONB :
{"titre": "...", "description": "...", "assignee": "...", "priorite": 1, "colonne": "TODO", "due_at": "..."}Graph Relation :
(TASK) -[AFFECTE_A]-> (SITREP)REX_ENTRY
Attributes JSONB :
{"kpis": {"duree_minutes": 120, "mttd": 15, "mttr": 105}, "analyse": {"points_positifs": "...", "points_amelio": "...", "lecons": "..."}}Graph Relation :
(REX_ENTRY) -[EVALUE]-> (SITREP)4. Exemple Concret de Requêtage (Avantages du modèle)
Exemple 1 : Insérer un Sitrep lié à un Hôpital et un Déclarant
Plus besoin de jointures complexes basées sur des IDs de tables éparpillées.
Exemple 2 : Requête JSONB (Trouver toutes les UFs d'un pôle spécifique)
Exemple 3 : Requête Graph (Trouver toutes les tâches liées à un Sitrep spécifique)
💡 Pourquoi ce choix est excellent pour votre projet :
plugin_tuteurou l'IA (ia_sessions) a besoin de stocker 15 nouveaux indicateurs, vous n'avez aucunALTER TABLEà faire. Vous injectez simplement les nouvelles clés dans l'objetattributesen JSONB.plugin_brc_*etplugin_tuteur_*se fondent naturellement dans le décor sous forme d'entités avec leurs propres types. Les relations historiques (plugin_brc_historique) deviennent des arêtes graph ou des tableaux d'historiques embarqués dans le JSONB.