it-swarm.com.de

Repository-Muster und verknüpfte Abfragen

In Verbindung mit Unit Tests und Dependency Injection erkunde ich (und mein Hauptmitarbeiter) Repositories. Wir können jedoch keinen soliden Aktionsplan für die Umsetzung finden.

In einem einfachen Szenario haben wir ein Repository, das einen einzelnen Kontext und eine oder mehrere Entitäten kapselt. Die öffentlichen Methoden dieses Repositorys geben entweder List oder ein einzelnes Entitätsergebnis zurück. IE

public class SomeEntity
{
    public int Id { get; set; }
    public string SomeValue { get; set; }
}

public class SomeContext : DbContext
{
    public virtual DbSet<SomeEntity> SomeEntities { get; set; }
}

public class SomeRepo
{
    public SomeRepo()
    {
        _context = new SomeContext();
    }

    public SomeEntity GetEntity(int id)
    {
        return _context.SomeEntities.Find(id);
    }

    public IQueryable<SomeEntity> GetAllEntities()
    {
        return _context.SomeEntities;
    }
}

Das ist alles schön und gut und funktioniert in 99% der Fälle gut für uns. Das Problem besteht darin, dass ein Repo mehrere Entitäten enthält und ein Join erforderlich ist. Derzeit machen wir in einer UOW-Klasse, die das Repository verwendet, nur die folgenden Schritte.

public SomeModel SomeMethod()
{
    var entity1 = _repo.GetEntity1();
    var entity2 = _repo.GetEntity2();
    return from a in entity1
           join b in entity2 on a.id equals b.id
           select new SomeModel
           {
               Foo = a.foo,
               Bar = b.bar
           };
}

Aus den vielen widersprüchlichen Diskussionen/Posts/Blogs/usw. über Repositories und unsere eigenen persönlichen Gefühle scheint dies nicht richtig zu sein. Was auch nicht richtig erscheint, ist, den Join innerhalb des Repos durchzuführen und dann etwas zurückzugeben, das nicht zu den Entitäten gehört.

Unser typisches Design besteht darin, einen Kontext in ein Repository einzuschließen, das in eine UOW-Klasse abhängig injiziert wird. Auf diese Weise können wir einen Unit-Test durchführen, der das Repo verspottet und gefälschte DB-Ergebnisse zurückgibt.

In dem Wissen, dass dies eine geladene Frage ist, was könnte ein gutes Muster für uns sein?


Für ein realistischeres Beispiel eines Join-Szenarios (ich bin mit diesem Code nicht zufrieden, es war ein Rush-Job, um etwas zu bewirken, aber es ist ein gutes Beispiel für das Szenario, das wir ansprechen müssen):

public class AccountingContext : DbContext
{
    public DbSet<Vendor> Vendors { get; set; }
    public DbSet<Check> Checks { get; set; }
    public DbSet<ApCheckDetail> CheckDetails { get; set; }
    public DbSet<Transaction> Transactions { get; set; }

    public AccountingContext(string connString) : base(connString)
    {

    }
}

public class AccountingRepo : IAccountingRepo
{
    private readonly AccountingContext _accountingContext;

    public AccountingRepo(IConnectionStringMaker connectionStringMaker, ILocalConfig localConfig)
    {
        // code to generate connString

        _accountingContext = new AccountingContext(connString);
    }

    public IEnumerable<Check> GetChecksByDate(DateTime checkDate)
    {
        return _accountingContext.Checks
            .Where(c => c.CHKDATE.Value == checkDate.Date &&
                        !c.DELVOIDDATE.HasValue);
    }

    public IEnumerable<Vendor> GetVendors(IEnumerable<string> vendorId)
    {
        return _accountingContext.Vendors
            .Where(v => vendorId.Contains(v.VENDCODE))
            .Distinct();
    }

    public IEnumerable<ApCheckDetail> GetCheckDetails(IEnumerable<string> checkIds)
    {
        return _accountingContext.CheckDetails
            .Where(c => checkIds.Contains(c.CheckId));
    }

    public IEnumerable<Transaction> GetTransactions(IEnumerable<string> tranNos, DateTime checkDate)
    {
        var ids = tranNos.ToList();
        var sb = new StringBuilder();
        sb.Append($"'{ids.First()}'");
        for (int i = 1; i < ids.Count; i++)
        {
            sb.Append($", '{ids[i]}'");
        }

        var sql = $"Select TranNo = TRANNO, InvoiceNo = INVNO, InvoiceDate = INVDATE, InvoiceAmount = INVAMT, DiscountAmount = DISCEARNED, TaxWithheld = OTAXWITHAMT, PayDate = PAYDATE from APTRAN where TRANNO in ({sb})";
        return _accountingContext.Set<Transaction>().SqlQuery(sql).ToList();
    }
}

public class AccountingInteraction : IAccountingInteraction
{
    private readonly IAccountingRepo _accountingRepo;

    public AccountingInteraction(IAccountingRepo accountingRepo)
    {
        _accountingRepo = accountingRepo;
    }

    public IList<CheckDetail> GetChecksToPay(DateTime checkDate, IEnumerable<string> excludeVendCats)
    {
        var todaysChecks = _accountingRepo.GetChecksByDate(checkDate).ToList();

        var todaysVendors = todaysChecks.Select(c => c.APCODE).Distinct().ToList();
        var todaysCheckIds = todaysChecks.Select(c => c.CheckId).ToList();

        var vendors = _accountingRepo.GetVendors(todaysVendors).ToList();
        var apCheckDetails = _accountingRepo.GetCheckDetails(todaysCheckIds).ToList();
        var todaysCheckDetails = apCheckDetails.Select(a => a.InvTranNo).ToList();

        var tranDetails = _accountingRepo.GetTransactions(todaysCheckDetails, checkDate).ToList();


        return (from c in todaysChecks
                join v in vendors on c.APCODE equals v.VENDCODE
                where !c.DELVOIDDATE.HasValue &&
                      !excludeVendCats.Contains(v.VENDCAT) &&
                      c.BACSPMT != 1 &&
                      v.DEFPMTTYPE == "CHK"
                select new CheckDetail
                {
                    VendorId = v.VENDCODE,
                    VendorName = v.VENDNAME,
                    CheckDate = c.CHKDATE.Value,
                    CheckAmount = c.CHKAMT.Value,
                    CheckNumber = c.CHECKNUM.Value,
                    Address1 = v.ADDR1,
                    Address2 = v.ADDR2,
                    City = v.CITY,
                    State = v.STATE,
                    Zip = v.Zip,
                    Company = c.COMPNUM.Value,
                    VoidDate = c.DELVOIDDATE,
                    PhoneNumber = v.OFFTELE,
                    Email = v.EMAIL,
                    Remittances = (from check in todaysChecks
                                   join d in apCheckDetails on check.CheckId equals d.CheckId
                                   join t in tranDetails on d.InvTranNo equals t.TranNo
                                   where check.CheckId == c.CheckId
                                   select new RemittanceModel
                                   {
                                       InvoiceAmount = t.InvoiceAmount,
                                       CheckAmount = d.PaidAmount,
                                       InvoiceDate = t.InvoiceDate,
                                       DiscountAmount = t.DiscountAmount,
                                       TaxWithheldAmount = t.TaxWithheld,
                                       InvoiceNumber = t.InvoiceNo
                                   }).ToList()
                }).ToList();
    }
}
6
gilliduck

Hauptverantwortung des Repository-Musters für die Abstraktion der tatsächlichen Datenbank von der Domänencodebasis.
Wenn Sie ein Repository pro Entität haben, werden Details zur Datenbankimplementierung an die Domänenschicht zurückgegeben.

Verwenden Sie stattdessen beispielsweise domänenbasierte Abstraktionen

public interface ISalesOrderRepository
{
    IEnumerable<SalesOrderBasicDto> GetAll();
    SalesOrderBasicDto GetById(Guid orderId);
    SalesOrderWithLinesDto GetWithLinesById(Guid orderId);
} 

Im Datenbankzugriffsprojekt können Sie dieses Repository dann so effizient implementieren, wie es das aktuelle Datenbankframework zulässt.

public class SqlServerSalesOrderRepository : ISalesOrderRepository
{
    private readonly ContextFactory _contextFactory;

    public SqlServerSalesOrderRepository(ContextFactory contextFactory)
    {
        _contextFactory = contextFactory;
    }

    publc SalesOrderWithLinesDto GetWithLinesById(Guid orderId)
    {
        // Here you can use joins to combine order and related order lines
        using (var context = _contextFactory.Create<SalesContext>())
        {
            return context.SalesOrders
                          .Include(o => o.SalesOrderLines)
                          .Where(o => o.Id == orderId)
                          .Select(o => o.ToDto())
                          .Single();
        }
    }
}

Anstatt die Datenbankstruktur in Ihrem Repository zu spiegeln, sollten Sie Abstraktionen verwenden, die den Domänenanforderungen entsprechen, und diese Abstraktionen dann mithilfe der Datenbankfunktionen effektiv implementieren.

10
Fabio

Was auch nicht richtig erscheint, ist, den Join innerhalb des Repos durchzuführen und dann etwas zurückzugeben, das nicht zu den Entitäten gehört.

Dies ist die wesentliche Unvollkommenheit von UOW-freien Repositories. Repositorys sind auf einen einzelnen Entitätstyp beschränkt. Alles, was Teil des Repositorys ist (d. H. Nur für ein einzelnes Repository-Objekt), ist daher auch inhärent einem einzelnen Entitätstyp zugeordnet.

Dies ist meist ein stilistisches und theoretisches Argument. Repositorys sind perfekt in der Lage, mehr als einen Typ zurückzugeben, aber es fühlt sich schmutzig an, dies zu tun, wenn das Repository im Wesentlichen für einen bestimmten Entitätstyp erstellt wurde.

Meiner Meinung nach ist dies nichts weiter als Entwicklerfehler. Sie haben ein System implementiert, das auf technischer Ebene funktioniert, jedoch erhebliche Leistungsprobleme aufweist. Uow-less-Repositorys werden unter der Annahme erstellt, dass Datenbankinteraktionen auf objektbasierte get/set-Methoden beschränkt sind. und dass alle Datenstiche oder Operationen (z. B. eine Gruppe nach + Anzahl) durchgeführt werden im Speicher.

Während dies auf technischer Ebene funktioniert, schlägt es auf Leistungsebene fehl. Das beabsichtigte Muster passt einfach nicht zur erforderlichen Ausführung.

Unser typisches Design besteht darin, einen Kontext in ein Repository einzuschließen, das in eine UOW-Klasse abhängig injiziert wird. Auf diese Weise können wir einen Unit-Test durchführen, der das Repo verspottet und gefälschte DB-Ergebnisse zurückgibt.

Eine Arbeitseinheit befasst sich genau mit diesem Problem. Es kehrt die Reihenfolge der Operationen um. Anstelle vieler Repositorys mit jeweils eigenem Kontext erhalten Sie einen Kontext mit vielen Repositorys.

Die kurze Antwort hier lautet: Wenn Sie möchten, dass Ihre Probleme ohne Fehler oder halbherzige Problemumgehungen gelöst werden, müssen Sie eine Arbeitseinheit verwenden. Ende der Geschichte.

Die Realität stimmt jedoch nicht immer mit uns überein. Ich stehe derzeit vor einem Projekt, bei dem ich die Behauptung des Teams, dass die Implementierung einer Arbeitseinheit nicht die Zeit wert ist, einfach nicht ändern kann. Versuchen Sie, wie Sie möchten, Sie könnten in einer ähnlichen Situation stecken.

Also, was machst du dann?


In meiner Erfahrung als Entwickler habe ich andere Ansätze gesehen, um dieses Problem anzugehen. Sie sind meiner Meinung nach einer Arbeitseinheit unterlegen, aber manchmal einfacher und gut genug für eine kleine Anwendung. Ich möchte nur auf die bemerkenswerten hinweisen und warum sie nicht gut waren.

1. Haben Sie auch Repositorys, die mehr als nur einen Entitätstyp umfassen.

Wenn eine Person beispielsweise viele Hüte und viele Katzen hat, erwarten Sie 3 separate Repositorys. Wenn Sie jedoch immer nur als Teil einer Person (niemals alleine) auf die Hüte und Katzen zugreifen, sind die Entitäten Hut und Katze nicht gleichberechtigt mit der Entität Person. Sie sind eine "untergeordnete" Entität, die effektiv als eine Eigenschaft verwendet wird, die einfach passiertIEnumerable ist, aber ansonsten genau wie eine Eigenschaft funktioniert.

In einem solchen Fall habe ich Repositorys wie PersonDetailRepository erstellt gesehen, die Ihnen effektiv mitteilen, dass sich dieses Repository im Bereich der Entität Person und aller ihrer untergeordneten Entitäten befindet.

Diese können mit Repositorys mit Entitätsbereich koexistieren. Beispielsweise verfügen Sie möglicherweise über ein Admin-Backend, mit dem Benutzer Entitäten in einer Tabelle erstellen können. und Sie haben möglicherweise eine Endbenutzer-Website, auf der sie ein Datenobjekt für Person + Katze + Hut anzeigen können.

Das Problem bei diesem Ansatz ist, dass Sie am Ende viel Logik zwischen PersonRepository und PersonDetailRepository duplizieren und nicht einmal Anwendungsfälle für den Beitritt zu "main" abdecken (im Gegensatz zu "untergeordnete") Einheiten zusammen.

2. Erfordert, dass ein Repository eine Liste seiner Hauptentität mit möglichen Navigationsrequisiten zurückgibt.

Mit anderen Worten:

  • Wenn Sie eine Liste der Katzen (einschließlich ihres Besitzers) erhalten, ist dies eine CatRepository -Methode.
  • Wenn Sie eine Liste von Personen (einschließlich ihrer Katzen) erhalten, ist dies eine PersonRepository -Methode.

Bei diesem Ansatz ist es in Ordnung, mehr als einen Entitätstyp in einem Repository zu verwenden, solange der Hauptrückgabetyp der Methode mit dem Entitätstyp des Repositorys selbst übereinstimmt.

Ich habe dies mit besserem Erfolg als Ansatz 1 verwendet. Dadurch wird ein konsistentes Muster erstellt, das es Ihnen ermöglicht, Daten nach Herzenslust zu verknüpfen, die Methoden jedoch größtenteils so zu optimieren, dass jede Methode nur einen logischen Ort hat (stattdessen) von es in any Repository für any der verwendeten Entitätstypen in Ihrer Abfrage).

Ist es perfekt Nein. Sie haben immer noch keine Transaktionssicherheit, wenn Aktualisierung viele Entitäten unterschiedlichen Typs. Bei Daten Abruf, bei denen der größte Teil Ihrer Verbindungslogik stattfindet, können Sie die Trennung auf diese Weise sinnvoll halten, auch wenn sie theoretisch nicht perfekt ist.

1
Flater

Es ist etwas verwirrend, weil Ihr Pseudocode-Beispiel ein generisches Repo pro Entität vorschlägt, Ihr reales Beispiel jedoch ein Repo für eine Datenbank (oder zumindest eine Reihe verwandter Entitäten).

Ich würde jedoch sagen, dass an Ihrem Beispiel aus dem wirklichen Leben nichts besonders Falsches ist.

Ihr Repository gibt Domänenentitäten zurück, die Tabellen zugeordnet sind und über Methoden verfügen, die vermutlich schnelle Abfragen verwenden, anstatt eine IQueryable verfügbar zu machen, und es dem Benutzer überlassen, festzustellen, welche Abfragen langsam sind.

Ihre AccountingInteraction-Klasse ist vermutlich Teil einer Anwendung und stellt ein CheckDetail ViewModel zusammen, das verschiedene Domänenentitäten enthält.

Solange die verfügbaren Repository-Methoden leistungsfähig sind, sollte auch die Assembly des ViewModel ausgeführt werden.

Das ViewModel ist korrekt von der Domänen- und Datenebene getrennt.

Wenn ich Kritik hätte, würde ich sagen, dass CheckDetail durch einfaches Einbeziehen ganzer Domain Entities verbessert würde und die zusätzlichen Filter zur Leistung in das Repository verschoben werden könnten. Dies hängt jedoch von den Details Ihres Falls ab. Möglicherweise ist ein sehr spezifisches ViewModel einfacher, und die where-Klausellogik ist ein spezifischer Geschäftsfall, den Sie nicht in die Datenschicht einfügen möchten

z.B

//include the extra parameters for your where clause, or choose an appropriate method name.

var todaysChecks = _accountingRepo.GetChecksRequiringPaymentByDate(checkDate).ToList();

//don't bother with selecting individual properties from the Domain Entities. Let the View decide what to show
return (from c in todaysChecks
                select new CheckDetail
                {
                    Vendor = vendors.FirstOrDefault(v=>c.APCODE == v.VENDCODE)
                    Check =  c,
                    CheckDetails = apCheckDetails.FirstOrDefault(d=>c.CheckId == d.CheckId),
                    //slightly awkward in the linq-sql syntax, but you get the idea
                    Transactions = tranDetails.Where(t=> t.InvTranNo == apCheckDetails.FirstOrDefault(d=>c.CheckId == d.CheckId).TranNo
                }).ToList();
1
Ewan