Unité de travail + modèle de référentiel: la chute du concept de transaction commerciale

La combinaison de l’ Unit of Work et du Repository Pattern est quelque chose qui est assez largement utilisé de nos jours. Comme le dit Martin Fowler , le but de l’utilisation de l’ UoW est de former une transaction commerciale tout en ignorant le fonctionnement UoW des référentiels (ignorants persistants). J’ai revu de nombreuses implémentations; et en ignorant les détails spécifiques (classe concrète / abstraite, interface, …) ils sont plus ou moins similaires à ce qui suit:

 public class RepositoryBase { private UoW _uow; public RepositoryBase(UoW uow) // injecting UoW instance via constructor { _uow = uow; } public void Add(T entity) { // Add logic here } // +other CRUD methods } public class UoW { // Holding one repository per domain entity public RepositoryBase OrderRep { get; set; } public RepositoryBase CustomerRep { get; set; } // +other repositories public void Commit() { // Psedudo code: For all the contained repositories do: store repository changes. } } 

Maintenant mon problème:

UoW expose la méthode publique Commit pour stocker les modifications. De plus, chaque référentiel ayant une instance partagée d’ UoW , chaque Repository peut accéder à la méthode Commit on UoW. L’appel par un seul référentiel permet également à tous les autres référentiels de stocker leurs modifications. d’où le résultat que tout le concept de transaction s’effondre:

 class Repository : RepositoryBase { private UoW _uow; public void SomeMethod() { // some processing or data manipulations here _uow.Commit(); // makes other repositories also save their changes } } 

Je pense que cela ne doit pas être autorisé. Compte tenu de l’objective de l’ UoW (transaction commerciale), la méthode Commit doit être exposée uniquement à celui qui a lancé une transaction commerciale, par exemple une couche métier. Ce qui m’a surpris, c’est que je n’ai trouvé aucun article traitant de ce problème. Dans chacun d’eux, Commit peut être appelé par n’importe quel repo injecté.

PS: Je sais que je peux dire à mes développeurs de ne pas appeler Commit dans un Repository mais une architecture de confiance est plus fiable que les développeurs de confiance!

Je suis d’accord avec vos préoccupations. Je préfère avoir une unité de travail ambiante, où la fonction la plus extérieure ouvrant une unité de travail est celle qui décide de s’engager ou d’abandonner. Les fonctions appelées peuvent ouvrir une unité de scope de travail qui s’inscrit automatiquement dans l’UOW ambiante s’il en existe une ou en crée une nouvelle s’il n’en existe pas.

L’implémentation du UnitOfWorkScope que j’ai utilisé est fortement inspirée par le fonctionnement de TransactionScope . L’utilisation d’une approche ambiante / scope élimine également le besoin d’dependency injection.

Une méthode qui exécute une requête ressemble à ceci:

 public static Entities.Car GetCar(int id) { using (var uow = new UnitOfWorkScope(UnitOfWorkScopePurpose.Reading)) { return uow.DbContext.Cars.Single(c => c.CarId == id); } } 

Une méthode qui écrit ressemble à ceci:

 using (var uow = new UnitOfWorkScope(UnitOfWorkScopePurpose.Writing)) { Car c = SharedQueries.GetCar(carId); c.Color = "White"; uow.SaveChanges(); } 

Notez que l’appel à uow.SaveChanges() ne fera une sauvegarde réelle dans la firebase database que s’il s’agit de l’étendue racine (la plus courante). Sinon, il est interprété comme un “vote correct” que l’étendue racine sera autorisée à enregistrer les modifications.

L’implémentation complète du UnitOfWorkScope est disponible à l’ adresse suivante : http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/

Faites de vos référentiels des membres de votre UoW. Ne laissez pas vos référentiels “voir” votre UoW. Laissez UoW gérer la transaction.

Ne transmettez pas le UnitOfWork , transmettez une interface avec les méthodes dont vous avez besoin. Vous pouvez toujours implémenter cette interface dans l’implémentation UnitOfWork concrète d’origine si vous souhaitez:

 public interface IDbContext { void Add(T entity); } public interface IUnitOfWork { void Commit(); } public class UnitOfWork : IDbContext, IUnitOfWork { public void Add(T entity); public void Commit(); } public class RepositoryBase { private IDbContext _c; public RepositoryBase(IDbContext c) { _c = c; } public void Add(T entity) { _c.Add(entity) } } 

MODIFIER

Après avoir posté ceci, j’ai repensé. Exposer la méthode Add dans l’implémentation UnitOfWork signifie qu’il s’agit d’une combinaison des deux modèles.

J’utilise Entity Framework dans mon propre code et DbContext utilisé ici est décrit comme “une combinaison du modèle Unit-of-Work et Repository”.

Je pense qu’il est préférable de séparer les deux, ce qui signifie que j’ai besoin de deux enveloppes autour de DbContext pour le bit Unit Of Work et une pour le bit Repository. Et je fais le repository de repository dans RepositoryBase .

La principale différence est que je ne passe pas le UnitOfWork aux référentiels, je passe le DbContext . Cela signifie que BaseRepository a access à un SaveChanges sur DbContext . Et comme l’intention est que les référentiels personnalisés héritent de BaseRepository , ils ont également access à DbContext . Il est donc possible qu’un développeur puisse append du code dans un référentiel personnalisé qui utilise DbContext . Donc je suppose que mon “wrapper” est un peu fuyant …

Cela vaut-il la peine de créer un autre wrapper pour DbContext qui peut être transmis aux constructeurs du référentiel pour le fermer? Pas sûr que ce soit …

Exemples de passage du DbContext:

Implémentation du référentiel et de l’unité de travail

Référentiel et unité de travail dans Entity Framework

Le code source original de John Papa

Dans .NET, les composants d’access aux données s’inscrivent généralement automatiquement dans les transactions ambiantes. Par conséquent, l’ enregistrement des modifications au sein de la transaction est séparé de la validation de la transaction pour conserver les modifications .

En d’autres termes, si vous créez une étendue de transaction, vous pouvez laisser les développeurs économiser autant qu’ils le souhaitent. Tant que la transaction n’est pas validée, l’état observable de la ou des bases de données sera mis à jour (eh bien, ce qui est observable dépend du niveau d’isolement de la transaction).

Cela montre comment créer une étendue de transaction dans c #:

 using (TransactionScope scope = new TransactionScope()) { // Your logic here. Save inside the transaction as much as you want. scope.Complete(); // <-- This will complete the transaction and make the changes permanent. } 

J’ai aussi récemment étudié ce modèle de conception et en utilisant l’Unité de travail et le modèle de référentiel générique, j’ai pu extraire l’unité de travail “Enregistrer les modifications” pour l’implémentation du référentiel. Mon code est le suivant:

 public class GenericRepository where T : class { private MyDatabase _Context; private DbSet dbset; public GenericRepository(MyDatabase context) { _Context = context; dbSet = context.Set(); } public T Get(int id) { return dbSet.Find(id); } public IEnumerable GetAll() { return dbSet.ToList(); } public IEnumerable Where(Expression, bool>> predicate) { return dbSet.Where(predicate); } ... ... } 

Essentiellement, nous ne faisons que passer dans le contexte des données et utiliser les méthodes dbSet du framework d’entités pour les opérations de base Get, GetAll, Add, AddRange, Remove, RemoveRange et Where.

Nous allons maintenant créer une interface générique pour exposer ces méthodes.

 public interface  where T : class { T Get(int id); IEnumerable GetAll(); IEnumerabel Where(Expression> predicate); ... ... } 

Maintenant, nous voudrions créer une interface pour chaque entité dans l’entité Framework et hériter de IGenericRepository afin que l’interface attende d’avoir les signatures de méthode implémentées dans les référentiels hérités.

Exemple:

 public interface ITable1 : IGenericRepository { } 

Vous suivrez ce même schéma avec toutes vos entités. Vous allez également append des signatures de fonction dans ces interfaces spécifiques aux entités. Les référentiels doivent donc implémenter les méthodes GenericRepository et toutes les méthodes personnalisées définies dans les interfaces.

Pour les référentiels, nous les implémenterons comme ceci.

 public class Table1Repository : GenericRepository, ITable1 { private MyDatabase _context; public Table1Repository(MyDatabase context) : base(context) { _context = context; } } 

Dans l’exemple de référentiel ci-dessus, je crée le référentiel table1 et hérite de GenericRepository avec un type de “table1”, puis je hérite de l’interface ITable1. Cela implémentera automatiquement les méthodes génériques dbSet, ce qui me permettra de ne me concentrer que sur les méthodes de référentiel personnalisées, le cas échéant. Lorsque je transmets le dbContext au constructeur, je dois également transmettre le dbContext au référentiel générique de base.

À partir d’ici, je vais créer le référentiel et l’interface de l’unité de travail.

 public interface IUnitOfWork { ITable1 table1 {get;} ... ... list all other repository interfaces here. void SaveChanges(); } public class UnitOfWork : IUnitOfWork { private readonly MyDatabase _context; public ITable1 Table1 {get; private set;} public UnitOfWork(MyDatabase context) { _context = context; // Initialize all of your repositories here Table1 = new Table1Repository(_context); ... ... } public void SaveChanges() { _context.SaveChanges(); } } 

Je gère mon étendue de transaction sur un contrôleur personnalisé dont tous les autres contrôleurs de mon système héritent. Ce contrôleur hérite du contrôleur MVC par défaut.

 public class DefaultController : Controller { protected IUnitOfWork UoW; protected override void OnActionExecuting(ActionExecutingContext filterContext) { UoW = new UnitOfWork(new MyDatabase()); } protected override void OnActionExecuted(ActionExecutedContext filterContext) { UoW.SaveChanges(); } } 

En implémentant votre code de cette façon. Chaque fois qu’une demande est faite au serveur au début d’une action, un nouvel UnitOfWork sera créé et créera automatiquement tous les référentiels et les rendra accessibles à la variable UoW dans votre contrôleur ou vos classes. Cela supprimera également votre SaveChanges () de vos référentiels et le placera dans le référentiel UnitOfWork. Et enfin, ce modèle ne peut utiliser qu’un seul dbContext dans tout le système via l’dependency injections.

Si vous êtes préoccupé par les mises à jour parent / enfant avec un contexte unique, vous pouvez utiliser des procédures stockées pour vos fonctions de mise à jour, d’insertion et de suppression et utiliser une structure d’entité pour vos méthodes d’access.

Réalisez que ça fait un moment que cela a été demandé, et que les gens sont peut-être morts de vieillesse, transférés à la direction, etc.

S’inspirant des bases de données, des contrôleurs de transactions et du protocole de validation en deux phases, les modifications suivantes apscopes aux modèles devraient vous convenir.

  1. Implémentez l’unité de l’unité de travail décrite dans le livre P of EAA de Fowler, mais injectez le référentiel dans chaque méthode UoW.
  2. Injecter l’unité de travail dans chaque opération de référentiel.
  3. Chaque opération de référentiel appelle l’opération UoW appropriée et s’injecte.
  4. Implémentez les méthodes de validation en deux phases CanCommit (), Commit () et Rollback () dans les référentiels.
  5. Si nécessaire, commit sur l’UoW peut exécuter Commit sur chaque référentiel ou il peut s’engager sur le magasin de données lui-même. Il peut également implémenter une validation en 2 phases si c’est ce que vous voulez.

Cela fait, vous pouvez prendre en charge un certain nombre de configurations différentes selon la manière dont vous implémentez les référentiels et l’UoW. Par exemple, à partir d’un simple magasin de données sans transactions, de RDBM uniques, de plusieurs magasins de données hétérogènes, etc. Les magasins de données et leurs interactions peuvent se trouver dans les référentiels ou dans l’UoW, selon la situation.

 interface IEntity { int Id {get;set;} } interface IUnitOfWork() { void RegisterNew(IRepsitory repository, IEntity entity); void RegisterDirty(IRepository respository, IEntity entity); //etc. bool Commit(); bool Rollback(); } interface IRepository() : where T : IEntity; { void Add(IEntity entity, IUnitOfWork uow); //etc. bool CanCommit(IUnitOfWork uow); void Commit(IUnitOfWork uow); void Rollback(IUnitOfWork uow); } 

Le code utilisateur est toujours le même quelles que soient les implémentations de la firebase database et ressemble à ceci:

 // ... var uow = new MyUnitOfWork(); repo1.Add(entity1, uow); repo2.Add(entity2, uow); uow.Commit(); 

Retour au message original Comme nous injectons l’UoW dans chaque opération de référentiel, l’UoW n’a pas besoin d’être stocké par chaque référentiel, ce qui signifie que Commit () sur le Repository peut être supprimé, Commit sur l’UoW faisant la validation réelle de la DB.

Oui, cette question me préoccupe et voici comment je le gère.

Tout d’abord, d’après moi, le modèle de domaine ne devrait pas connaître l’unité de travail. Le modèle de domaine est constitué d’interfaces (ou de classes abstraites) qui n’impliquent pas l’existence du stockage transactionnel. En fait, il ne sait pas du tout l’existence d’ un stockage. D’où le terme modèle de domaine.

L’unité de travail est présente dans la couche d’ implémentation du modèle de domaine . Je suppose que c’est mon terme, et par là je veux dire une couche qui implémente les interfaces du modèle de domaine en incorporant la couche d’access aux données. D’habitude, j’utilise ORM en tant que DAL et, par conséquent, il est livré avec une UOW intégrée (méthode Entity Framework SaveChanges ou SubmitChanges pour valider les modifications en attente). Cependant, celui-ci appartient à DAL et n’a pas besoin de magie d’inventeur.

D’un autre côté, vous faites référence à l’UoW que vous devez avoir dans la couche d’implémentation du modèle de domaine, car vous devez faire abstraction de la partie “modifications de validation dans DAL”. Pour cela, j’irais avec la solution d’Anders Abel (scrursive récursifs), car cela répond à deux choses que vous devez résoudre en une seule fois :

  • Vous devez prendre en charge l’enregistrement des agrégats en une seule transaction, si l’agrégat est un initiateur de l’étendue.
  • Vous devez prendre en charge l’enregistrement des agrégats dans le cadre de la transaction parent , si l’agrégat n’est pas l’initiateur de l’étendue, mais en fait partie.