【问题标题】:Best practice: entity framework: where to do joins if using repository and UoW patterns最佳实践:实体框架:如果使用存储库和 UoW 模式,在哪里进行连接
【发布时间】:2019-03-12 21:55:22
【问题描述】:

我有一个用户存储库和合作伙伴存储库。我的存储库不返回 IQuerables。用户实体有一个 partnerID。我想通过使用 Linq 的存储库来使用 partnerID 来加入两个表 user 和 partner Table。但是我不确定在哪里进行这些连接。合作伙伴和用户没有外键,因此我无法通过导航属性进行包含。

我知道联接不应该进入存储库。加入应该发生在 UoW 吗?还是服务?就我在哪里进行这些连接而言,最佳做法是什么?

【问题讨论】:

  • 最佳实践是不要在存储库/uow 后面使用实体框架,这只会给您带来很大的性能损失。如果您不打算使用 Entity Framework 的单个功能,请使用 Dapper。此外,JOIN 总是必须在单个数据库查询中运行,不要为了遵循完全过时的模式而破坏性能
  • 完全同意@CamiloTerevinto。存储库模式用于低级数据访问,例如直接使用 SQL(ADO.NET、Dapper 等)。它不适用于像 EF 这样的 ORM,已经实现存储库/UoW 模式。使用 ORM 时,您选择使用第三方 DAL,而不是创建自己的 DAL。就那么简单。如果您的目标是抽象数据层,那么您应该寻找更高级别的架构模式,例如微服务。
  • 我主要使用它来帮助单元测试,我已经阅读并看到了这种模拟方法的好处:docs.microsoft.com/en-us/dotnet/standard/…。我不应该使用这种模式吗??
  • 我的存储库不返回 IQuerables - 好吧,如果您坚持将 DbSets 包装在冗余存储库中,至少修复 那个 部分.并确保两个 repos 具有相同的上下文实例。那么导航属性呢?

标签: c# .net entity-framework asp.net-core design-patterns


【解决方案1】:

聚合根:https://martinfowler.com/bliki/DDD_Aggregate.html

在您的聚合中,根目录将是您的存储库的名称,因为

来自聚合外部的任何引用都应该只转到 聚合根

【讨论】:

  • 谢谢。是的,我已经这样做了,但是我需要在存储库中进行的联接在表中没有外键,所以我不能使用 Include。米
【解决方案2】:

在我们公司,我们将包含必须执行的用例的对象(工作单元)与存储数据的概念(存储库)与如何存储数据的方法(在使用实体框架的数据库中)分开.

将 DbContext 与 Repository 分离的优点

通过使用这种分离,可以将数据库更改为存储表的任何其他内容。例如,您可以使用一系列 CSV 文件,或使用 Dapper 访问数据库。

Entity Framework 和 Repository 分离的另一个优点是,您可以提供不授予访问您不希望用户访问的项目的接口。例如,一些用户可能只查询数据,另一些用户可能会添加或更新数据,而只有少数用户可能会删除对象。

一个非常好的副作用是我们可以对使用带有Test Lists 集合而不是真实数据库表的存储库的代码进行单元测试。

仅当新用例需要新数据时,我们才需要更改所有这三个。我们工作单元中不需要新数据的用户不会注意到差异

数据库上下文

DbContext 中的 DbSet 代表我们数据库的表。每个表至少有一个 Id 作为主键,以及一个可以为空的 DateTime 对象,用于标记对象被声明为过时的日期。后台进程会定期删除所有过时一段时间的对象。

后一部分是为了防止用户 A 正在更新一条记录,而用户 B 正在删除同一条记录。用户只能将记录标记为过时,不能删除。

interface IDbItem
{
    int Id {get; }      // no need to ever change the primary key
    DateTime? Obsolete {get; set;}
}

例如客户:

class Customer : IDbItem
{
     public int Id {get; set;}
     public DateTime? ObsoleteDate {get; set;}

     public string Name {get; set;}
     ... // other properties
}

DbContext 尽可能简单:它只表示表和表之间的关系

存储库

存储库隐藏了用于存储数据的存储方法。它可以是一个数据库,一系列 CSV 文件,数据可以分布在多个数据库中。

存储库通常有几个接口:

  • 仅查询接口,返回您要公开的每个表的IQueryable<...>。这个界面的用户可以做他们想做的每一个查询。他们无法更改数据。这样做的好处是您可以隐藏不想公开的属性和表。用户不能意外更改项目。
  • 创建/更新项目以及查询的界面。对于真正添加或更新数据库的少数表单。他们还可以将项目标记为已过时。
  • 删除标记为Obsolete 的数据的接口。由后台进程用于定期删除过时的数据。

就像实体框架有代表实体的类(表:客户、订单、订单行等)和代表实体集合的类(IDbSet<Customer>)一样,存储库也有类似的类和接口。它们中的大多数都是可重复使用的,并且是单行的

存储库实体类

interface IId
{
    int Id {get;}
}

interface IRepositoryEntity : IId
{
    bool IsObsolete {get;}
    void MarkObsolete();
}

每个存储库项目都可以标记为过时。一个通用的基类:

class RepositoryEntity<TSource> : IId, IRepositoryEntity
      where TSource : IDbItem
{
     public TSource DbItem {get; set;}

     // Interface IId
     public int Id => this.DbItem.Id;

     // Interface IRepositoryEntity
     public bool IsObsolete => this.DbItem.ObsoleteDate != null;
     public void MarkObsolete()
     {
         this.DbItem.ObsoleteDate = DateTime.UtcNow;
     }
}

例如客户:

interface IReadOnlyCustomer : IId
{
    string Name {get;}
    ...
}
interface ICustomer : IRepositoryItem
{
    string Name {get; set;}
}
class Customer : RepositoryEntity<Customer>, IReadOnlyCustomer, ICustomer
{
     // Interfaces IId and IRepositoryItem implemented by base class
    
     // Interface ICustomer
     public string Name {get; set;}
     ...

     // Interface IReadOnlyCustomer
     string IReadOnlyCustomer.Name => this.Name;
     ...
}

您会看到存储库客户只需要实现您实际想要向外部世界公开的客户属性。存储库不需要代表您的数据库表。

例如,如果您的数据库有客户FirstName、MiddleName、FamilyName 的拆分值,那么您可以在 get Name 函数中将它们连接起来。

存储库集合

存储库集合类似于IDbSet&lt;...&gt;。 Query only 有一个接口,Query, Update, Mark Obsolete 有一个接口。当然,我们也有完全的访问权限,只授予少数快乐的人。

对于 ReadOnly 来说,IQueryable&lt;TEntity&gt; where TEntity : Iid 就足够了

要查询/添加/更新/废弃,我需要一个 ISet 和一个 Set:

interface ISet<TEntity> : IQueryable<TEntity> where TEntity: IRepositoryEntity
{
     TEntity Add(TEntity item);
}

class Set<TEntity, TDbEntity> : ISet<TEntity>
  where TEntity: IRepositoryEntity,
  where TDbEntity: IDbItem
{
     public IDbSet<TEntity> DbSet {get; set;}

     // implement the interfaces via DbSet
     public TEntity Add(TEntity item)
     {
         // TODO: convert item to a dbItem
         return this.DbSet.Add(dbItem);
     }
     // Similar for IQueryable<TEntity> and IQueryable
}

ReadOnly 访问和 CRUD 访问的接口:

interface IReadOnlyRepository : IDisposable
{
     IQueryable<IReadOnlyCustomer> Customers {get;}
     IQueryable<IReadOnlyOrders> Orders {get;}
}
interface IRepository : IDisposable
{
     ISet<ICustomer> Customers {get;}
     ISet<IOrder> Orders {get;}
     void SaveChanges();
}

有权访问 ReadOnlyRepository 的人只能查询数据。他们无法做出任何改变。有权访问 IRepository 的用户可以添加项目、更新项目和保存更改。

类存储库实现所有接口:

class Repository : IReadOnlyRepository,     // Query Only
 IRepository,                               // Query, Add and Update
 IDisposable
{
     private readonly dbContext = new CustomerDbContext();
     // TODO: Dispose() will Dispose dbContext

     // Used by the other interfaces
     protected IDbSet<Customer> Customers => this.dbContext.Customers;
     protected IDbSet<Orders> Orders => this.dbContext.Orders;
     void SaveChanges() {this.dbContext.SaveChanges();}

     // IRepository:
     ISet<ICustomer> IRepository.Customers => new Set<Customer>{DbSet = this.Customers};
     ISet<IOrder> IRepository.Orders => new Set<Order>{DbSet = this.Orders};
     void IRepository.SaveChanges() {this.DbContext.SaveChanges();}

     // IReadOnlyRepository
     IQueryable<IReadOnlyCustomer> IReadOnlyRepository.Customers => this.Customers;
     IQueryable<IReadOnlyOrders> IReadOnlyRepository.Orders => this.Orders;
}
    

看起来代码很多,但是大部分函数都是单行代码,调用对应的entity-framework函数。

最后,我们需要一个创建存储库的工厂。如果您想为多个存储库重新使用它,请创建一个通用工厂类。为简单起见,我为 Ordering 数据库创建它:

class OrdersRepository
{
    public IReadOnlyRepository CreateReadOnly()
    {
         // TODO: if desired check rights: can this user access this database?
         return new Repository();
    }
    public IRepository CreateUpdateAccess()
    {
         // TODO: if desired check rights: can this user access this database?
         return new Repository();
    }
    public Repository CreateFullControl()
    {
         // TODO: if desired check rights: can this user access this database?
         return new Repository();
    }

事实上:对于删除所有过时项目的后台进程,我们有一个特殊的界面,可以删除所有过时一段时间的项目。此处不再提及。

用法:

var repositoryFactory = new RepositoryFactory() {AccessRights = ...}

// I need to query only:
using (var repository = repositoryFactory.CreateUpdatAccess())
{
     // you can query, change value and save changes, for instance after a Brexit:
     var customersToRemove = repository.Customers.Where(customer => customer.State == "United Kingdom")
     foreach (var customerToRemove in customersToRemove);
     {
         customerToRemove.MarkObsolete();
     }
     repository.SaveChanges();
}

// I need to change data:
using (var repository = repositoryFactory.CreateReadOnly())
{
     // do some queries. Compiler error if you try to change
}

【讨论】:

  • 列出的存储库/UoW 模式的“优势”都与直接使用 EF 不同,尤其是现在 EF Core 支持内存提供程序。唯一的轻微优势是不依赖于特定的 ORM,但切换 ORM 有足够的摩擦力,它会很好地扩展到您的存储库/UoW 层,导致无论如何都必须重写应用程序代码。
  • int Id {get; } 然后当一个表需要一个复合主键甚至只是一个 Guid 时,一切都去 sh*t
  • 完全。这就是 ORM 上的存储库/UoW 的问题。不可避免地,您的 repo 层将不支持 ORM 所做的事情,这使得将内容从一个翻译到另一个几乎是不可能的。如果您构建了一个非常复杂的 repo 层,它可以支持 EF 所做的一切,那么您只需重建 EF。换句话说,EF 是你的数据层。除此之外,添加您自己的数据层是没有意义的。人们使用第三方路由系统或第三方模板系统没有问题,但他们不知何故认为他们需要创建自己的 DAL。
  • 如果我直接使用 dbContext 我将我的数据库结构暴露给用户。每个拥有 dbContext 的人都可以添加/删除/更改值。相当多的用户只需要查询数据。你必须在某个地方保护它。离 DbContext 越远,这种拆分成多个接口的可能性就越小
猜你喜欢
  • 2020-06-02
  • 1970-01-01
  • 1970-01-01
  • 2013-11-08
  • 2014-04-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多