使用 LINQ 创建聚合与 nHydrate






4.17/5 (3投票s)
使用 nHydrate API 和 LINQ 进行聚合和批量操作。
引言
有许多关于使用 nHydrate 代码生成器进行领域驱动设计(DDD)的通用开发文章。本文将深入探讨聚合的专门领域,以及由此延伸的其他批量操作。该工具显然可以用来创建一个强类型、编译时检查的数据访问层(DAL);然而,DAL 也具有一些非常出色的聚合功能。
完整的开源项目可以在 http://nhydrate.codeplex.com 找到。
首先,让我们回顾一下。nHydrate 代码生成器会创建你在模型中定义的集合对象实体。这些集合是特定类型实体(由它们的名称定义)的容器。它们还拥有许多重载的静态方法,可以让你用一行代码轻松地查询、聚合、更新和删除数据。例如,下面的代码可用于选择数据库中的所有 Job 对象。
JobCollection jobCollection = JobCollection.RunSelect();
当然,这除非你想选择所有工作,否则用处有限。更实际的场景是定义过滤条件。下面的代码显示了如何使用过滤器选择一组 job 实体。
JobCollection jobCollection = JobCollection.RunSelect(x =>
x.CreatedDate > DateTime.Now.Date &&
x.Description.Contains("lollypop"));
现在,这是回顾;然而,这并不是聚合。遵循上述用法,我们可以使用各种静态方法来聚合数据。下面的代码显示了如何调用各种聚合方法。
int count = JobCollection.GetCount(x =>
x.CreatedDate > DateTime.Now.Date &&
x.Description.Contains("lollypop"));
//The min value is nullable because there maybe no
//matching records, in which case null is returned
//the return value is an INT because JobId is an INT
int? min = JobCollection.GetMin(x => x.JobId,
x => x.CreatedDate > DateTime.Now.Date &&
x.Description.Contains("lollypop"));
//Similar to above
int? max = JobCollection.GetMax(x => x.JobId,
x => x.CreatedDate > DateTime.Now.Date &&
x.Description.Contains("lollypop"));
//Get a list of all UserIDs who posted a job that meets the criteria
IEnumerable<int> idList = JobCollection.GetDistinct(x => x.OwnerId,
x => x.CreatedDate > DateTime.Now.Date &&
x.Description.Contains("lollypop"));
这些示例展示了如何从客户集合中获取聚合数据。当然,这可以扩展到所有集合。所有集合都是强类型的,并且只包含一种类型的实体。如果计算继承对象,这并不完全准确;然而,集合只会提取指定类型的实体。请注意,聚合会接受一个 Where 子句,这是 LINQ 语法。这会根据你的模型进行编译时检查。这意味着在运行时不会有意外,即运行时错误。你不需要也不应该使用魔术数字或文字字符串。你也不应该在 LINQ 中定义关系。你已经在模型中完成了这些。上述示例很简单,因为 LINQ 只为客户表定义。在实际情况中,我们的查询需求当然会跨越多个实体类型。
现在,我们将研究一个更复杂的场景,即基于依赖关系遍历来跨越表。这在概念上更复杂,但在代码上并不复杂。我们只需稍微调整一下 LINQ 语法。在示例中,我们有一个三表数据库。在数据库图像中,你可以看到数据库有一个用户表,每个用户可以发布多个工作。每个工作可以有多个工作申请。最后,每个用户可以有多个工作申请。这是一个实际场景,你拥有用户,有些是发布者,例如招聘人员,有些是申请工作的,例如候选人。
聚合此表布局所需的 C# 代码并不困难。第一个查询返回用户 ID 的唯一列表,其中用户至少发布了一个工作,并且其中一个发布的工作在昨天收到了申请。第二个查询仅返回符合条件的用户的计数。请注意,这两个查询是针对两个不同的集合对象运行的。第一个实际上可以针对多个对象类型运行,因为 LINQ 查询定义了要聚合的字段。你可以从与包含聚合字段的表实体有任何关系的任何集合对象定义该字段。这可以通过强类型 LINQ 语法来实现,因为所有关系都在模型中定义。第二个查询只能从 UserCollection
对象运行,因为我们正在计算用户。请注意,我们从不在 LINQ 代码中进行 JOIN 或定义遍历条件。模型是王道,领域设计者在生成之前就已经定义了实体交互。
DateTime startDate = DateTime.Now.Date.AddDays(-1);
DateTime endDate = startDate.AddDays(1);
//Get distinct user ID list for people who posted jobs
//and there was a job application for 1 or more of
//these jobs yesterday. The LINQ knows that Job
//relates to JobApplication so the syntax
//shows a JobApplication object has
//1 related Job entity object
IEnumerable idList =
JobApplicationCollection.GetDistinct(
x => x.JobEntity.OwnerId,
x => startDate <= x.CreatedDate &&
x.CreatedDate < endDate);
//Same criteria as above but just get the number of the
//users with 1 or more jobs that had applications
//yesterday. Notice that we are counting users so
//we must use that collection
int count = UserAccountCollection.GetCount(x =>
startDate <= x.JobApplicationEntity.CreatedDate &&
x.JobApplicationEntity.CreatedDate < endDate);
更新
另一个重要的分组功能是批量更新。你可能需要更新一组用户的状态,或者更改一组工作帖子的到期日期。在这两种情况下,你需要指定要更新的字段、选择记录的过滤器以及指定字段的新值。nHydrate 创建和维护的生成的 DAL API 允许你以强类型的方式非常轻松地做到这一点。下面的代码片段更新了 job 表的 expiration date 字段到 30 天后,前提是昨天有任何工作申请。请注意,过滤器很复杂,并且跨越多个表。
DateTime startDate = DateTime.Now.Date.AddDays(-1);
DateTime endDate = startDate.AddDays(1);
//Update the expiration date of all jobs to 30 days out
//if they had at least one application yesterday
int count = JobCollection.UpdateData(x => x.ExpirationDate,
x => startDate <= x.JobApplicationEntity.CreatedDate &&
x.JobApplicationEntity.CreatedDate < endDate,
DateTime.Now.AddDays(30));
你会注意到,这个查询基于定义的模型关系连接了 job 和 job application 表。无需在 LINQ 过滤器中指定连接条件。还要注意,指定的字段值和新值是同一类型。由于到期日期是日期,因此新值也只能是该类型。新数据不是作为对象传递的。如果数据类型不匹配,代码将无法编译。对于可空对象也是如此。如果到期日期在数据库中可以接受 null,那么代码中的数据类型就是可空日期。受影响的行数是从 UpdateData
方法返回的。
删除
最后的批量操作是删除。有时我们需要根据复杂的过滤器删除一组记录。将所有这些数据从数据库加载仅仅是为了标记它们以供删除,这可能不合理,当然也不高效。同样,集合的静态方法为我们提供了强大的功能和效率。下面的代码将删除所有 job application 记录,其中相关的 JobID 小于 100,申请的用户住在 Springfield(任何州),并且发布的职位在“IL”州。
//Remove all job applications if the related JobID is less then 100
//and the user that applied lives in Springfield
//and the posted job is in the state IL
int count = JobApplicationCollection.DeleteData(x =>
x.JobId < 100 &&
x.UserAccountEntity.City == "Springfield" &&
x.JobEntity.State == "IL");
这是一个跨越三个不同表的复杂过滤器。生成的 SQL 无疑很复杂,但你的 C# 代码优雅而简单。即使是非开发人员也能几乎像阅读自然语言一样阅读它。
摘要
正如你所见,nHydrate 提供的先进聚合功能非常强大。最棒的是,这是开箱即用的功能。无需特殊设置即可获得。当你定义模型时,所有这些功能都源于你定义的实体和关系。我无法过多强调编译时检查语法所带来的卓越优势。你可以将其视为编译器的扩展。一旦你定义了 nHydrate 模型,它就会根据各种规则进行验证。当你生成代码时,它会由编译器进行检查。当你编写包含所有复杂性以及用户定义的字段和值的自定义代码时,它也会由编译器进行检查,因为所有与数据库的交互都是通过 LINQ 语法和生成的 Select 命令完成的。没有自由格式的 SQL 或弱类型。这大大降低了应用程序出现运行时错误的几率。