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!
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/
Machen Sie Ihre Repositorys zu Mitgliedern Ihrer UoW. Lassen Sie Ihre Repositories Ihre UOW nicht "sehen". Lassen Sie UoW die Transaktion abwickeln.
Ü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
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.
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.
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.
}
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.
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: