web-dev-qa-db-fra.com

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

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

public class RepositoryBase<T>
{
    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<Order> OrderRep { get; set; }
    public RepositoryBase<Customer> 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, comme chaque référentiel a une instance partagée de UoW, chaque Repository peut accéder à la méthode Commit sur UoW. L'appeler par un référentiel fait que tous les autres référentiels stockent également leurs modifications; d'où le résultat que tout le concept de transaction s'effondre:

class Repository<T> : RepositoryBase<T>
{
    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'objectif de UoW (transaction commerciale), la méthode Commit ne doit être exposée qu'à celui qui a démarré une transaction commerciale par exemple Business Layer. 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 dépôt 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!

56
Alireza

Je suis d'accord avec vos préoccupations. Je préfère avoir une unité de travail ambiante, où la fonction la plus externe ouvrant une unité de travail est celle qui décide de s'engager ou d'interrompre. Les fonctions appelées peuvent ouvrir une unité de travail qui s'enrôle automatiquement dans l'UoW ambiante s'il y en a une, ou en crée une nouvelle s'il n'y en a 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/portée supprime également la nécessité d'une injection de dépendance.

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

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

Une méthode qui écrit ressemble à ceci:

using (var uow = new UnitOfWorkScope<CarsContext>(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 base de données que s'il s'agit de la portée racine (la plus éloignée). Sinon, il est interprété comme un "vote correct" que la portée racine sera autorisée à enregistrer les modifications.

L'implémentation complète de UnitOfWorkScope est disponible sur: http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/

27
Anders Abel

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.

9
Chalky

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

public interface IDbContext
{
   void Add<T>(T entity);
}

public interface IUnitOfWork
{
   void Commit();
}

public class UnitOfWork : IDbContext, IUnitOfWork
{
   public void Add<T>(T entity);
   public void Commit();
}

public class RepositoryBase<T>
{
    private IDbContext _c;

    public RepositoryBase(IDbContext c) 
    {
       _c = c;
    }

    public void Add(T entity)
    {
       _c.Add(entity)
    }
}

[~ # ~] modifier [~ # ~]

Après avoir posté cela, j'ai repensé. L'exposition de 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 le DbContext utilisé ici est décrit comme "une combinaison du modèle d'unité de travail et de référentiel".

Je pense qu'il est préférable de diviser les deux, et cela signifie que j'ai besoin de deux wrappers autour de DbContext un pour le bit Unit Of Work et un pour le bit Repository. Et je fais le dépôt de dépôt 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 le BaseRepository a accès à un SaveChanges sur le DbContext. Et comme l'intention est que les référentiels personnalisés héritent de BaseRepository, ils ont également accès à un DbContext. Il est donc possible qu'un développeur pourrait ajouter du code dans un référentiel personnalisé qui utilise ce DbContext. Donc je suppose que mon "wrapper" est un peu fuyant ...

Alors vaut-il la peine de créer un autre wrapper pour le DbContext qui peut être passé 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

4
Colin

Sachez que cela fait un moment que cela n'a pas été demandé, et que les gens sont peut-être morts de vieillesse, transférés à la direction, etc., mais voilà.

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

  1. Implémentez l'interface de l'unité de travail décrite dans le livre P de EAA de Fowler, mais injectez le référentiel dans chaque méthode UoW.
  2. Injectez l'unité d'oeuvre 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, la validation sur l'UdW peut exécuter la validation sur chaque référentiel ou la validation sur le magasin de données lui-même. Il peut également implémenter un commit 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 façon dont vous implémentez les référentiels et l'UoW. par exemple. à partir d'un magasin de données simple 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 être 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<T>() : 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 base de données et ressemble à ceci:

// ...
var uow = new MyUnitOfWork();

repo1.Add(entity1, uow);
repo2.Add(entity2, uow);
uow.Commit();

Retour au message d'origine. Étant donné que nous injectons l'UoW dans chaque opération de repo, l'UoW n'a pas besoin d'être stocké par chaque référentiel, ce qui signifie que Commit () sur le référentiel peut être tronqué, Commit sur l'UoW effectuant la validation de base de données réelle.

3
Patrick Farry

Dans .NET, les composants d'accès aux données s'inscrivent généralement automatiquement aux transactions ambiantes. Par conséquent, enregistrement des modifications intra-transactionnel est séparé de validation de la transaction pour conserver les modifications.

Autrement dit - si vous créez une étendue de transaction, vous pouvez laisser les développeurs économiser autant qu'ils le souhaitent. Ce n'est que lorsque la transaction est validée que l'état observable des bases de données sera mis à jour (enfin, ce qui est observable dépend du niveau d'isolement de la transaction).

Cela montre comment créer une étendue de transaction en 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.
}
2
lightbricko

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 la mise en œuvre du référentiel. Mon code est le suivant:

public class GenericRepository<T> where T : class
{
  private MyDatabase _Context;
  private DbSet<T> dbset;

  public GenericRepository(MyDatabase context)
  {
    _Context = context;
    dbSet = context.Set<T>();
  }

  public T Get(int id)
  {
    return dbSet.Find(id);
  }

  public IEnumerable<T> GetAll()
  {
    return dbSet<T>.ToList();
  }

  public IEnumerable<T> Where(Expression<Func<T>, bool>> predicate)
  {
    return dbSet.Where(predicate);
  }
  ...
  ...
}

Essentiellement, tout ce que nous faisons est de passer dans le contexte de données et d'utiliser les méthodes dbSet du framework d'entité pour Get, GetAll, Add, AddRange, Remove, RemoveRange et Where de base.

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

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

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

Exemple:

public interface ITable1 : IGenericRepository<table1>
{
}

Vous suivrez ce même schéma avec toutes vos entités. Vous ajouterez également toutes les signatures de fonction dans ces interfaces qui sont spécifiques aux entités. Cela entraînerait la nécessité pour les référentiels d'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<table1>, 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 du GenericRepository avec un type de "table1" puis j'hérite de l'interface ITable1. Cela implémentera automatiquement les méthodes génériques dbSet pour moi, me permettant ainsi de me concentrer uniquement sur mes méthodes de référentiel personnalisées le cas échéant. Comme je passe le dbContext au constructeur, je dois également passer le dbContext au référentiel générique de base également.

À 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 ma portée 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 nouveau 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 vos SaveChanges () de vos référentiels et les placera dans le référentiel UnitOfWork. Enfin, ce modèle ne peut utiliser qu'un seul dbContext dans tout le système via l'injection de dépendances.

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

2
logan gilley

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

Tout d'abord, à ma connaissance, le modèle de domaine ne devrait pas connaître l'unité de travail. Le modèle de domaine se compose d'interfaces (ou de classes abstraites) qui n'impliquent pas l'existence du stockage transactionnel. En fait, il ne connaît pas du tout l'existence de stockage . D'où le terme Domaine Modèle .

L'unité d'oeuvre est présente dans la couche Implémentation du modèle de domaine . Je suppose que c'est mon terme, et j'entends par là une couche qui implémente des interfaces de modèle de domaine en incorporant la couche d'accès aux données. Habituellement, j'utilise ORM comme DAL et il est donc livré avec UoW intégré (méthode Entity Framework SaveChanges ou SubmitChanges pour valider les modifications en attente). Cependant, celui-ci appartient à DAL et n'a besoin d'aucune magie d'inventeur.

D'un autre côté, vous faites référence à l'UoW que vous devez avoir dans la couche de mise en œuvre du modèle de domaine, car vous devez supprimer la partie de "validation des modifications de DAL". Pour cela, j'irais avec la solution d'Anders Abel (scropes récursifs), car cela répond à deux choses que vous devez résoudre en un seul coup:

  • 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 d'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.
0
Tengiz