web-dev-qa-db-ja.com

リポジトリパターンと結合クエリ

単体テストおよび依存性注入と連携して、私(および私の主要な同僚)はリポジトリを調査しています。しかしながら、我々は実施のための確かな行動計画を立てることはできません。

基本的なシナリオでは、単一のコンテキストと1つ以上のエンティティをカプセル化するリポジトリがあります。そのリポジトリのパブリックメソッドは、リストまたは単一のエンティティの結果を返します。 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;
    }
}

これはすべてうまく機能しており、99%の時間で問題なく機能します。問題は、リポジトリに複数のエンティティがあり、結合が必要な場合です。現在、リポジトリを使用するUOWクラスで次のようなことを行っています。

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

リポジトリと私たち自身の個人的な感情についての多くの矛盾した議論/投稿/ブログ/その他から、これは正しくないようです。また、正しくないように見えるのは、リポジトリ内で結合を行ってから、エンティティーの1つではない何かを返すことです。

私たちの典型的な設計は、UOWクラスに依存性注入されたリポジトリ内にラップされたコンテキストを持つことです。このようにして、Repoが偽のDB結果を返すモックを行う単体テストを作成できます。

これがロードされた質問であることを知って、私たちにとって良いパターンは何でしょうか?


結合シナリオのより現実的な例(このコードには不満です。何かを起こすのはラッシュの仕事でしたが、これは私たちが取り組む必要があるシナリオの良い例です):

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

ドメインコードベースから実際のデータベースを抽象化するリポジトリパターンの主な責任。
エンティティごとに1つのリポジトリがある場合、データベース実装の詳細をドメインレイヤーにリークします。

代わりに、ドメインベースの抽象化を使用します。

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

次に、データベースアクセスプロジェクトで、現在のデータベースフレームワークで可能な最も効率的な方法でこのリポジトリを実装できます。

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

したがって、リポジトリ内のデータベース構造をミラーリングする代わりに、ドメインのニーズに合った抽象化を用意し、データベースの機能を効果的に使用してそれらの抽象化を実装します。

10
Fabio

また、正しくないように見えるのは、リポジトリ内で結合を行ってから、エンティティーの1つではない何かを返すことです。

これは、UOWのないリポジトリの本質的な欠陥です。リポジトリのスコープは単一のエンティティタイプです。したがって、リポジトリの一部であるもの(つまり、単一のリポジトリオブジェクトに固有のもの)も、本質的に単一のエンティティタイプにスコープされます。

これは主に文体的で理論的な議論です。リポジトリは複数のタイプを完全に返すことができますが、リポジトリが特定の1つのエンティティタイプを処理するために作成された場合、そうするのは面倒です。

私の意見では、これは開発者エラーにすぎません。技術レベルで機能するシステムを実装しましたが、パフォーマンスにかなりの問題があります。 Uow-lessリポジトリーは、データベースの対話がオブジェクトベースのget/setメソッドに限定されているという想定の下で構築されています。そして、データのスティッチングまたは操作(+カウントによるグループなど)が実行されるメモリ内

これは技術レベルでは機能しますが、パフォーマンスレベルでは失敗します。 意図したパターンは単に必要な実行に適合しません。

私たちの典型的な設計は、UOWクラスに依存性注入されたリポジトリ内にラップされたコンテキストを持つことです。このようにして、Repoが偽のDB結果を返すモックを行う単体テストを作成できます。

作業単位はこの正確な問題に取り組みます。操作の順序を逆にします。独自のコンテキストを持つ多数のリポジトリーの代わりに、多数のリポジトリーを持つ1つのコンテキストを取得します。

ここでの短い答えは、欠陥や中途半端な回避策なしに懸念事項を解決したい場合は、作業単位を使用する必要があるということです。話の終わり。

しかし、現実は常に私たちに同意するわけではありません。私は現在、作業単位は実装する価値がないというチームの主張を変更できないプロジェクトに直面しています。可能であれば、同じような状況で行き詰まる可能性があります。

それであなたはその後何をしますか?


開発者としての私の経験では、この問題に取り組むために他のアプローチを見てきました。私の意見では、これらは作業単位よりも劣っていますが、場合によっては簡単で、小規模アプリケーションの場合は十分十分です。注目すべき点とその理由を指摘したいのですが。

1。複数のエンティティタイプにスコープするリポジトリもある

たとえば、ある人に多くの帽子と猫がいる場合、3つの別々のリポジトリが必要になります。ただし、人の一部として帽子と猫にのみアクセスする場合(自分自身では決して)、帽子と猫のエンティティはPersonエンティティと同等の立場にはありません。これらは、単純なhappensIEnumerableになるプロパティとして効果的に使用される「従属」エンティティですが、それ以外はプロパティとまったく同じように機能します。

このような場合、PersonDetailRepositoryのようなリポジトリが作成され、このリポジトリがPersonエンティティおよびそのすべての従属エンティティのスコープ内にあることがわかります。

これらは、エンティティスコープのリポジトリと共存できます。たとえば、ユーザーがテーブルにエンティティを作成できるようにする管理バックエンドがあるとします。また、person + cat + hatデータオブジェクトを確認できるエンドユーザーWebサイトがあるとします。

このアプローチの問題は、PersonRepositoryPersonDetailRepositoryの間で多くのロジックを複製することになり、 "main"に参加するためのユースケースさえカバーしないことです(反対に) 「下位」)エンティティを一緒に。

2。リポジトリがそのメインエンティティと可能なナビゲーションプロパティのリストを返すことを要求します。

言い換えると:

  • 猫(飼い主を含む)のリストを取得すると、それはCatRepositoryメソッドになります。
  • 人(猫を含む)のリストを取得する場合、それはPersonRepositoryメソッドです。

このアプローチでは、メソッドのメインの戻り値の型がリポジトリ自体のエンティティ型と一致している限り、リポジトリで複数のエンティティ型を使用しても問題ありません。

私はこれをアプローチ1よりも優れた方法で使用しました。これにより、一貫したパターンが作成され、データを心のコンテンツに結合できますが、ほとんどの場合、すべてのメソッドが1つの論理的な場所しか存在しないようにメソッドを合理化します(代わりにクエリで使用されるエンティティタイプのanyのリポジトリをanyに配置する方法)。

完璧ですか?いいえ。pdatingさまざまなタイプのエンティティが多数ある場合、トランザクションの安全性はまだありません。しかし、データretrievalの場合、結合ロジックのほとんどが発生する傾向があるため、理論的に完全ではない場合でも、これにより分離を感覚的に保つことができます。

1
Flater

疑似コードの例ではエンティティごとに一般的なリポジトリを提案しているので少し混乱しますが、実際の例にはDB(または少なくとも関連するエンティティのセット)のリポジトリがあります。

ただし、実際の例では特に問題はありません。

リポジトリは、テーブルにマップするドメインエンティティを返し、メソッドを持っていると思われます。IQueryableを公開してユーザーに任せて、遅いクエリを発見するのではなく、高速なクエリを使用します。

AccountingInteractionクラスはおそらくアプリケーションの一部であり、さまざまなドメインエンティティを含むCheckDetail ViewModelをアセンブルします。

公開されたリポジトリメソッドが高性能である限り、ViewModelのアセンブリも同様です。

ViewModelは、ドメインレイヤーとデータレイヤーから正しく分離されています。

批判がある場合は、ドメインエンティティ全体を含めるだけでCheckDetailが改善され、パフォーマンスのために追加のフィルターをリポジトリに移動できると思います。これはケースの詳細に依存しますが、おそらく非常に特定のViewModelの方が簡単であり、where句のロジックは、データレイヤーに配置しないようにする特定のビジネスケースです。

例えば

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