it-swarm.com.de

Arbeitseinheit + Repository-Muster: Der Fall des Geschäftsvorgangskonzepts

Das Kombinieren von Unit of Work Und Repository Pattern Ist heutzutage weit verbreitet. Wie Martin Fowler sagt ein Zweck der Verwendung von UoW ist es, eine Geschäftstransaktion zu bilden, ohne zu wissen, wie Repositorys tatsächlich funktionieren (weil sie beständig ignorant sind). Ich habe viele Implementierungen überprüft. Wenn Sie bestimmte Details ignorieren (konkrete/abstrakte Klasse, Schnittstelle, ...), ähneln sie mehr oder weniger dem Folgenden:

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.
    }
}

Nun mein Problem:

UoW macht die öffentliche Methode Commit verfügbar, um die Änderungen zu speichern. Da jedes Repository über eine gemeinsam genutzte Instanz von UoW verfügt, kann jedes Repository unter UoW ​​auf die Methode Commit zugreifen. Wenn Sie es von einem Repository aus aufrufen, werden die Änderungen auch in allen anderen Repositorys gespeichert. daher kollabiert das gesamte Konzept der Transaktion:

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
    }
}

Ich denke das darf nicht sein. In Anbetracht des Zwecks des UoW (Geschäftsvorgangs) sollte die Methode Commit nur dem zugänglich gemacht werden, der einen Geschäftsvorgang gestartet hat, z. B. Business Layer. Was mich überrascht hat, ist, dass ich keinen Artikel gefunden habe, der sich mit diesem Problem befasst. In allen kann Commit von jedem Repo aufgerufen werden, das injiziert wird.

PS: Ich weiß, dass ich meinen Entwicklern sagen kann, dass sie Commit in einem Repository nicht aufrufen sollen, aber eine vertrauenswürdige Architektur ist zuverlässiger als vertrauenswürdige Entwickler!

56
Alireza

Ich stimme Ihren Bedenken zu. Ich bevorzuge eine Ambient-Arbeitseinheit, bei der die äußerste Funktion, die eine Arbeitseinheit öffnet, diejenige ist, die entscheidet, ob sie festgeschrieben oder abgebrochen wird. Mit den aufgerufenen Funktionen kann eine Arbeitseinheit geöffnet werden, die automatisch in der Umgebungs-UOW eingetragen wird, wenn es eine gibt, oder eine neue erstellt, wenn es keine gibt.

Die Implementierung von UnitOfWorkScope, die ich verwendet habe, ist stark von der Funktionsweise von TransactionScope inspiriert. Die Verwendung eines Umgebungs/Bereichs-Ansatzes beseitigt auch die Notwendigkeit einer Abhängigkeitsinjektion.

Eine Methode, die eine Abfrage ausführt, sieht folgendermaßen aus:

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

Eine Methode, die schreibt, sieht folgendermaßen aus:

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

Beachten Sie, dass der uow.SaveChanges() -Aufruf nur dann ein tatsächliches Speichern in der Datenbank ausführt, wenn dies der Stammbereich (der äußerste Bereich) ist. Andernfalls wird es als "OK-Abstimmung" interpretiert, dass der Root-Bereich die Änderungen speichern darf.

Die gesamte Implementierung von UnitOfWorkScope ist verfügbar unter: http://coding.abel.nu/2012/10/make-the-dbcontext-ambient-with-unitofworkscope/

27
Anders Abel

Machen Sie Ihre Repositorys zu Mitgliedern Ihrer UoW. Lassen Sie Ihre Repositories Ihre UOW nicht "sehen". Lassen Sie UoW die Transaktion abwickeln.

9
Chalky

Übergeben Sie nicht das UnitOfWork, sondern eine Schnittstelle mit den von Ihnen benötigten Methoden. Sie können diese Schnittstelle weiterhin in der ursprünglichen konkreten UnitOfWork Implementierung implementieren, wenn Sie Folgendes möchten:

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)
    }
}

[~ # ~] edit [~ # ~]

Nach dem Posten hatte ich ein Umdenken. Wenn Sie die Add-Methode in der UnitOfWork -Implementierung verfügbar machen, handelt es sich um eine Kombination der beiden Muster.

Ich verwende Entity Framework in meinem eigenen Code und das dort verwendete DbContext wird als "eine Kombination aus Unit-Of-Work- und Repository-Muster" beschrieben.

Ich denke, es ist besser, die beiden zu teilen, und das bedeutet, dass ich zwei Wrapper um DbContext brauche, einen für das Unit Of Work-Bit und einen für das Repository-Bit. Und ich wickle das Repository in RepositoryBase ein.

Der Hauptunterschied besteht darin, dass ich das UnitOfWork nicht an die Repositorys übergebe, sondern das DbContext. Das bedeutet, dass der BaseRepository Zugriff auf einen SaveChanges auf dem DbContext hat. Und da die Absicht besteht, dass benutzerdefinierte Repositorys BaseRepository erben sollen, erhalten sie auch Zugriff auf ein DbContext. Es ist daher möglich, dass ein Entwickler könnte Code in einem benutzerdefinierten Repository hinzufügt, das diesen DbContext verwendet. Also denke ich, mein "Wrapper" ist ein bisschen undicht ...

Lohnt es sich also, einen weiteren Wrapper für das DbContext zu erstellen, der an die Repository-Konstruktoren übergeben werden kann, um das abzuschließen? Ich bin mir nicht sicher, ob es ...

Beispiele für die Übergabe des DbContext:

Implementierung des Repository und der Arbeitseinheit

Repository und Arbeitseinheit in Entity Framework

John Papas ursprünglicher Quellcode

4
Colin

Beachten Sie, dass es eine Weile her ist, seit dies gefragt wurde, und dass Menschen möglicherweise an Altersschwäche gestorben sind, in die Geschäftsleitung versetzt wurden usw. Aber hier ist es.

Anhand von Datenbanken, Transaktionscontrollern und dem Zwei-Phasen-Festschreibungsprotokoll sollten die folgenden Änderungen an den Mustern für Sie funktionieren.

  1. Implementieren Sie die Unit of Work-Schnittstelle, die in Fowlers Buch P of EAA beschrieben ist, aber fügen Sie das Repository in jede UoW-Methode ein.
  2. Injizieren Sie die Arbeitseinheit in jede Repository-Operation.
  3. Jede Repository-Operation ruft die entsprechende UoW-Operation auf und injiziert sich selbst.
  4. Implementieren Sie die beiden Phase-Commit-Methoden CanCommit (), Commit () und Rollback () in den Repositorys.
  5. Bei Bedarf kann Commit für die UOW für jedes Repository ausgeführt werden oder es kann ein Commit für den Datenspeicher selbst ausgeführt werden. Es kann auch ein 2-Phasen-Festschreiben implementieren, wenn Sie dies wünschen.

Danach können Sie eine Reihe von verschiedenen Konfigurationen unterstützen, je nachdem, wie Sie die Repositorys und die UoW implementieren. z.B. Vom einfachen Datenspeicher ohne Transaktionen über einzelne RDBMs bis hin zu mehreren heterogenen Datenspeichern usw. Die Datenspeicher und ihre Interaktionen können sich je nach Situation entweder in den Repositorys oder in der UoW ​​befinden.

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);
}

Der Benutzercode ist unabhängig von der DB-Implementierung immer derselbe und sieht folgendermaßen aus:

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

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

Zurück zum ursprünglichen Beitrag. Da es sich um eine Methode handelt, mit der die UOW in jede Repo-Operation injiziert wird, muss die UOW nicht von jedem Repository gespeichert werden, was bedeutet, dass Commit () im Repository ausgeblendet werden kann, wobei Commit auf der UOW das eigentliche DB-Commit ausführt.

3
Patrick Farry

In .NET werden Datenzugriffskomponenten normalerweise automatisch für Umgebungstransaktionen registriert. Daher wird Speichern von Änderungen innerhalb von Transaktionen von Festschreiben der Transaktion, um die Änderungen beizubehalten getrennt.

Anders ausgedrückt: Wenn Sie einen Transaktionsbereich erstellen, können die Entwickler so viel sparen, wie sie möchten. Erst wenn die Transaktion festgeschrieben ist, wird der beobachtbare Status der Datenbank (en) aktualisiert (nun, was beobachtbar ist, hängt von der Transaktionsisolationsstufe ab).

Hier sehen Sie, wie Sie einen Transaktionsbereich in c # erstellen:

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

Auch ich habe kürzlich dieses Entwurfsmuster untersucht und mithilfe der Arbeitseinheit und des generischen Repository-Musters konnte ich die Arbeitseinheit "Änderungen speichern" für die Repository-Implementierung extrahieren. Mein Code lautet wie folgt:

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);
  }
  ...
  ...
}

Im Wesentlichen übergeben wir nur den Datenkontext und verwenden die dbSet-Methoden des Entity-Frameworks für die grundlegenden Funktionen Get, GetAll, Add, AddRange, Remove, RemoveRange und Where.

Nun erstellen wir eine generische Schnittstelle, um diese Methoden verfügbar zu machen.

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

Jetzt möchten wir eine Schnittstelle für jede Entität in Entity Framework erstellen und von IGenericRepository erben, sodass die Schnittstelle erwartet, dass die Methodensignaturen in den geerbten Repositorys implementiert werden.

Beispiel:

public interface ITable1 : IGenericRepository<table1>
{
}

Sie werden mit all Ihren Entitäten dem gleichen Muster folgen. Außerdem fügen Sie in diesen Schnittstellen Funktionssignaturen hinzu, die für die Entitäten spezifisch sind. Dies würde dazu führen, dass die Repositorys die GenericRepository-Methoden und alle in den Schnittstellen definierten benutzerdefinierten Methoden implementieren müssen.

Für die Repositories werden wir sie so implementieren.

public class Table1Repository : GenericRepository<table1>, ITable1
{
  private MyDatabase _context;

  public Table1Repository(MyDatabase context) : base(context)
  {
    _context = context;
  }
} 

Im obigen Beispielrepository erstelle ich das Repository table1 und erbe das GenericRepository mit dem Typ "table1". Anschließend erbe ich das Repository von der ITable1-Schnittstelle. Dadurch werden die generischen dbSet-Methoden automatisch für mich implementiert, sodass ich mich nur auf meine benutzerdefinierten Repository-Methoden konzentrieren kann, sofern vorhanden. Wenn ich den dbContext an den Konstruktor übergebe, muss ich den dbContext auch an das allgemeine Basis-Repository übergeben.

Jetzt werde ich das Unit of Work-Repository und -Interface erstellen.

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();
  }
}

Ich verwalte meinen Transaktionsbereich auf einem benutzerdefinierten Controller, von dem alle anderen Controller in meinem System erben. Dieser Controller erbt vom Standard-MVC-Controller.

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();
  }
}

Indem Sie Ihren Code auf diese Weise implementieren. Jedes Mal, wenn zu Beginn einer Aktion eine Anforderung an den Server gestellt wird, wird ein neues UnitOfWork erstellt, das automatisch alle Repositorys erstellt und diese für die UoW-Variable in Ihrem Controller oder Ihren Klassen zugänglich macht. Dadurch werden auch Ihre SaveChanges () aus Ihren Repositorys entfernt und im UnitOfWork-Repository abgelegt. Und letztendlich kann dieses Muster über die Abhängigkeitsinjektion nur einen einzigen dbContext im gesamten System verwenden.

Wenn Sie sich Sorgen über übergeordnete/untergeordnete Aktualisierungen in einem bestimmten Kontext machen, können Sie gespeicherte Prozeduren für das Aktualisieren, Einfügen und Löschen von Funktionen und das Entitätsframework für Ihre Zugriffsmethoden verwenden.

2
logan gilley

Ja, diese Frage ist mir ein Anliegen, und so gehe ich damit um.

Zunächst sollte Domain Model meines Erachtens nichts über Unit of Work wissen. Das Domänenmodell besteht aus Schnittstellen (oder abstrakten Klassen), die nicht die Existenz des Transaktionsspeichers implizieren. Tatsächlich weiß es überhaupt nicht, ob ein Speicher vorhanden ist. Daher der Begriff Domain Model .

Die Arbeitseinheit ist in der Ebene Domain Model Implementation vorhanden. Ich denke, dies ist mein Begriff, und damit meine ich eine Ebene, die Domänenmodellschnittstellen durch Einbeziehung der Datenzugriffsebene implementiert. Normalerweise verwende ich ORM als DAL und daher wird es mit integrierter UoW ​​geliefert (Entity Framework SaveChanges- oder SubmitChanges-Methode zum Festschreiben der ausstehenden Änderungen). Dieser gehört jedoch zu DAL und benötigt keine Erfindermagie.

Auf der anderen Seite beziehen Sie sich auf die UoW, die Sie in der Domain Model Implementation-Schicht benötigen, da Sie den Teil "Änderungen an DAL vornehmen" abstrahieren müssen. Dafür würde ich mich für Anders Abels Lösung entscheiden (rekursive Skripte), da hier zwei Dinge angesprochen werden, die zu lösen sind in einem Schuss:

  • Sie müssen das Speichern von Aggregaten als eine Transaktion unterstützen, wenn das Aggregat ein Initiator des Bereichs ist.
  • Sie müssen das Speichern von Aggregaten als Teil der übergeordneten Transaktion unterstützen, wenn das Aggregat nicht der Initiator des Bereichs ist, sondern Teil davon.
0
Tengiz