Skip to content

proposition de traduction complète de votre base de données relationnelle vers une architecture hybride **EAV (Entity-Attribute-Value) / JSONB / Graph**. #10

Description

@graphtwinio

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 :

  1. 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).
  2. 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 :

  1. É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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions