应该在Repository层还是Service层编写复杂查询?

11

我计划将我们的数据访问层迁移到使用仓储模式和工作单元。

我知道仓储库将帮助我轻松更改持久性存储(数据库,集合等)和技术,如EF到MongoDB。因此,我注意到了一些仓储实现的关键点,例如:

  1. 返回IEnumerable而不是IQueryable
  2. 仓储应该仅负责CRUD操作
  3. 仓储方法的返回类型应该是模型(实体)
  4. 仅为聚合根实现仓储库

如果我在项目中实现仓储时应用这些关键点,我就完全不知道如何处理涉及多个实体的复杂查询。

目前我已经有的是,在BLL库上有很多服务类直接联系EF的DbContextDbSet以及一些验证,如下所示:

public IEnumerable<ProjectDTO> GetProjectWithDetails()
{
    // Validation

    // Logging

    // Can be any logic need to before query data.  

    Dbcontext.Projects.Where(p => 
    // multiple of conditions go here follow business rules
    // conditions will need to check another entities (task, phase, employee...) such as:
    // 1. project have task status 'in-progress' .. etc
    // 2. project have employeeid 1,2,3..
    // 3. project have stask start at some specific date.
    // 4....    
    )
    .Select(p => new ProjectDTO
    {
        Label = p.Label,
        Phase = new PhaseDTO{
            Label = p.Phase.Label,
            Tasks = p.Phase.Tasks.Select(t => new TaskDTO{
                // some related properties
            })
        }
    }).ToList();
} 

我目前正在使用数据传输对象(DTO)作为控制器中模型和视图模型之间的中间类,并使用映射器来映射属性。
如果我将关键注释保留在存储库上,那么我需要进行多次往返数据库以获取数据,而且它会返回整个模型而不是有用的列。但是,如果我将这些方法迁移到存储库中,就会破坏存储库模式,因为它将包含业务逻辑和返回类型而不是模型。
因此问题是,在这种情况下我该怎么办?请给我一些建议,帮助我找到正确的方向。
非常感谢。

你可以在存储库方法中返回 IEnumerable<T>,但我认为使用 IQueryable<T> 也没有什么问题(它与 IEnumerable<T> 一样是 .NET BCL 的一部分)。但即使你想返回 IEnumerable<T>,你也可以在实际返回值中使用 IQueryable<T>,因为它扩展了 IEnumerable<T>。所以在你的示例中没有必要调用 ToList()。这里有一个类似的问题:https://dev59.com/FFgQ5IYBdhLWcg3wdzvX#42736117 - Akos Nagy
@AkosNagy的意思是从存储库中仅公开IQueryable<T>,并让服务层执行其需要执行的操作吗? - vietvoquoc
基本上是的。你需要创建的查询通常是某些业务需求的结果(例如:筛选过期任务),所以它们应该在业务逻辑层,而不是领域无关仓储中。唯一的“缺点”是,由于从仓储中返回了 IQueryable<T>,你确实需要在业务逻辑层调用 ToList(),老实说,这会有点泄露抽象层。但是,在 .NET 中,IQueryable<T> 和延迟执行已经集成得如此完美,我认为这并不是一个非常严重的问题。 - Akos Nagy
2个回答

9
这取决于个人意见和使用情况,但就我个人而言,我不同意你提到的一些关键点。

返回 IEnumerable 而不是 IQueryable

同意。返回 IQueryable 违背了 Repository 存在的基本目的。有很多文章在网上解释了这样做会带来比解决问题更多的问题。虽然我已经学会了永远不要说“永远不”。可以参考这里这里这里。或者只需搜索 google

Repository 应该只负责 CRUD 操作

同意。对于简单的 CRUD 操作,它可能还涉及复杂的读写操作。我的经验告诉我,在特殊情况下,如果您想在 RDBMS 方面实现业务逻辑,您必须将部分业务逻辑放在 Repository 中。这既不对也不错。如果您知道自己在做什么,就不应该有问题。

Repository 方法的返回类型应该是模型(实体)

如果您没有使用 DDD,那么是的。否则,这是实现决策。对于完整的 ORM,如 EF 或 NHibernate,最好直接返回 Domain Model 而不是每个表 Entity 实例。
通常建议 Repository 应该返回 Domain Model。这样,从 RDBMS 返回的数据与 Domain Model (反之亦然)的映射成为 Repository 的职责。这避免了在 Repository 之外泄露持久性问题的必要性,从而使您的应用程序的其余部分保持独立于持久性。
但是,并非所有应用程序都实现 DDD。许多小型应用程序设计实体与其数据库设计一一映射的实体。在这种情况下,Repository 可以返回实体(相当于您的表和字段),并且映射成为调用代码的责任。或者,Repository 可以映射必要的模型并返回模型本身。强烈不建议这样做,因为上述问题。这样做,您必须放弃一些完整的 ORM 提供的特性。
所有这些都取决于您的问题、设计目标、应用程序的规模和其他已实施的设计模式等。这就是为什么它成为设计决策的原因。

只为聚合根实现 Repository

如果符合 DDD,同意。如果不是,则可以选择多个选项,例如每个表存储库。同样,这取决于用例。 关于复杂查询

不必让存储库仅实现简单的CRUD操作,它也可以返回复杂的对象图形,可以进行复杂的查询。尽管有像GetGetById 等简单方法,但它也可以使用像GetTopBrokenVehicles(vehicleType, top)这样的复杂方法。如果您为复杂查询编写单独的方法,那么完全没问题。
挑战在于如何接受必要的参数。您可以内联接受参数或构建单独的简单输入参数类。
这里是RepositoryUoW的示例代码。

3
使用仓储模式的好处之一是隐藏复杂查询,您应该将仓储视为内存中对象的集合(Martin Fowler):
仓库在领域和数据映射层之间进行中介,像一个内存中的领域对象集合。客户端对象以声明方式构造查询规范,并将其提交给仓库进行满足。对象可以从仓库中添加和删除,就像从简单的对象集合中一样,仓库封装的映射代码将在幕后执行相应的操作。从概念上讲,仓库封装了持久存储在数据存储器中的对象集合及其上执行的操作,提供了更加面向对象的持久化层视图。仓库还支持实现领域和数据映射层之间的清晰分离和单向依赖的目标。 https://martinfowler.com/eaaCatalog/repository.html

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接