Pattern di deposito e query congiunte

0

In concomitanza con Test unitari e Iniezione delle dipendenze I (e il mio collega principale) esploriamo i Repository. Tuttavia, non possiamo arrivare a un solido piano di azione per l'implementazione.

In uno scenario di base abbiamo un repository che incapsula un singolo contesto e una o più entità. I metodi pubblici di tale repository restituiscono un risultato List o un singolo entity. 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;
    }
}

Questo è tutto a posto e funziona bene per noi il 99% delle volte. Il quandry è quando ci sono più entità in un Repo e è richiesto un join. Al momento eseguiamo qualcosa come il seguente in una classe UOW che utilizza il repository;

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

Dalle molte discussioni / post / blog / ecc. contraddittorie sui Repository e sui nostri sentimenti personali, questo non sembra giusto. Ciò che non sembra giusto è fare il join all'interno del repository e quindi restituire qualcosa che non è una delle entità.

Il nostro tipico design è di avere un contesto racchiuso all'interno di un repository che è Dependency Injected in una classe UOW. In questo modo possiamo avere un Test unitario che deride il Repo restituendo risultati di DB falsi.

Sapendo che questa è una domanda carica, quale potrebbe essere un buon modello per noi?

Per un esempio più realistico di uno scenario di join (non sono contento di questo codice, è stato un lavoro urgente fare accadere qualcosa, ma è un buon esempio dello scenario che dobbiamo affrontare):

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();
    }
}
    
posta gilliduck 18.07.2018 - 23:00
fonte

3 risposte

2

Responsabilità principale del modello di repository per astrarre il database effettivo dal codebase del dominio.
Quando si dispone di un repository per entità, i dettagli dell'implementazione del database verranno persi nel livello del dominio.

Invece di avere astrazioni basate sul dominio, ad esempio

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

Quindi nel progetto di accesso al database è possibile implementare questo repository nel modo più efficiente consentito dal framework di database corrente.

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

Quindi, invece di eseguire il mirroring della struttura del database nel tuo repository, disponi di astrazioni che soddisfano le esigenze del dominio e quindi implementare tali astrazioni utilizzando efficacemente le funzionalità del database.

    
risposta data 19.07.2018 - 04:08
fonte
0

What also doesn't seem right is doing the join inside the repo and then returning something that isn't one of the entities.

Questa è l'imperfezione essenziale dei repository UOW-less. I repository hanno un ambito per un singolo tipo di entità. Tutto ciò che fa parte del repository (vale a dire univoco per un singolo oggetto respository) è quindi anche intrinsecamente orientato a un singolo tipo di entità.

Questo è principalmente un argomento stilistico e teorico. I repository sono perfettamente in grado di restituire più di un tipo, ma è logico farlo quando il repository è stato creato essenzialmente per gestire un determinato tipo di entità.

A mio parere, questo non è altro che errore dello sviluppatore . Hai implementato un sistema che funziona a livello tecnico, ma presenta notevoli problemi di prestazioni. I repository Uow-less sono costruiti partendo dal presupposto che le interazioni tra database sono limitate ai metodi get / set basati su oggetti; e che qualsiasi punto o operazione di dati (ad es. un gruppo per + count) viene eseguita in memoria .

Sebbene funzioni a livello tecnico, fallisce a livello di prestazioni. Il modello desiderato semplicemente non si adatta all'esecuzione richiesta.

Our typical design is to have a context wrapped inside an Repository which is Dependency Injected into a UOW class. This way we can have a Unit Test that mocks the Repo returning fake DB results.

Un'unità di lavoro affronta questo problema esatto. Inverte l'ordine delle operazioni. Invece di molti repository con ciascuno il proprio contesto, si ottiene un contesto con molti repository.

La risposta breve qui è che se vuoi che le tue preoccupazioni siano risolte senza difetti o soluzioni alternative, allora devi usare un'unità di lavoro. Fine della storia.

Tuttavia, la realtà non è sempre d'accordo con noi. Attualmente mi trovo di fronte a un progetto in cui semplicemente non posso cambiare l'asserzione del team secondo cui un'unità di lavoro non vale la pena di implementarla. Prova come potresti, potresti essere bloccato in una situazione simile.

Quindi cosa fai allora?

Nella mia esperienza di sviluppatore, ho visto altri approcci per provare ad affrontare questo problema. Sono, a mio parere, inferiori a un'unità di lavoro, ma a volte sono più facili e abbastanza buoni per un'applicazione di piccole dimensioni. Voglio solo far notare i notabili e perché sono / non sono buoni.

1. Disponi anche di repository con un ambito che va oltre un solo tipo di entità .

Ad esempio, se una persona ha molti cappelli e molti gatti, allora ti aspetteresti 3 repository separati. Tuttavia, se si accede solo a cappelli e gatti come parte di una persona (mai da soli), le entità Cappello e Gatto non si trovano sullo stesso piano dell'entità Person. Si tratta di un'entità "subordinata" che viene effettivamente utilizzata come una proprietà che semplice accade per essere IEnumerable ma che altrimenti funziona esattamente come una proprietà.

In questo caso, ho visto repository come PersonDetailRepository creati, che ti dicono che questo repository appartiene all'entità Person e tutte le sue entità subordinate .

Possono coesistere con repository con scope entità. Ad esempio, potresti avere un back-end di amministrazione che consente agli utenti di creare entità in una tabella; e potresti avere un sito web per gli utenti finali in cui possono guardare un oggetto dati persona + gatto + cappello.

Il problema con questo approccio è che si finisce per duplicare molta logica tra PersonRepository e PersonDetailRepository , e non copre nemmeno i casi d'uso per entrare in "main" (al contrario di "subordinato" ) entità insieme.

2. Richiede che un repository restituisca un elenco della sua entità principale con possibili oggetti di scena nav.

In altre parole:

  • Quando ottieni un elenco di gatti (incluso il loro proprietario), questo è un metodo CatRepository .
  • Quando ottieni un elenco di persone (compresi i loro gatti), questo è un metodo PersonRepository .

In questo approccio, va bene utilizzare più di un tipo di entità in un repository, purché il tipo di ritorno principale del metodo corrisponda al tipo di entità del repository stesso.

Ho usato questo metodo con un successo migliore rispetto all'approccio 1. Ciò crea un modello coerente, ti consente di unire i dati al contenuto del tuo cuore, ma comunque di semplificare maggiormente i metodi in modo che ogni metodo abbia solo un posto logico per esiste (invece di metterlo in qualsiasi repository per qualsiasi dei tipi di entità utilizzati nella query).

È perfetto? No. Non hai ancora sicurezza transazionale quando aggiorna molte entità di tipi diversi. Ma per i dati recupero , che è dove la maggior parte della tua logica di unione tende a verificarsi, questo ti dà un modo per mantenere la separazione sensoriale, anche se non è teoricamente perfetta.

    
risposta data 19.07.2018 - 14:27
fonte
0

È un po 'confuso perché il tuo esempio di pseudo codice suggerisce un repository generico per entità, ma il tuo esempio di vita reale ha un repository per un DB (o almeno un insieme di entità correlate).

Tuttavia, direi che non c'è niente di particolarmente sbagliato nel tuo esempio di vita reale.

Il tuo repository restituisce entità di dominio che si associano a tabelle e hanno metodi che presumibilmente usano query veloci invece di esporre un IQueryable e lasciandolo all'utente per scoprire quali query sono lente.

La tua classe AccountingInteraction è presumibilmente parte di un'applicazione e assembla un ViewModel CheckDetail contenente varie entità di dominio.

Finché i metodi di repository esposti sono performanti, dovrebbe essere anche l'assembly di ViewModel.

ViewModel è correttamente separato dal dominio e dai livelli dati.

Se avessi qualche critica, direi che CheckDetail sarebbe stato migliorato semplicemente includendo intere entità di dominio e che i filtri extra potrebbero essere spostati nel repository per ottenere prestazioni. Ciò dipende tuttavia dai dettagli del tuo caso, forse un ViewModel molto specifico è più semplice e la logica della clausola where è un caso aziendale specifico che vuoi evitare di inserire nel livello dati

es

//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();
    
risposta data 19.07.2018 - 14:30
fonte

Leggi altre domande sui tag