web-dev-qa-db-fra.com

Modèle de référentiel et requêtes jointes

En conjonction avec les tests unitaires et l'injection de dépendances I (et mon collègue principal) explorent les référentiels. Cependant, nous ne pouvons pas parvenir à un plan d'action solide pour la mise en œuvre.

Dans un scénario de base, nous avons un référentiel qui encapsule un seul contexte et une ou plusieurs entités. Les méthodes publiques de ce référentiel renvoient soit une liste, soit un résultat d'entité unique. C'EST À DIRE

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

Tout cela va bien et fonctionne bien pour nous 99% du temps. Le dilemme, c'est quand il y a plusieurs entités dans un Repo et qu'une jointure est requise. Actuellement, nous faisons juste quelque chose comme ci-dessous dans une classe UOW qui utilise le référentiel;

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

D'après les nombreuses discussions/publications/blogs/etc. contradictoires sur les référentiels et nos sentiments personnels, cela ne semble pas correct. Ce qui ne semble pas non plus correct, c'est de faire la jointure à l'intérieur du référentiel, puis de renvoyer quelque chose qui ne fait pas partie des entités.

Notre conception typique consiste à avoir un contexte enveloppé dans un référentiel qui est injecté dans une classe UOW. De cette façon, nous pouvons avoir un test unitaire qui se moque du Repo renvoyant de faux résultats DB.

Sachant que c'est une question chargée, quel pourrait être un bon schéma pour nous?


Pour un exemple plus réel d'un scénario de jointure (je ne suis pas satisfait de ce code, c'était une tâche urgente de faire bouger les choses, mais c'est un bon exemple du scénario que nous devons aborder):

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

Responsabilité principale du modèle de référentiel d'abstraire la base de données réelle de la base de code du domaine.
Lorsque vous avez un référentiel par entité, vous divulguerez les détails d'implémentation de la base de données à la couche de domaine.

Ayez à la place des abstractions basées sur le domaine, par exemple

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

Ensuite, dans le projet d'accès à la base de données, vous pouvez implémenter ce référentiel de la manière la plus efficace que le cadre de base de données actuel permettra.

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

Ainsi, au lieu de mettre en miroir la structure de la base de données dans votre référentiel, ayez des abstractions qui correspondent aux besoins du domaine, puis implémentez ces abstractions en utilisant efficacement les fonctionnalités de la base de données.

10
Fabio

Ce qui ne semble pas non plus correct, c'est de faire la jointure à l'intérieur du référentiel, puis de renvoyer quelque chose qui ne fait pas partie des entités.

Il s'agit de l'imperfection essentielle des référentiels sans UOW. Les référentiels sont limités à un seul type d'entité. Tout ce qui fait partie du référentiel (c'est-à-dire unique à un seul objet référentiel) est donc intrinsèquement limité à un seul type d'entité.

Il s'agit principalement d'un argument stylistique et théorique. Les référentiels sont parfaitement capables de renvoyer plus d'un type, mais cela semble sale de le faire lorsque le référentiel a été créé essentiellement pour gérer un type d'entité donné.

À mon avis, ce n'est rien de plus que erreur de développeur. Vous avez implémenté un système qui fonctionne au niveau technique, mais qui présente des problèmes de performances considérables. Les référentiels sans uow sont construits en supposant que les interactions de base de données sont limitées aux méthodes get/set basées sur des objets; et que tout assemblage ou opération de données (par exemple, un groupe par + nombre) est effectué en mémoire .

Bien que cela fonctionne au niveau technique, il échoue au niveau des performances. Le modèle prévu ne correspond tout simplement pas à l'exécution requise.

Notre conception typique consiste à avoir un contexte enveloppé dans un référentiel qui est injecté dans une classe UOW. De cette façon, nous pouvons avoir un test unitaire qui se moque du Repo renvoyant de faux résultats DB.

Une unité de travail s'attaque à ce problème précis. Il inverse l'ordre des opérations. Au lieu de plusieurs référentiels avec chacun leur propre contexte, vous obtenez un contexte avec de nombreux référentiels.

La réponse courte ici est que si vous voulez que vos problèmes soient résolus sans défauts ni solutions de contournement à moitié déterminées, vous devez utiliser une unité de travail. Fin de l'histoire.

Cependant, la réalité n'est pas toujours d'accord avec nous. Je suis actuellement confronté à un projet où je ne peux tout simplement pas changer l'affirmation de l'équipe selon laquelle une unité de travail ne vaut pas le temps de la mettre en œuvre. Essayez comme vous pourriez, vous pourriez être coincé dans une situation similaire.

Alors que fais-tu alors?


D'après mon expérience en tant que développeur, j'ai vu d'autres approches pour essayer de résoudre ce problème. Ils sont, à mon avis, inférieurs à une unité de travail, mais ils sont parfois plus faciles et assez bons pour une application de petite taille. Je veux juste souligner les notables et pourquoi ils n'étaient pas bons.

1. Ont également des référentiels qui s'étendent à plusieurs types d'entités.

Par exemple, si une personne a plusieurs chapeaux et plusieurs chats, vous vous attendez à 3 référentiels distincts. Cependant, si vous accédez uniquement aux chapeaux et aux chats en tant que membre d'une personne (jamais par eux-mêmes), les entités Chapeau et Chat ne sont pas sur un pied d'égalité avec l'entité Personne. Ils sont une entité "subordonnée" qui est effectivement utilisée comme une propriété qui ( se trouve être IEnumerable mais sinon fonctionne exactement comme une propriété .

Dans un tel cas, j'ai vu des référentiels comme PersonDetailRepository créés, vous indiquant effectivement que ce référentiel est à la portée de l'entité Person et de toutes ses entités subordonnées .

Ceux-ci peuvent coexister avec des référentiels à portée d'entité. Par exemple, vous pourriez avoir un backend d'administration qui permet aux utilisateurs de créer des entités dans une table; et vous pourriez avoir un site Web d'utilisateur final où ils peuvent regarder un objet de données personne + chat + chapeau.

Le problème avec cette approche est que vous finissez par dupliquer beaucoup de logique entre le PersonRepository et le PersonDetailRepository, et il ne couvre même pas les cas d'utilisation pour rejoindre "main" (par opposition à "subordonnées") ensemble.

2. Exiger qu'un référentiel renvoie une liste de son entité principale avec des accessoires de navigation possibles.

En d'autres termes:

  • Lorsque vous obtenez une liste de chats (y compris leur propriétaire), c'est une méthode CatRepository.
  • Lorsque vous obtenez une liste de personnes (y compris leurs chats), c'est une méthode PersonRepository.

Dans cette approche, il est correct d'utiliser plus d'un type d'entité dans un référentiel, tant que le type de retour principal de la méthode correspond au type d'entité du référentiel lui-même.

J'ai utilisé cela avec plus de succès que l'approche 1. Cela crée un modèle cohérent, vous permet de joindre des données au contenu de votre cœur, mais toujours de rationaliser les méthodes de manière à ce que chaque méthode n'ait qu'un seul endroit logique pour exister (à la place de le placer dans tout référentiel pour tout des types d'entités utilisés dans votre requête).

C'est parfait? Non. Vous n'avez toujours pas de sécurité transactionnelle lorsque mettez à jour de nombreuses entités de types différents. Mais pour les données récupération, qui est l'endroit où la plupart de votre logique de jonction a lieu, cela vous donne un moyen de garder la séparation sensée, même si elle n'est pas théoriquement parfaite.

1
Flater

C'est un peu déroutant car votre exemple de pseudo-code suggère un référentiel générique par entité, mais votre exemple réel a un référentiel pour une base de données (ou au moins un ensemble d'entités liées).

Cependant, je dirais qu'il n'y a rien de particulièrement mal avec votre exemple réel.

Votre référentiel renvoie des entités de domaine qui sont mappées à des tables et ont des méthodes qui utilisent vraisemblablement des requêtes rapides plutôt que d'exposer un IQueryable et de laisser à l'utilisateur le soin de découvrir quelles requêtes sont lentes.

Votre classe AccountingInteraction fait probablement partie d'une application et assemble un CheckDetail ViewModel contenant diverses entités de domaine.

Tant que les méthodes de référentiel exposées sont performantes, l'assemblage du ViewModel devrait également l'être.

Le ViewModel est correctement séparé du domaine et des couches de données.

Si j'avais des critiques, je dirais que CheckDetail serait amélioré en incluant simplement des entités de domaine entières et les filtres supplémentaires pourraient être déplacés vers le référentiel pour des performances. Cela dépend des détails de votre cas, cependant, un ViewModel très spécifique est plus facile et la logique de la clause where est un cas métier spécifique que vous voulez éviter de mettre dans la couche de données

par exemple

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