Une des fonctionnalités phare requises d’un SGL est la sauvegarde de l’historique des changements appliqués aux données storées dans la base de données sous-jacente. Ceci peut représenter une fonctionnalité non triviale à implémenter et/ou déployer et il existe certainement plusieurs visions de la forme que cette implémentation devrait prendre.
Heureusement pour tous les fans de SQLAlchemy, une solution prête à l’usage est suggérée sur la page des exemples de l’ORM (en anglais seulement). Bien que la page d’exemple suggère différents types d’implémentation, ma préférée demeure Versioning with a History Table.

Elle est incroyablement simple à mettre en place et son utilisation subséquente ne requiert l’ajout d’aucune ligne de code… Fantastique !
Cette implémentation se présente sous la forme d’un mixin conçu pour fonctionner avec la technique de définition des objet nommée « declarative ». C’est cette technique qui est employée dans l’exemple ci-bas. Si vous travaillez avec du code un peu plus vieux et faisant usage des mappeurs classiques de SQLAlchemy, un ensemble de fonctions est disponible pour assurer les même fonctionnalités.

Incroyablement simple à mettre en place, son utilisation subséquente ne requiert l’ajout d’aucune ligne de code.

Laissez-moi vous démontrer sa mise en place à l’aide d’un exemple:

En gros, il ne faut qu’importer le mixin et un wrapper pour la session SQL, ajouter le mixin à notre déclaration de classe et envelopper votre session dans une versioned_session.

Cet extrait de code:








Base = declarative_base()

class MaClasse(Base):
    __tablename__ = 'ma_table'

    id = Column(Integer, primary_key=True)
    nom = Column(String(50))


Session = sessionmaker(bind=engine)




sess = Session()
# Faites quelque chose avec votre session

Devient:


# Ajout des déclarations d'import nécessaires
from history_meta import (
Versioned, 
versioned_session
)

Base = declarative_base()

class MaClasse(Versioned, Base):
    __tablename__ = 'ma_table'

    id = Column(Integer, primary_key=True)
    nom = Column(String(50))


Session = sessionmaker(bind=engine)

# enveloppez votre session dans une versioned_session
versioned_session(Session)

sess = Session()
# Faites quelque chose avec votre session

Facile, n’est-ce pas ?!
L’ajout du mixin dans la déclaration des objets instancie des tables « soeurs » (avec « _history » d’ajouté à leur nom) pour chaque objet Versioned déclaré. Ces tables contiendront une copie complète des attributs de la table d’origine en plus d’un attribut version (essentiellement un entier auto incrémental) et, optionnellement, un attribut changed de type utc_timestamp.

Une fois ces petits ajouts effectués, chaque changement ou suppression appliqué sur un objet occasionnera une écriture de l’objet *avant* sa modification dans la table soeur « _history » accompagné d’une incrémentation du champ « version ». Qui plus est: toute la cuisine est automatiquement gérée par l’ORM, sans l’ajout d’une seule ligne de code supplémentaire.

Alors comment fait-on pour accéder à l’historique d’une entrée de la BD ? Rien de plus simple ! Les lignes de code suivantes expliquent comment instancier une classe correspondant à l’historique de la classe soeur. Celle-ci s’utilise ensuite comme un objet SQLAlchemy classique:


HistoriqueDeMaClasse = MaClasse.__history_mapper__.class_
etats_precedents = HistoriqueDeMaClasse.filter(HistoriqueDeMaClasse.id == un_id)\
                  .order_by(HistoriqueDeMaClasse.version).all()

## Faites quelque chose avec les états précédents de votre objet :)

Merveilleux, non ?
C’est maintenant le temps de vous mettre au boulot et de déployer des objets avec historique dans tous les projets qui pourraient en bénéficier !