Dans l'architecture logicielle moderne, l'intégrité et la traçabilité des données sont des exigences non négociables. Alors que beaucoup de développeurs traitent les journaux d'audit comme une pensée après-coup — en se contentant d'ajouter des lignes à une table `audit_log` séparée lorsque des changements surviennent — une approche plus robuste est l'Event Sourcing. En traitant les changements d'état comme des événements immuables, vous obtenez un historique complet et rejouable de l'évolution de votre application.
Cet article explore comment implémenter un motif d'Event Sourcing spécifiquement conçu pour les journaux d'audit au sein de systèmes de bases de données relationnelles traditionnels (comme PostgreSQL ou MySQL), en tirant parti des fonctionnalités SQL pour garantir l'immuabilité des données et les performances.
Le concept fondamental : Événements vs État
Traditionnellement, les bases de données relationnelles stockent l'état actuel des entités. Si l'e-mail d'un utilisateur change, l'ancien e-mail est écrasé. Avec l'Event Sourcing, nous ne stockons pas l'état ; nous stockons les événements qui ont créé cet état. À des fins d'audit, cela signifie que chaque création, mise à jour et suppression est enregistrée en tant qu'enregistrement distinct et en lecture seule (append-only).
Cette approche offre plusieurs avantages :
- Responsabilité complète : Vous savez exactement ce qui a changé, qui l'a changé et quand.
- Rejouabilité : Vous pouvez reconstituer l'état de n'importe quel enregistrement à n'importe quel moment.
- Conformité : Répond aux exigences réglementaires strictes en matière de lignée des données (par exemple, RGPD, HIPAA).
Conception du schéma pour des événements immuables
La base de ce motif est une table dédiée audit_events. Contrairement aux tables standard, cette table doit imposer l'immuabilité. Dans les versions modernes de PostgreSQL, nous pouvons y parvenir élégamment en utilisant WITHOUT OVERWRITE ou en restreignant entièrement les permissions DELETE et UPDATE.
Voici une définition de schéma robuste :
CREATE TABLE audit_events (
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_id UUID NOT NULL, -- L'ID de l'entité modifiée (ex: user_id)
event_type VARCHAR(50) NOT NULL, -- ex: 'USER_CREATED', 'EMAIL_UPDATED'
payload JSONB NOT NULL, -- Les changements de données
metadata JSONB, -- Contexte supplémentaire comme l'adresse IP, l'agent utilisateur
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_audit_no_delete CHECK (true) -- Imposé via les permissions
);
-- Indexes pour une récupération rapide
CREATE INDEX idx_audit_aggregate ON audit_events(aggregate_id, created_at DESC);
CREATE INDEX idx_audit_type ON audit_events(event_type);
Imposer l'immuabilité avec les permissions de la base de données
Les vérifications au niveau du code peuvent être contournées. Pour garantir une véritable immuabilité, nous devons restreindre les opérations directes DELETE et UPDATE sur la table audit_events au niveau de la base de données.
-- Révoquer les permissions d'écriture directe pour les utilisateurs de l'application
REVOKE DELETE, UPDATE ON audit_events FROM app_user;
-- Créer une fonction pour insérer des événements en toute sécurité
CREATE OR REPLACE FUNCTION insert_audit_event(
p_aggregate_id UUID,
p_event_type VARCHAR,
p_payload JSONB,
p_metadata JSONB
) RETURNS VOID AS $$
BEGIN
INSERT INTO audit_events (aggregate_id, event_type, payload, metadata)
VALUES (p_aggregate_id, p_event_type, p_payload, p_metadata);
END;
$$ LANGUAGE plpgsql;
Reconstitution de l'état : Le modèle en lecture
L'un des défis de l'Event Sourcing est la lecture des données. Puisque nous ne stockons que des événements, nous devons calculer l'état actuel. Bien que cela puisse être fait dans le code de l'application, SQL peut gérer cela efficacement à l'aide de fonctions de fenêtrage.
Pour obtenir la dernière version du profil d'un utilisateur, vous pouvez utiliser une expression de table commune (CTE) pour récupérer l'événement le plus récent pour chaque agrégat :
WITH latest_events AS (
SELECT
aggregate_id,
event_type,
payload,
created_at,
ROW_NUMBER() OVER (PARTITION BY aggregate_id ORDER BY created_at DESC) as rn
FROM audit_events
WHERE aggregate_id = 'user-123-uuid'
)
SELECT payload, created_at
FROM latest_events
WHERE rn = 1;
Mise en œuvre pratique au niveau de l'application
Dans votre code d'application, vous devez envelopper la logique métier qui modifie les données dans une transaction. Lorsqu'un utilisateur met à jour son profil, vous ne mettez pas seulement à jour la table users ; vous invoquez également la procédure stockée insert_audit_event.
Par exemple, dans un service Python ou Node.js :
- Démarrer la transaction
- Exécuter la logique métier (Mettre à jour la table
users) - Enregistrer l'événement (Appeler la procédure stockée)
- Valider la transaction
Cela garantit que le journal d'audit et le changement de données réel se produisent ensemble. Si la logique métier échoue, l'enregistrement d'audit est annulé, empêchant la création d'événements orphelins.
Conclusion
La mise en œuvre de l'Event Sourcing pour les journaux d'audit dans les bases de données relationnelles fournit un mécanisme puissant pour maintenir l'intégrité des données et la conformité. En séparant le stockage des événements de l'état actuel, vous créez un système qui est non seulement plus transparent, mais aussi intrinsèquement plus robuste. Bien que cela ajoute de la complexité au chemin de lecture, les avantages d'un historique immuable et rejouable l'emportent largement sur la surcharge pour les systèmes critiques. Commencez par une simple table audit_events, imposez l'immuabilité au niveau de la base de données, et construisez progressivement vos capacités de rejouabilité à mesure que vos besoins évoluent.