在 .NET 世界中,访问数据库最常用的方法之一是使用实体框架 (EF),这是一种与语言语法紧密集成的对象关系映射器 (ORM)。使用 .NET 语言原生的语言集成查询 (LINQ),使数据访问感觉就像使用普通的 .NET 集合一样,而无需了解太多 SQL 知识。这有其优点和缺点,我将尽量不在这里咆哮。但它始终造成的问题之一是软件项目结构、抽象级别和最终单元测试的混乱。
这篇文章将尝试解释为什么存储库抽象总是有用的。请注意,许多人使用存储库作为抽象数据访问的术语,虽然也有与类似事物相关的存储库软件模式,但它不是同一件事。在这里,我将把存储库称为一系列抽象数据访问的实现细节的接口,并完全忽略设计模式。
如果您意识到这一点,请随意跳过这一点,但我必须首先解决我们如何开始想到存储库的想法。
在史前时期,代码只是按原样编写,没有结构,一切都在做你想让它做或至少希望它做的事情。没有自动化测试,只有手动黑客和测试,直到它起作用。每个应用程序都是用现有的任何东西编写的,对硬件要求的关注比代码结构、重用或可读性更重要。这就是恐龙被杀死的原因!真实的事实。
慢慢地,模式开始出现。特别是对于业务应用程序,业务代码、数据持久性和用户界面之间存在明显的分离。这些被称为层,很快被分成不同的项目,不仅因为它们涵盖了不同的关注点,而且因为构建它们所需的技能特别不同。 UI 设计与代码逻辑工作非常不同,与 SQL 或任何用于持久数据的语言或系统也非常不同。
因此,业务和数据层的交互是通过抽象成接口和模型来完成的。作为商务舱,您不会要求表中的条目列表,您将需要复杂对象的过滤列表。数据层有责任访问持久保存的内容并将其映射到业务可以理解的内容。这些抽象开始被称为存储库。
在数据访问的较低层,CRUD 等模式很快就占据了主导地位:您定义了表等结构化持久性容器,并且可以创建、读取、更新或删除记录。在代码中,这种逻辑将被抽象为集合,例如列表、字典或数组。因此,目前还有一种观点认为存储库的行为应该像集合一样,甚至可能足够通用,除了实际的创建、读取、更新和删除之外没有其他方法。
但是,我强烈不同意。作为业务数据访问的抽象,它们应该尽可能远离数据访问模式,而不是根据业务需求进行建模。这是实体框架(尤其是许多其他 ORM)的思维方式开始与存储库的原始想法发生冲突的地方,最终呼吁永远不要将存储库与 EF 一起使用,称其为反模式。
模型之间的父子关系会产生很多混乱。就像一个部门实体,里面有人员。部门存储库是否应该返回包含人员的模型?也许不是。那么,我们如何将存储库分成部门(没有人员)和人员,然后有一个单独的抽象来映射到业务模型?
当我们将业务层分成子层时,混乱实际上会增加。例如,大多数人所说的业务服务是仅将特定业务逻辑应用于特定类型的业务模型的抽象。假设您的应用程序与人一起工作,因此您有一个名为 Person 的模型。处理人员的类将是 PeopleService,它将通过 PeopleRepository 从持久层获取业务模型,但也可以执行其他操作,包括数据模型和业务模型之间的映射或仅与人员相关的特定工作,例如计算工资。然而,大多数业务逻辑使用多种类型的模型,因此服务最终成为存储库上的映射包装器,几乎没有额外的责任。
现在假设您正在使用 EF 访问数据。您必须声明一个 DbContext 类,其中包含映射到 SQL 表的实体集合。您可以使用 LINQ 来迭代、过滤和映射它们,这些数据会在后台有效地转换为 SQL 命令,并为您提供所需的内容,以及分层的父子结构。该转换还负责内部业务数据类型的映射,例如特定的枚举或奇怪的数据结构。那么为什么你甚至需要存储库,甚至可能需要服务?
我相信,虽然更多的抽象层似乎是毫无意义的开销,但它们增加了人们对项目的理解,并提高了变革的速度和质量。显然,这是一种平衡,我见过一些系统架构明显要求所有软件设计模式都在任何地方使用。抽象只有在提高代码可读性和关注点分离时才有用。
EF 变得麻烦的环境之一是单元测试。 DbContext 是一个复杂的系统,具有大量依赖项,必须花费很大的精力手动模拟。因此,微软提出了一个想法:内存数据库提供程序。因此,为了测试任何内容,您只需使用内存数据库即可完成。
请注意,在 Microsoft 页面上,此测试方法现在标记为“不推荐”。另请注意,即使在这些示例中,EF 也是由存储库抽象的。
虽然内存数据库测试有效,但它们添加了几个不容易解决的问题:
因此,最终发生的情况是,人们在“帮助程序”方法中设置数据库中的所有内容,然后创建从这种难以理解且复杂的方法开始的测试,以测试甚至最小的功能。如果没有此设置,任何包含 EF 代码的代码都将无法测试。
因此使用存储库的原因之一是将测试抽象移至 DbContext 之上。现在您根本不需要数据库,只需要一个存储库模拟。然后使用真实数据库在集成测试中测试您的存储库本身。内存数据库非常接近真实数据库,但也略有不同。
另一个原因(我承认我在现实生活中很少看到它具有实际价值)是您可能想要改变访问数据的方式。也许你想换成NoSql,或者一些内存分布式缓存系统。或者,更有可能的是,您从一个数据库结构开始,也许是一个整体数据库,现在您想将其重构为具有不同表结构的多个数据库。让我立即告诉您,如果没有存储库,这是不可能的。
特定于实体框架,您获得的实体是映射到数据库的活动记录。您对一个实体进行更改并保存对另一个实体的更改,然后您突然也会在数据库中更新第一个实体。或者也许您没有,因为您没有包含某些内容,或者上下文已更改。
EF 的支持者总是将实体跟踪宣传为一件非常积极的事情。假设您从数据库中获取一个实体,然后执行一些业务,然后更新该实体并保存它。通过存储库,您将获取数据,然后开展业务,然后再次获取数据以执行一些更新。 EF 会将其保留在内存中,知道它在更改之前没有更新,因此它永远不会读取它两次。这是真的。他们描述了数据库的内存缓存,它以某种方式感知数据库更改并跟踪您从数据库处理的所有内容,除非另有指示,否则将数据库条目双向映射到复杂的 C# 实体并来回跟踪更改,同时深度嵌入在业务代码中。就我个人而言,我认为这种过多的责任和缺乏关注点分离比使用它所获得的任何性能更具破坏性。此外,通过一些初步的努力,所有这些功能仍然可以在存储库中抽象,甚至可以在存储库的另一层内存缓存中抽象,同时保持业务、缓存和数据访问之间的清晰边界。
事实上,所有这一切的实际困难在于确定应该有单独关注点的系统之间的边界。例如,通过将过滤逻辑移至数据库中的存储过程,可以获得大量性能,但这会失去所用算法的可测试性和可读性。相反,使用 EF 或其他一些机制将所有逻辑转移到代码中,性能较差,有时甚至不可行。或者数据实体成为业务实体的点在哪里(参见上面的部门和人员示例)?
也许最好的策略是首先定义这些边界,然后决定采用哪些技术和设计。
我相信应该始终使用服务和存储库抽象,即使存储库在底层使用实体框架或其他 ORM。这一切都归结为关注点分离。我永远不会认为实体框架是一个有用的软件抽象,因为它带有如此多的包袱,因此存储库非常适合在代码中对其进行抽象。 EF 是一个有用的抽象,但用于数据库访问,而不是在软件中。
我的软件编写理念是,从应用程序需求开始,为这些需求创建组件,并使用接口抽象任何较低级别的功能。然后,您在下一个级别重复该过程,始终确保代码可读,并且不需要了解使用的组件或当前级别使用的组件。如果情况并非如此,那么您就已经很好地分离了关注点。因此,由于没有任何业务应用程序需要使用特定的数据库或 ORM,因此数据层抽象应该隐藏所有这些知识。
企业想要什么?过滤后的人员列表? var people = service.GetFilteredListOfPeople(filter);不多不少。并且服务方法只会 return mapPeople(repo.GetFilteredListOfPeople(mappedFilter));同样,不多不少。回购如何获取人员、拯救人员或执行其他任何操作不是服务关心的问题。您需要缓存,然后实现一些实现 IPeopleRepository 并依赖于 IPeopleRepository 的缓存机制。您想要映射,请实现正确的 IMapper 接口。等等。
我希望我在这篇文章中没有过于冗长。我特意保留了代码示例,因为这更多的是一个概念问题,而不是软件问题。实体框架可能是我在这里抱怨的大部分目标,但这适用于任何在小事情上神奇地帮助你,但却破坏了重要事情的系统。
希望有帮助!
以上是使用存储库甚至 ORM 进行代码中的数据访问的详细内容。更多信息请关注PHP中文网其他相关文章!