【问题标题】:Aggregate functions with EF repository使用 EF 存储库聚合函数
【发布时间】:2017-03-23 14:05:48
【问题描述】:

假设我有一个使用 EF 6.0 的产品目录 MVC Web 应用程序,其中所有 CRUD 操作都由存储库处理。现在,其中一个视图必须显示CategoriesList 以及来自Category 的一些关于Products 的聚合信息,我的意思是Category 名称列旁边应该出现三列,总数为该类别的产品,以及产品的最低和最高价格。

public class Product
{
    public string   Name    { get; set; }
    public decimal  Price   { get; set; }
    public int      CategoryId             { get; set; }
    public virtual  Category    Category   { get; set; }
}

public class Category
{
    public string   Name    { get; set; }
    public virtual  ICollection<Product>   Products   { get; set; }
}

如果我有烘焙产品的存储库

unitOfWork.ProductRepository.Insert(new Product(){Name="Doughnut", Price=4.0, CategoryId=1});
unitOfWork.ProductRepository.Insert(new Product(){Name="Apple Pie", Price=7.0, CategoryId=1});
unitOfWork.ProductRepository.Insert(new Product(){Name="Meat Pie", Price=9.0, CategoryId=1});

我需要显示视图

#  Category        Total Products        Min Price        Max Price
1  Bakery products 3                     4                9

我不认为从存储库中处理查询是一个好主意,因为它破坏了使用存储库的所有想法,而且 AFAIK 被广泛认为是一种不好的做法。
可以编写一些直接使用DbContext 的查询,但我想知道这种类型的任务在现实世界的应用程序中是如何处理的? 也许需要一个新的视图模型,然后应该添加另一个只读存储库来获取这些数据?像这样?

public class CategoryStatsVM
{
    public string   Name    { get; set; }
    public virtual  ICollection<Product>   Products   { get; set; }
    public int      Count   { get; set; }
    public decimal  MinPrice    { get; set; }
    public decimal  MaxPrice    { get; set; }
}

【问题讨论】:

  • 您的问题不是由 EF 引起的(DbContext Repository 和 UOW 并且很容易支持此类场景),而是来自您放在上面的其他抽象(限制) ,所以请删除entity-framework标签。
  • @IvanStoev 我正在关注微软的文章docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions/…,它解释了如何在 EF 之上实现 Repository 和 UoW,所以是的,我有一个额外的抽象,但我想了解人们如何处理这些当他们遵循 Microsoft 的文档时遇到问题。

标签: c# asp.net-mvc entity-framework repository


【解决方案1】:

我不确定您所说的“我不认为在存储库之外处理查询是一个好主意,因为它破坏了使用存储库的所有想法”。 存储库背后的主要思想是抽象出数据访问。如果你想做一些聚合、一些其他的计算或类似的事情,那主要是你的业务逻辑的一部分。 VM 是需要您进行计数、最大值和最小值的组件。 VM基本上是一个抽象视图,视图通常来源于业务需求。所以我认为做这些查询不是数据访问的一部分,因此不是存储库的一部分。 我要做的是向存储库接口添加一个方法,如下所示:

public IQueryable<Category> GetById(int i)
{
    return dbContext.Category.Single(c=>c.CategoryId==i);
}

然后在业务逻辑中你可以这样使用它:

unitOfWork.CategoryRepository.GetById(3).Products.Count();
unitOfWork.CategoryRepository.GetById(3).Products.Min(p=>p.Price);

这还有一个额外的好处,那就是不会失去对实体的 LInQ 的延迟执行,因此将在数据库上执行计数和查找最小值。

【讨论】:

  • 感谢您的洞察力,但我相信您误会了我的意思,我想显示一个包含Categories 列表的网格,我不想运行几十个查询来构建它.为了理解我所说的返回 IQueryable 是一个坏主意,你可以阅读 f.e.这篇文章codetunnel.io/… 和许多其他文章,只需用谷歌搜索即可。但同样,我并不是说这是做事的正确方式,我只是想了解如果我遵循最佳实践应该如何实现......
  • 感谢您的博文。我不一定同意,但阅读其他意见总是好的。
  • 完全同意你的看法,这就是我在这里问的原因。如果您能想到一些可以解决我的限制的事情,如果您能分享您的想法,我将不胜感激。
【解决方案2】:

正如@Ivan Stoev 已经提到的。 DbContext 是存储库。它处理所有基本的 CRUD 操作,而在它之上的另一个存储库层似乎是多余的。您所说的听起来更像是一个业务层,它调用“原始”数据的存储库,应用额外的逻辑并向表示层提供数据。

因此,在您的情况下,您将拥有一个服务/业务工厂/​​类 - 无论您如何称呼它 - 使用如下方法:

public IEnumerable<CategoryStatsVM> GetCategoryStats() 
{
    IList<Category> categories = dbContext.Categories.Include(m => m.Products).ToList();

    foreach (Category category in categories)
    {
        yield return new CategoryStatsVM
        {
            Name = category.Name,
            Count = category.Products.Count(),
            Products = this.GetProducts(category.Products),
            MinPrice = category.Products.Aggregate(GetMinPrice),
            MaxPrice = category.Products.Aggregate(GetMaxPrice) 
        }
    }
}

private Product GetMinPrice(Product min, Product current)
{
    return min == null || curr.Price < min.Price ? curr : min;
}

private Product GetMaxPrice(Product max, Product current)
{
    return max == null || curr.Price > max.Price ? curr : max;
}

private IEnumerable<ProductVM> GetProducts(IEnumerable<Product> products)
{
    List<ProductVM> products; // create instance of the product view model list
    return products;
}

关于最后一个函数的注释 - 它在那里是因为您也应该为产品创建一个视图模型类。您不应该在表示层中使用数据模型。

我为聚合创建了单独的函数,只是为了使其更具可读性。您当然可以通过一种方法获得所有内容:

MinPrice = category.Products.Aggregate((min, curr) => min == null || curr.Price < min.Price ? curr : min), 

【讨论】:

  • 如果你这样使用IEnumerable,你会损失很多性能。您将所有类别对象读入内存以进行计数,而不是让 IQueryable 转换为 SQL 查询,由数据库执行并仅返回一个数字。
  • 仅仅使用 IEnumerable 的成员类型并不意味着您将所有对象加载到内存中。 IQueryable 也继承自 IEnumerable。变量定义不影响代码的执行。您对查询所做的操作决定了您是使用内存中的对象还是在需要对象时进行数据库调用。它是 GetCategoryStats 中将所有内容加载到内存中的第一行,即 .ToList() 方法。这种方式是否更好,取决于其他逻辑、数据库结构、项目数量等。但 IEnumerable 与此无关。
  • 我想我并不清楚,但我的意思正是你所说的“如果你使用 IEnumerable 像这样”。我应该更清楚,并补充说我也指的是“ToList()”。这样你就不用为使用组件提供任何控制来决定何时执行。如果您保留 IQueryable 和查询本身,您将保留控制查询执行甚至组合的选项,这些都是重要功能。
  • 感谢您的洞察力,我不想争论,但如果这层抽象是多余的,微软为什么要发布这篇文章?-docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions/…
  • 老实说。这正是我几年前所问的:)我也曾经使用过这种模式,而且我很确定这正是我之前读过的这篇文章。唯一合理的论点是,使用本文中描述的这种模式,您可以轻松地从 EF 切换到其他东西,并保持业务层不变。但这只是理论。练习通常没那么简单:)
【解决方案3】:

您可以使用这样的 Aggregate 函数来获取 UserLogs 最后记录的事务文件

UsersLogs.Where(i => i.Id== _id).Max(x => x.transId)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-09
    相关资源
    最近更新 更多