我应该将 DDD 聚合根存储库与 EF 4.1 一起使用吗? LINQ?
我读过 DDD Evans,并且尝试使用 C# 和 Entity Framework 4.1 + LINQ 进行聚合根存储库设计。
但是,我担心发送到数据库的实际查询。我正在使用 SQL 2008 R2,并运行 SQL Profiler 来检查数据库响应 LINQ 代码所做的操作。
考虑使用 Person 和 EmailAddress 的简单 2 实体设计。一个人可以有零到多个电子邮件地址,并且一个电子邮件地址必须恰好有一个人。 Person 是聚合根,因此不应该有电子邮件地址的存储库。电子邮件地址应从人员存储库中选择(根据 DDD Evans)。
为了进行比较,我确实为电子邮件地址设置了一个临时存储库。以下代码行:
var emailString = "[email protected]";
var emailEntity = _tempEmailRepository.All.SingleOrDefault(e =>
e.Value.Equals(emailString, StringComparison.OrdinalIgnoreCase));
... 根据探查器执行一个漂亮的干净 SQL 查询:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[PersonId] AS [PersonId],
[Extent1].[Value] AS [Value],
[Extent1].[IsDefault] AS [IsDefault],
[Extent1].[IsConfirmed] AS [IsConfirmed],
FROM [dbo].[EmailAddress] AS [Extent1]
我可以使用以下代码从人员存储库中选择电子邮件:
var emailEntity = _personRepository.All.SelectMany(p => p.Emails)
.SingleOrDefault(e => e.Value.Equals(emailString,
StringComparison.OrdinalIgnoreCase))
这在运行时使我获得相同的实体,但显示不同的命令在 SQL Profiler 中:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
FROM [dbo].[Person] AS [Extent1]
除了上面从 Person 中选择的查询之外,还有许多“RPC:Completed”事件,数据库中的每个 EmailAddress 行都有一个事件:
exec sp_executesql N'SELECT
[Extent1].[Id] AS [Id],
[Extent1].[PersonId] AS [PersonId],
[Extent1].[Value] AS [Value],
[Extent1].[IsDefault] AS [IsDefault],
[Extent1].[IsConfirmed] AS [IsConfirmed],
FROM [dbo].[EmailAddress] AS [Extent1]
WHERE [Extent1].[PersonId] =
@EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1
exec sp_executesql N'SELECT
[Extent1].[Id] AS [Id],
[Extent1].[PersonId] AS [PersonId],
[Extent1].[Value] AS [Value],
[Extent1].[IsDefault] AS [IsDefault],
[Extent1].[IsConfirmed] AS [IsConfirmed],
FROM [dbo].[EmailAddress] AS [Extent1]
WHERE [Extent1].[PersonId] =
@EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=2
我的测试数据库在 dbo.EmailAddress 中有 14 行,并且有14 个不同的 RPC:Completed 调用,每个调用都有不同的 @EntityKeyValue1 值。
我假设这对 SQL 性能不利,因为随着 dbo.EmailAddress 表获取更多行,将在数据库上调用更多这些 RPC。是否有另一种更好的方法将 DDD 聚合根存储库与 EF 4.1 + LINQ 结合使用?
更新:已解决
问题在于 All 属性返回 IEnumerable
。将其更改为 IQueryable
后,LINQ 启动并一次性选择整个人员 + 电子邮件。但是,在从 All 返回 IQueryable 之前,我必须链接 .Include(p => p.Emails) 。
I've read DDD Evans, and I' experimenting with an aggregate root repository design using C# and Entity Framework 4.1 + LINQ.
However, I'm concerned about the actual queries that are being sent to the DB. I'm using SQL 2008 R2, and running SQL Profiler to examine what the DB is doing in response to the LINQ code.
Consider a simple 2 entity design with Person and EmailAddress. One Person can have zero to many EmailAddresses, and an EmailAddress must have exactly one Person. Person is the aggregate root, so there should not be a repository for email addresses. Email addresses should be selected out of the Person repository (according to DDD Evans).
For comparison, I do have a temporary repository set up for email addresses. The following line of code:
var emailString = "[email protected]";
var emailEntity = _tempEmailRepository.All.SingleOrDefault(e =>
e.Value.Equals(emailString, StringComparison.OrdinalIgnoreCase));
... executes a nice clean SQL query according to the profiler:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[PersonId] AS [PersonId],
[Extent1].[Value] AS [Value],
[Extent1].[IsDefault] AS [IsDefault],
[Extent1].[IsConfirmed] AS [IsConfirmed],
FROM [dbo].[EmailAddress] AS [Extent1]
I can select the email out of the person repository, with the following code:
var emailEntity = _personRepository.All.SelectMany(p => p.Emails)
.SingleOrDefault(e => e.Value.Equals(emailString,
StringComparison.OrdinalIgnoreCase))
This gets me the same entity at runtime, but with different commands showing up in the SQL Profiler:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
FROM [dbo].[Person] AS [Extent1]
In addition to the above query that selects from Person, there are a number of "RPC:Completed" events, one for each EmailAddress row in the DB:
exec sp_executesql N'SELECT
[Extent1].[Id] AS [Id],
[Extent1].[PersonId] AS [PersonId],
[Extent1].[Value] AS [Value],
[Extent1].[IsDefault] AS [IsDefault],
[Extent1].[IsConfirmed] AS [IsConfirmed],
FROM [dbo].[EmailAddress] AS [Extent1]
WHERE [Extent1].[PersonId] =
@EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1
exec sp_executesql N'SELECT
[Extent1].[Id] AS [Id],
[Extent1].[PersonId] AS [PersonId],
[Extent1].[Value] AS [Value],
[Extent1].[IsDefault] AS [IsDefault],
[Extent1].[IsConfirmed] AS [IsConfirmed],
FROM [dbo].[EmailAddress] AS [Extent1]
WHERE [Extent1].[PersonId] =
@EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=2
My test db has 14 rows in dbo.EmailAddress, and there are 14 different RPC:Completed calls, each with a different @EntityKeyValue1 value.
I'm assuming this is bad for SQL performance, since as the dbo.EmailAddress table gets more rows, more of these RPC's will be invoked on the db. Is there another better approach to using DDD aggregate root repositories with EF 4.1 + LINQ?
Update: Solved
The problem was that the All property was returning an IEnumerable<TEntity>
. After this was changed to IQueryable<TEntity>
, LINQ kicked in and selected the whole Person + Emails in one shot. However, I had to chain in .Include(p => p.Emails) before returning the IQueryable from All.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
鉴于现代 ORM 已经为您提供的抽象级别,我个人建议不要在您和数据库之间添加额外的抽象层。除了重新发明轮子之外,您还会发现直接在服务层中使用所选的 ORM 可以让您对查询、获取和缓存策略进行更细粒度的控制。
Ayende 的系列工资of Sin 是反对在现代 ORM 中使用规范/存储库的各种其他论据的一个很好的资源,特别是考虑到 LINQ 实际上已经为您提供了几乎所有您可能需要的东西。
我在过去的一个项目中走了“DDD”路线(用引号引起来,因为它与我当时对 DDD 的理解有关)。事后看来,我意识到在公开辩论中 DDD 常常被简化为应用这些模式,这是一种耻辱。我已经陷入了这个陷阱,我希望我可以帮助其他人避免它。
存储库和规范是基础设施模式。 基础设施的存在是为了服务于某个目的,而不是其本身就是一个目的。当谈到基础设施时,我主张严格应用重用抽象原则。为了进行快速总结,RAP 表示,当且仅当它将被 2 个以上的消费者使用并且该附加抽象层实际上实现了某些行为时,您才应该引入一个抽象。如果您只是引入一个抽象来将您与某些东西(例如 ORM)解耦,请务必小心,很可能您最终会得到一个有漏洞的抽象。
DDD 的全部要点是使域模型与基础设施分开,并使域模型尽可能具有表现力。没有证据表明如果不使用存储库就无法实现这一点。存储库仅用于隐藏数据访问的详细信息,这是 ORM 已经做的事情。 (顺便说一句,考虑到 DDD 书的年代,我认为 ORM 的常见用法并不在当时的情况中)。现在,存储库可能有助于强制执行聚合根等。但是,我认为应该通过明确区分“读”操作(查询)和“写”操作(命令)来处理这一点。仅对于后者,域模型才应该是相关的,定制的(并且更灵活的)模型(例如 DTO 或匿名对象)通常可以更好地服务于查询。
规格的情况类似。规范的预期目的是相似的。它们的力量在于构建用于查询对象的领域特定语言的元素。随着 LINQ 的出现,通用规范模式提供的用于组合这些元素的许多“粘合剂”已经过时。提示:看看 Predicate Builder(<50 行 C#),它可能是您实现规范所需的全部内容。
总结一下这篇冗长的(希望不是太杂乱,我希望稍后会重温)帖子:
Given the level of abstraction modern ORMs already give you, I personally would advice against adding an additional layer of abstraction between you and your database. Besides reinventing the wheel, you will find using the chosen ORM in your service layer directly will give you finer grained control over the queries, fetching and caching strategies.
Ayende's series Wages of Sin is a good resource for various other arguments against using Specifications/Repositories with a modern ORM, especially considering that LINQ effectively already gives you almost everything you are likely to need.
I have gone the route of "DDD" on a past project (in quotes because it's bound to the understanding of DDD I had at the time). In hindsight, I realize that it's a shame that in public debate DDD is often reduced to applying these patterns. I've fallen into that trap, and I hope I can help others avoid it.
Repository and Specification are Infrastructure pattern. Infrastructure is there to serve a purpose, not to be a purpose on it's own. When it comes to Infrastructure, I advocate applying the Reused Abstraction Principle rigorously. To give a quick summary, the RAP says you should introduce an abstraction if, and only if it's going to be consumed by more than 2 consumers and that additional layer of abstraction actually implements some behavior. If you only introduce an abstraction to decouple you from something (such as an ORM) be very careful, it is likely you will end up with a leaky abstraction.
The whole point of DDD is to keep your Domain Model separate from your Infrastructure and make your Domain Model as expressive as possible. There's no evidence this can't be achieved without using Repositories. Repositories are only there to hide the details of data access, something an ORM does already. (On a side note, considering the age of the DDD book I don't think the common use of ORMs was in the picture back then). Now, Repositories may be useful to enforce Aggregate roots etc. However, I think this should be treated by making a clear distinction between "read" operations (Queries) and "write" operations (Commands). It's only for the latter that the domain model should be relevant, queries are often better served by tailored (and more flexible) models (such as DTOs or annonymous objects).
The case for Specifications is similar. The intended purpose of Specifications is similar. Their power lies in building the elements of a domain specific language for querying objects. Much of the "glue" that generalized Specification patterns provided to combine those elements has become obsolete with the advent of LINQ. Hint: Take a look at Predicate Builder (<50 Lines of C#), it is likely all you are going to need to implement specifications.
To summarize this lengthy (and hopefully not too disorganized, I will revisit later on I hope) post: