Nous avons vu dans la première partie de l’article ce que sont les tables temporelles, quels sont leurs intérêts et comment les manipuler via des commandes SQL, puis dans une seconde partie comment les utiliser avec EF Core grâce à l’extension NuGet EfCoreTemporalTable. Dans cette troisième partie nous allons voir comment les utiliser nativement via une nouveauté dans l’ORM Entity Framework Core (EF Core) 6+.
Quoi de neuf sur la temporalité dans EF Core 6 ?
Suite au manque de support des tables temporelles de l’ORM et face à la forte demande de la communauté (GitHub #4693), Entity Framework les supporte enfin depuis la version disponible en date de Novembre 2021 :
- Création de tables temporelles à l’aide de migrations
- Transformation de tables existantes en tables temporelles, à nouveau à l’aide de migrations
- Interrogation des données historique
- Restauration des données à partir d’un point antérieur dans le passé
Toutes ces opérations étaient auparavant plus ou moins réalisables via des extensions NuGet, dorénavant elles font enfin directement parties d’EF Core !
Nous allons donc dans un premier temps créer une logique similaire au précédent article (utilisant des extensions NuGet) mais avec cette fois EF Core 6, en utilisant par ailleurs les migrations.
Création du projet
Création d’une application Console de démo avec .NET 6.0
Installation des NuGet EF SQL Server :
Création du modèle – Une entreprise possèdes des employés :
Création du contexte :
Nous avons décomposé la création du modèle en 2 parties, d’abord la partie sans temporalité puis en toute fin l’ajout de temporalité (IsTemporal()) :
- Employe : Utilisation des défauts
- Entreprise : Utilisation d’un nom spécifique à la table historique (EntrepriseHistorique) ainsi que pour les colonnes temporelles (ValideDu et ValideAu).
Création d’une migration
Nous allons maintenant vérifier si les migrations EF Core 6 supportent en effet la temporalité.
Initialisation d’une migration EF (doc officielle migrations) :
Cette commande va vous générer 2 fichiers dans le répertoire « Migrations » :
- 20211211104839_Initial.cs
- DemoTemporelleContextModelSnapshot.cs
Vérifions ce que EF nous génère dans « 20211211104839_Initial.cs » :
Nous pouvons remarquer :
- La temporalité est bien prise en compte
- Pour Employe nous n’avions pas spécifié de nom de colonne temporelle, elle seront par défaut nommées « PeriodStart » et « PeriodeEnd »
- Pour Entreprise en revanche nous avions spécifié des noms temporels personnalisés et ils semblent effectivement pris en compte, idem pour le nom de table historique
Générons la base de données dans SQL Server :
Nous retrouvons bien la génération de notre schéma avec la temporalité :
Ajout d’un jeu de données
Nous allons ajouter 2 entreprises et 3 employés ainsi :
Puis modifier ces données :
A noter que nous avons effectué une pause de 5 secondes entre ajout/modification afin de mieux distinguer les périodes temporelles.
Vérifions ce que contient la base de données :
Nous retrouvons bien nos 3 modifications historisées, effectuons maintenant ce travail avec EF Core 6.
Requêtage sans temporalité
Affichage des noms employé/entreprise actuels :
Résultat :
Nous retrouvons donc les dernières données présentes en base. Vérifions maintenant les anciennes données.
Requêtage avec temporalité
Données initialement ajoutées
Retrouvons l’ensemble des données initialement ajoutées :
Attention à bien utiliser des dates UTC lors du requêtage temporel, aussi bien côté Entity Framework que SQL. Pour convertir une date C# en UTC il suffit d’utiliser .ToUniversalTime().
Résultats :
Nous retrouvons bien les données que nous avions initialement insérées.
Les tables jointes bénéficient automatiquement du filtre de temporalité, le comportement étant identique avec l’utilisation d’Include().
Ensemble des données
Résultats :
Nous retrouvons cette fois cinq lignes. Pour rappel nous avions effectué les modifications suivantes :
- « Patricia Martin » change de nom en « Martin Durant »
- « Jacques Dupont » rejoint « Webnet »
- Renommer « Microsoft » en « Microsoft France »
Sur nos trois modifications, deux concernaient l’employé (changement nom + changement entreprise associée) et une concernait l’Entreprise (changement nom). Puisque la seule table de notre requête était Employe nous affichons toutes les modifications ayant eu lieu seulement sur cette table, soit trois insertions et deux modifications, soit cinq lignes.
Il est possible de récupérer les valeurs des colonnes temporelles via EF.Property<DateTime>(i, « PeriodStart »), nous en reparlerons en fin d’article.
Gestion des migrations de modification de colonnes de tables temporelles
Nous allons voir si les migrations fonctionnent avec l’ajout puis la suppression d’une colonne Email :
Migration d’ajout de colonne SQL :
Code généré par EF Core :
Exécution de la migration :
La migration fonctionne correctement et la table « EmployeHistory » contient également notre nouvelle colonne, supprimons-là du modèle puis :
La colonne a correctement été supprimée des tables « Employe » et « EmployeHistory ».
Attention : Commenter les lignes « EnsureDeleted() » et « EnsureCreated() » lors du test des migrations, le cas échéant la table d’historique des migrations « [__EFMigrationsHistory] » sera supprimée et EF tentera en vain de réexécuter toutes les migrations ou se baser sur le modèle (qui n’est pas forcément à jour).
Gestion des migrations d’ajout/suppression temporalité
Pour retirer la temporalité d’une table il suffit de supprimer/commenter son flag associé :
Migration du retrait de temporalité :
Code généré par EF Core :
Exécution de la migration :
Les modifications sont correctement appliquées sur le schéma SQL.
Important : Comme nous l’avons constaté le retrait de temporalité supprime les 2 colonnes ainsi que l’historique, attention à la perte des données associées (par exemple les 2 colonnes système pourraient être déjà utilisées comme valeur de date création et modification ailleurs dans le code.
Récupération des dates de temporalité
Entity Framework gère les colonnes temporelles en tant que shadow property. Cela signifie qu’il est possible de les utiliser mais seulement de façon explicite via EF.Property<DateTime>(i, « MonNomColonne »).
Exemple de récupération des valeurs :
SQL généré :
Ces colonnes sont de type système et gérés pas la base de données, d’où la raison qu’EF les considère comme shadow puisque non-modifiables. Le principe est le même lorsqu’on exécute la requête « SELECT * FROM [Employe] », pour récupérer les colonnes temporelles il faut l’écrire ainsi « SELECT *, PeriodStart, PeriodEnd FROM [Employe] ».
Récupération des anciennes données
Restauration données encore existantes
Pour revenir à une ancienne version des données, il suffit de récupérer la version souhaitée via TemporalAll() puis de la définir en tant que EntityState.Modified afin qu’EF puisse générer la requête UPDATE associée :
Résultat :
Restauration données déjà supprimées
Le principe de fonctionnement est identique à des données existantes, il suffit de remplacer EntityState.Modified par EntityState.Added (ou de l’ajouter au DbContext comme ici) :
Résultat :
Il a fallu ici réinitialiser l’Id puisqu’il est auto-incrémenté et ne permet donc pas de définir une valeur spécifique.
Conclusion
A travers cet article nous avons pu constater le très grand travail réalisé sur Entity Framework 6 afin de le rendre parfaitement compatible avec les tables temporelles, notamment :
- Création de tables temporelles
- Migrations temporelles
- Requêtage historique
- Restauration données
Il n’y a dorénavant plus besoin de de multiplier les extensions, la temporalité étant maintenant parfaitement prise en charge par Entity Framework.
Vous pouvez retrouver l’ensemble du code source de cet article sur GitHub.
Liens utiles :