从联接表的存储库中返回域对象

问题描述 投票:0回答:2

我一直在阅读,存储库应仅返回域对象。我很难实现这一点。我目前拥有带有服务层,存储库的API,并且正在使用EF Core访问sql数据库。

如果我们将User(Id,名称,地址,电话号码,电子邮件,用户名)和订单(id,OrderDetails,UserId)视为2个域对象。一个客户可以有多个订单。我已经创建了导航属性

public virtual User User{ get; set; }

和外键。

服务层需要返回DTO以及OrderId,OrderDetails,CustomerId,CustomerName。在这种情况下,存储库应该返回什么?这就是我正在尝试的:

public IEnumerable<Orders> GetOrders(int orderId)
        {
            var result = _context.Orders.Where(or=>or.Id=orderId)
                .Include(u => u.User)
                .ToList();
            return result;
        }

我在急切加载时遇到问题。我试图使用包含。我首先使用数据库。在上述情况下,导航属性始终使用NULL进行调整。我能够将数据获取到Navigation Properties中的唯一方法是启用带有上下文代理的延迟加载。我认为这将是一个性能问题

任何人都可以帮助我返回什么,为什么.include无法正常工作?

entity-framework domain-driven-design eager-loading ef-core-3.0
2个回答
0
投票

关于存储库模式的建议是,存储库应返回IQueryable<TEntity>,而不是IEnumerable<TEntity>

存储库的目的是:

  • 使代码更易于测试。
  • 集中通用业务规则。

存储库的目的应not为:

  • 远离您的项目的抽象EF。
  • 隐藏您的域的知识。 (实体)

[如果要引入存储库以隐藏解决方案取决于EF的事实或隐藏域,那么您将牺牲EF可以带到表中的用于管理与数据交互的功能,或者您正在引入很多内容解决方案中不必要的复杂性以尝试保持该功能。 (过滤,排序,分页,选择性急切加载等)

相反,通过利用IQueryable并将EF视为您的域的头等公民,您可以利用EF进行灵活,快速的查询以获取所需的数据。

提供您要在其中“返回具有OrderId,OrderDetails,CustomerId,CustomerName的DTO的服务。”

步骤1:原始示例,没有存储库...

服务代码:

public OrderDto GetOrderById(int orderId)
{
    using (var context = new AppDbContext())
    {
        var order = context.Orders
            .Select(x => new OrderDto
            {
                OrderId = x.OrderId,
                OrderDetails = x.OrderDetails,
                CustomerId = x.Customer.CustomerId,
                CustomerName = x.Customer.Name
            }).Single(x => x.OrderId == orderId);
        return order;
    }    
}

该代码可以很好地工作,但是它与DbContext耦合,因此很难进行单元测试。我们可能还有其他业务逻辑要考虑,这将需要应用于几乎所有查询,例如Orders处于“ IsActive”状态(软删除)还是数据库服务于多个客户端(多租户)。在我们的控制器中将有很多查询,这将导致需要很多东西,例如.Where(x => x.IsActive)

[使用存储库模式(IQueryable),工作单位:

public OrderDto GetOrderById(int orderId)
{
    using (var context = ContextScopeFactory.CreateReadOnly())
    {
        var order = OrderRepository.GetOrders()
            .Select(x => new OrderDto
            {
                OrderId = x.OrderId,
                OrderDetails = x.OrderDetails,
                CustomerId = x.Customer.CustomerId,
                CustomerName = x.Customer.Name
            }).Single(x => x.OrderId == orderId);
        return order;
    }    
}

现在上面的控制器代码具有面值,这与第一个原始示例看起来并没有太大的区别,但是有一些位使此测试成为可测试的并且可以帮助管理诸如通用条件之类的事物。

存储库代码:

public class OrderRepository : IOrderRepository
{
    private readonly IAmbientContextScopeLocator _contextScopeLocator = null;

    public OrderRepository(IAmbientContextScopeLocator contextScopeLocator)
    {
        _contextScopeLocator = contextScopeLocator ?? throw new ArgumentNullException("contextScopeLocator");
    }

    private AppDbContext Context => return _contextScopeLocator.Get<AppDbContext>();

    IQueryable<Order> IOrderRepository.GetOrders()
    {
        return Context.Orders.Where(x => x.IsActive);
    }
}

此示例使用Mehdime的DbContextScope作为工作单元,但是可以适应于其他对象或注入的DbContext,只要它在生命周期内受请求限制即可。它还演示了一个案例,该案例具有非常常见的过滤条件(“ IsActive”),我们可能希望将其集中到所有查询中。

在上面的示例中,我们使用存储库将订单作为IQueryable返回。存储库方法是完全可模拟的,其中可以对DbContextScopeFactory.CreateReadOnly调用进行存根,并且可以模拟存储库调用以使用List<Order>().AsQueryable()返回所需的任何数据。通过返回IQueryable,调用代码可以完全控制如何使用数据。请注意,无需担心急于加载客户/用户数据。在执行Single(或ToList等)调用之前,将不会执行查询,这将导致非常高效的查询。存储库类本身保持非常简单,因为告诉它包括哪些记录和相关数据并不复杂。我们可以调整查询以添加排序,分页(Skip / Take)或获取Count或简单地检查是否存在任何数据(Any),而无需在存储库中添加函数等,也不会增加开销加载数据只是为了进行简单检查。

我听到的关于存储库返回IQueryable的最常见的异议是:

它泄漏。调用方需要了解EF和实体结构。”是的,调用方需要了解EF限制和实体结构。但是,许多其他方法(例如,注入表达式树以管理过滤,排序和急切加载)都需要对EF和实体结构的局限性有相同的了解。例如,注入表达式以执行过滤仍然不能包含EF无法执行的细节。完全抽象出EF将导致存储库中类似但又残缺的方法lot,并且/或者放弃EF带来的许多性能和功能。如果您在项目中采用EF,那么当它被视为项目中的一等公民时,它的工作就会好得多。

作为域层的维护者,当存储库负责标准时,我可以优化代码。”我将此归结为过早的优化。存储库可以强制执行核心级别的过滤,例如活动状态或租期,而所需的查询和检索则由实施代码决定。的确,您无法预测或控制这些查询对您的数据源的外观,但是在考虑实际数据使用时,最好进行查询优化。 EF生成的查询反映了可以进一步完善的所需数据,以及哪些索引最有效的基础。替代方法是尝试预测将使用哪些查询,并为要使用的服务提供那些有限的选择,以使它们请求进一步的“味道”。当将新查询引入存储库比较困难时,这通常会转换为运行效率较低查询的服务,从而更频繁地获取其数据。


0
投票

存储库可以返回其他类型的对象,即使您想根据某个标准计算一定数量的对象,也可以返回整数之类的原始类型。

这是来自域驱动设计的书:

它们(存储库)还可以返回符号信息,例如(域对象的)多少实例满足某些条件的计数。他们甚至可以返回汇总计算,例如具有某个数字属性的所有匹配对象。

如果返回的不是域对象,那是因为您需要有关域对象的某些信息,因此您只应返回不可变的对象和原始数据类型,例如整数。

如果您查询要获取的对象,并且要在获取对象后对其进行更改,那么它应该是域对象。

如果需要,请在域对象周围放置边界,并在聚合中对其进行组织。

这是一篇很好的文章,解释了如何将模型分解为聚合:https://dddcommunity.org/library/vernon_2011/

在您的情况下,您可以将User和Order实体组合在一个Aggreate中,也可以将它们放在单独的Aggregate中。

编辑:

示例:

在这里,我们将使用Reference by Id,并且来自不同聚合的所有实体将按ID引用来自不同聚合的其他实体。

我们将有三个AggregatesUserProductOrder,其中一个ValueObject OrderLineItem

public class User {

    public Guid Id{ get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
}

public class Product {

    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public Money Price { get; private set; }
}

public class OrderLineItem {

    public Guid ProductId { get; private set; }
    public Quantity Quantity { get; private set; }
    // Copy the current price of the product here so future changes don't affect old orders
    public Money Price { get; private set; } 
}

public class Order {

    public Guid Id { get; private set; }
    public IEnumerable<OrderLineItem> LineItems { get; private set; }
}

现在,如果您必须在应用程序中进行大量查询,则可以创建一个将由上面的模型创建的ReadModel。>


public class OrderLineItemWithProductDetails {

    public Guid ProductId { get; private set; }
    public string ProductName { get; private set; }

    // other stuff quantity, price etc.
}

public class OrderWithUserDetails {

    public Guid Id { get; private set; }
    public string UserFirstName { get; private set; }
    public string UserLastName { get; private set; }
    public IEnumerable<OrderLineItemWithProductDetails > LineItems { get; private set; }
    // other stuff you will need

}

如何填充ReadModel是一个完整的主题,所以我无法涵盖所有​​内容,但是这里有一些指针。

您说过您将要进行Join,所以您可能正在使用某种类型的RDBMS,例如PosteSQL或MySQL。您可以在特殊的ReadModel Repository

中进行Join。如果您的数据在单个数据库中,则可以只使用ReadModel存储库。

public class OrderReadModelRepository {

    public OrderWithUserDetails FindForUser(Guid userId) {

        // this is suppose to be an example, my SQL is a bit rusty so...
        string sql = @"SELECT * FROM orders AS o 
                    JOIN orderlineitems AS l
                    JOIN users AS u ON o.UserId = u.Id
                    JOIN products AS p ON p.id = l.ProductId
                    WHERE u.Id = userId";

        var resultSet = DB.Execute(sql);

        return CreateOrderWithDetailsFromResultSet(resultSet);
    }
}

如果不是,那么您将不得不构建它并将其保存在单独的数据库中。您可以使用DomainEvents来做到这一点,但是如果您只有一个SQL数据库,我就不会走得太远。

© www.soinside.com 2019 - 2024. All rights reserved.