nHydrate ADO.NET 生成器






4.67/5 (3投票s)
如何使用 nHydrate ADO.NET 生成器并使数据库保持同步。
引言
CodeProject 上有很多关于 nHydrate 代码生成平台的文章,但它们更多地侧重于它的功能和工作原理。本文将深入探讨如何使用一个特定的生成器:ADO.NET 数据访问层。不要被本文的篇幅所迷惑。它的语法和用法确实非常简单。我只是概述了很多你可以做的事情。这是一个非常有用的生成模板。最近,微软推出了新的生成技术 Entity Framework,这引起了很多关注。EF 是一个不错的平台,但大部分生成都是内置于平台中的,这当然是重点,但我认为这有时会让自定义变得麻烦(例如创建只读实体)。T4 模板在这种情况下可以派上用场,但它仍然没有将模型提升到解决方案层面,而这正是我希望看到的。要了解更多关于如何下载和安装生成器的信息,请参阅这篇文章。
我喜欢使用领域驱动设计(DDD),也称为模型驱动开发。这包括在模型中定义平台的所有(或几乎所有)信息。然后使用模型在你的 VS.NET 解决方案中创建由该模型定义的众多项目。从数据库(和数据库更改)到 API,再到控制反转库,甚至用户界面,都可以由模型定义。这是模型驱动开发的真正目标,尽管实现得并不完美。
ADO.NET 生成器可以实现一种简单而有效的策略。一旦定义了模型,就可以使用它来创建 DAL。这是一个使用多年的非常成熟的生成模板。它支持 LINQ 并基于存储过程。它使用工厂模式来创建和操作对象。API 支持延迟加载,因此你可以遍历关系,也可以选择使用预加载。语法很简单,并且用法的所有方面都经过编译器检查。你的代码中没有“魔法”字符串或数字。如果你进行模型中的破坏性更改,例如重命名或删除字段,你只需要重新生成并编译,你的破坏性更改就会被编译器发现。几乎没有什么需要担心的。如果你更改关系或实体交互方式,这会导致相关类、方法或字段更新,使你的现有代码无法编译。破坏性更改会被编译器捕获!这回到了模型驱动开发的企业理念,你有编译器,所以要使用它。如果你的代码中有魔法字符串,说明有问题。如果你的代码中有魔法数字,说明有问题。如果你进行了破坏性的数据库更改而代码仍然编译,说明有问题。有更好的方法。
我假设你在阅读本文之前已经有了一些 nHydrate 的经验,如果没有,本网站还有其他文章解释了创建模型的基础知识。本质上,模型是组织数据的设计。nHydrate 模型可以看作是带有元数据的数据模型。许多人将他们的数据库作为模型。这适用于一些简单的情况。然而,除非你手动编码,否则你无法使用这种方法创建多个层。nHydrate 试图消除许多繁琐的代码编写工作。
让我们假设我们有一个模型,并且添加了几个表。表将是 Customer、Order、OrderDetails 和 Product。关系将是直接的,Customer 将有多个 Order,Order 将有多个 OrderDetails,每个 OrderDetail 将有一个指定的 Product。现在,在生成 API 和数据库项目之后,我们可以开始使用我们新的 API 编写代码了。
插入对象
首先,让我们添加一个客户。我们使用工厂模式来完成此操作,因此我们需要一个集合来创建一个新对象,然后我们可以填充该对象的属性,然后将其添加到集合中。
//Insert a customer
var customerCollection = new CustomerCollection();
var customer = customerCollection.NewItem();
customer.FirstName = "John";
customer.LastName = "Doe";
customerCollection.AddItem(customer);
customerCollection.Persist();
在上面的代码片段中,创建了一个新的 Customer
集合,并从中请求了一个新的 Customer
对象。此时,新对象不属于任何集合。NewItem
方法是静态的,它返回一个独立的免费对象,你可以选择将其添加回集合,也可以选择忽略它而不使用它。我已经设置了必需的属性,然后将其添加回集合。此时,所有操作都是在内存中执行的。只有当我调用集合对象的 Persist
方法时,才会发生数据库调用。你可以将任意数量的项目添加到集合中,这有效地将对象排队等待稍后保存,然后在一个数据库事务中保存所有对象。你实际上可以保存整个对象图,并按任意顺序添加对象。框架内置的依赖关系引擎将根据依赖关系确定哪些对象首先被插入。你无需手动跟踪此操作。现在,更高级的用法也并不困难。假设我们希望将所有类型的对象保存在同一个容器和同一个数据库事务中,我们可以使用以下语法添加一个客户和一个相关的订单。
//Insert a customer
var customerCollection = new CustomerCollection();
var customer = customerCollection.NewItem();
customer.FirstName = "John";
customer.LastName = "Doe";
customerCollection.AddItem(customer);
//Get a reference to the order collection in the same subdoamin and add an Order
var orderCollection =
customerCollection.SubDomain.GetCollection<OrderCollection>();
var order = orderCollection.NewItem();
order.CustomerItem = customer;
order.TotalCost = 100;
orderCollection.AddItem(order);
customerCollection.Persist();
请注意,顶部的代码是相同的。这段代码创建了一个 Customer
,然后添加了一个相关的 Order。请注意,OrderCollection
不是新创建的。它是从客户集合的容器中检索出来的,因此它将位于同一个名为 Subdomain
的容器内。Subdomain
是对象图的内存容器。调用 Persist
方法时,整个对象图将在一次数据库事务中保存。当然,你可以创建任意多个新集合,但它们将独立访问数据库。使用我们上面定义的这种方法,新的 Order
可以访问新的 Customer
。此时,这两个对象都不在数据库中。另外请注意,两个对象的主键都是数据库生成的或标识符。所以我们此时不知道它们是什么。由于我们将 Order
对象与 Customer
对象关联起来了,所以这无关紧紧要。让框架去做工作。这些对象将在同一个数据库事务中保存,所有对象将一起保存或失败。不会出现部分保存。
保存操作返回后,对象将具有数据库中存在的真实主键(以及所有其他数据)。因此,假设这是第一个保存的对象,那么客户将具有 ID 1,订单也将具有 ID 1,因为它们都是标识符。重要的是 Order
对象将有一个外键客户 ID,该 ID 指向正确的 Customer
对象。你可以遵循此模式来添加 OrderDetails 等。
选择数据
现在让我们检索一些现有数据。生成了用于选择所有查询的存储过程。所有存储过程都是基于模型生成的,并且是数据库安装程序项目的一部分。所有存储过程和动态 SQL 都将参数化发送到数据库引擎,因此无需担心 SQL 注入攻击。还为所有按主键和外键选择的查询生成了存储过程。所有更新和删除操作也将通过存储过程进行。所有 LINQ 查询由于其动态性,必然会生成动态 SQL 查询;但是,它们都是经过编译时检查的。
首先要理解的是,静态的 RunSelect
方法用于从任何集合中选择数据。以下代码片段显示了如何获取所有对象以及按主键获取单个客户。后台运行了一个存储过程,没有使用动态 SQL。
//Select all customers
var customerCollection = CustomerCollection.RunSelect();
//Select a single customer by primary key
var customer = CustomerCollection.SelectUsingPK(1);
这是一个非常简单的示例。很可能你需要根据非常动态的情况选择数据。下面的代码片段根据 lambda 语句选择数据。
//Select all customers where name is John
var customerCollection = CustomerCollection.RunSelect(x => x.FirstName == "John");
同样,这是一个相当简单的场景。很多时候你想根据次级表中的数据或条件来提取数据。在 SQL 中,你可以使用内部连接来实现这一点,而在其他 ORM 工具中,你需要了解数据结构才能发出连接条件。使用这个 DAL 模板,你不需要了解太多关于模型的信息。Intellisense 提供了你编写代码所需的内容。在下面的示例中,我将根据相关表中的数据选择客户;但是,请注意,我没有指定任何连接信息。开发人员不知道连接基于哪个确切的字段。没有必要知道这一点。模型包含所有这些信息,代码也基于模型。开发人员无需在代码中再次指定这些信息。
//Select all customers that have an order where the total cost is more than $100
var customerCollection = CustomerCollection.RunSelect(x => x.Order.TotalCost > 100);
预定义的 lambda 语法提供了一种以编译时检查的方式遍历关系的方法,而无需指定如何实际提取数据的任何元数据信息。请记住,如果我在模型中删除了 Customer
和 Order
之间的关系,这段代码将无法编译。编译器知道我的模型的关系!
分页
数据查询的另一个重要方面是分页。这是一个在许多数据访问层中并不直接的任务。使用这个模板,它变得非常容易。你只需创建并使用自定义分页对象。每个实体类型都有自己的分页对象,并且是强类型化的。在下面的代码片段中,我创建了一个客户分页对象来获取第 2 页,每页 10 条记录。然后,我像以前一样使用 lambda 调用静态的 RunSelect
方法,只是这次我传入了分页对象。这就是分页结果集所需的一切。方法调用后,分页对象有一个 RecordCount
属性,其中包含非分页记录的总数。你可以使用此属性来填充你的分页 UI。
//Setup a paging object and run a query
CustomerPaging paging =
new CustomerPaging(2, 10, Customer.FieldNameConstants.CustomerId, true);
var list = CustomerCollection.RunSelect(x => x.LastName == "Jones", paging);
System.Diagnostics.Debug.WriteLine("Total Records: " + paging.RecordCount);
依赖关系遍历
检索到对象后,你自然会想要与其相关的信息。在我们使用的模型中,很自然地会有一个客户,然后遍历他的订单,然后遍历每个订单的明细。我们甚至可能想查看与每个订单明细相关的产品信息。再次请注意,没有指定连接信息。此外,所有 1:M 关系都有一个项目列表表示,而向上遍历到父项是通过单个项目完成的。
//Select a single Customer by primary key
var customer = CustomerCollection.SelectUsingPK(1);
//Walk each order for this Customer
foreach (Order order in customer.OrderList)
{
//Write out the total cost of the Order
System.Diagnostics.Debug.WriteLine(order.TotalCost);
foreach (OrderDetail orderDetail in order.OrderDetailList)
{
//Write out the OrderDetail's product anme
System.Diagnostics.Debug.WriteLine(orderDetail.ProductItem.Name);
}
}
批量操作
你还可以执行批量操作。这包括在单个数据库事务中发出数据库语句。整个操作将作为一个整体失败或成功,不会有部分保存。删除和更新也可以这样执行。语法仍然是一个简单的 lambda 语句。使用 lambda 语句意味着编译器将检查语句的语法,并且不必担心因为拼写错误的字段或实体而导致运行时错误。第一个 lambda 非常简单,但当然,它可以根据你的需要变得复杂。你甚至可以遍历到其他表的关系,根据复杂的连接删除数据。
//Delete all customers with the first name John
CustomerCollection.DeleteData(x => x.FirstName == "John");
//Delete all customers with the first name John and
//with 1 or more Orders of less than $100
CustomerCollection.DeleteData(x =>
x.FirstName == "John" &&
x.Order.TotalCost < 100);
要更新数据,也非常相似。只需发出一个 lambda 和你想更新的字段。目前这只适用于单个字段更新。但是,所有数据类型都是强类型的。在这个例子中,我们正在设置一个字符串属性,所以第三个参数是字符串。如果我们更新一个整数类型的字段,设置字段也将是一个整数。换句话说,编译器再次检查我们的代码以消除运行时错误。
//Update all customers with first name John to Dave
CustomerCollection.UpdateData(x => x.FirstName, x => x.FirstName == "John", "Dave");
对象事件
每个对象都有内置的事件,允许你构建回调机制。每个对象上的每个属性都有一个 PropertyChanging
和 PropertyChanged
事件。这些实际上基于标准的 .NET 接口 INotifyPropertyChanged
和 INotifyPropertyChanging
。你可以将它们绑定到支持这些接口的任何组件,或者自己挂接事件并执行某些操作。在下面的代码片段中,从存储中检索了一个 Customer
对象,并捕获了它的一个事件。在事件处理程序中,你可以检查哪个属性正在更改并执行某些操作。请注意,我没有将字符串字面量进行比较来查找属性名。我使用了生成的枚举来表示 Customer
对象上的字段。此枚举用于各种功能点,例如从生成的 Customer
对象中检索字段长度等元数据。
//Select a single Customer by primary key
var customer = CustomerCollection.SelectUsingPK(1);
customer.PropertyChanging +=
new PropertyChangingEventHandler(customer_PropertyChanging);
void customer_PropertyChanging(object sender, PropertyChangingEventArgs e)
{
if (e.PropertyName == Customer.FieldNameConstants.FirstName.ToString())
{
//Perform some UI action
}
}
所有属性也有特定的更改事件。在模型上,你可以将 EnableCustomChangeEvents
设置为 true
来查看此功能。为每个对象的每个属性生成一个更改事件和一个正在更改的事件。现在你可以编写更具体的代码来跟踪更改。在下面的代码片段中,我选择了一个 Customer
对象并处理了它的 NameChanging
事件。每次设置 Name
属性时,都会触发此事件处理程序。我检查要设置的新值,如果它是“Dave”,我将取消设置。当我取消设置器时,不会引发错误,但新值将被忽略,并且属性将保留先前的值。如果对象被标记为未更改(而不是为保存而标记为脏),它将保持不变。
//Select a single Customer by primary key
var customer = CustomerCollection.SelectUsingPK(1);
customer.FirstNameChanging +=
new EventHandler<BusinessObjectCancelEventArgs<string>>(FirstNameChanging);
void FirstNameChanging(object sender, BusinessObjectCancelEventArgs<string> e)
{
if (e.NewValue == "Dave")
e.Cancel = true;
}
部分类
所有生成的项都创建为部分类。有一个“生成一次”的类,你可以修改它,因为它永远不会被覆盖。还有一个“始终生成”的类,它总是会被覆盖。这非常类似于 VS.NET 处理 Windows 窗体类的方式。有一个设计器,每次在可视化设计器中进行更改时都会被覆盖,还有一个代码类,你可以在其中添加窗体的自定义代码。
Customer
对象有一个 FirstName
和 LastName
属性。你的 UI 需要显示全名,所以请在“生成一次”文件中创建一个自定义属性。无需在 UI 代码中连接字符串。只需扩展你生成的 Customer
类。该类的存根已生成。我只是添加了一个名为 FullName
的只读属性。Intellisense 将其视为另一个属性。你可以在所有其他属性被使用的地方使用它。除了它位于“生成一次”文件中之外,无法区分它是手动编写的还是生成的。
partial class Customer
{
public string FullName
{
get { return this.FirstName + " " + this.LastName; }
}
}
代码外观
很多时候,你希望你的代码看起来与你的数据库不同。许多人不得不处理带有表前缀和丑陋字段名的遗留数据库。例如,数据库中的表结构可能是 TBLCustomer、TBLOrder 等。这会导致非常丑陋的 C# 代码。字段名也存在类似的问题。使用表和字段的代码外观,你可以在不更改数据库结构的情况下获得漂亮的代码。在下图你看到一个 nHydrate 模型。有一个 Customer 实体,其中有一个名为“SomeDatabaseField”的字段。这是数据库中的实际名称。然而,请注意 codefacade
属性被设置为“MyAlias”。这将在代码中显示。因此,在代码中,你不知道它映射到哪个物理数据库字段。
//Select a single Customer by primary key
var customer = CustomerCollection.SelectUsingPK(1);
customer.MyAlias = "MyValue";
customer.Persist();
关系
不仅支持父子、一对多关系,还支持一对一和多对多关系。如果你在两个实体之间定义了一个关系,并且链接字段在各自的表中都标记为唯一,那么该关系就被定义为一对一关系。生成的代码将反映这一点,即父实体将有一个子项,而不是子列表,并且每个子项都将有一个指向父项的引用。这两个关系遍历都是单数的,而不是复数的。
也支持多对多关系。定义两个表,如 Product 和 Feature,并假设一个产品可以有许多功能,一个功能可以与许多产品关联。要定义这种关系,我们创建一个名为 ProductFeature 的中间表,并添加 Product 和 Feature 表的主键。之后,将此中间表在模型中标记为关联表。就这样。数据库脚本将生成以创建所有三个表;但是,你永远不会在代码中看到中间表。在代码中,每个 Product
项将有一个 FeatureList
,并且每个 Feature
项将有一个 ProductList
。没有其他特殊的映射或需要编写的处理器。
查询计划
生成的代码架构与 SQL 缓存配合良好。生成的代码在后台使用两种不同的查询方法。第一种方法是对生成存储过程的封装。这种方法产生极快的結果,因为查询计划在 SQL Server 中进行了缓存。所有(2-N)对存储过程的调用执行速度都尽可能快。由于 SQL Server 的优化和缓存,使用生成的方法将获得最大的性能。
第二种方法是参数化查询。每当对 API 编写 LINQ 语句时,都会使用此方法。在后台会生成参数化的 SQL 语句。这具有与存储过程相同的优点。如果你再次运行相同的 LINQ 语句,查询计划将被 SQL Server 缓存。需要注意的是,由于 LINQ 的形式更自由,你可以创建各种各样的查询。这种属性在实际应用中比理论上执行得更好,因为应用程序通常不会发出成千上万个不同的查询,而是使用相同的查询和不同的参数。
事务和并发
所有选择和更新都是原子的,并且在 SQL Server 事务中完成。当你加载一个对象、一组对象或多个集合时,这些项将存在于子域中。这是一个包含所有相关信息的容器。你可以加载任意数量的子域。它们不会相互干扰,彼此之间也没有任何了解。每个对象都存在于一个强类型的父集合对象中。所有集合对象都存在于子域容器中。这是隐含的。即使你只加载一个对象,它也已经有一个父集合和一个父子域。
当对象被持久化时,这一点就显现出来了。调用集合的 Persist
方法时,它的整个子域将在一个 SQL 事务中持久化。
摘要
当然,API 中还有更多功能。这是你可以轻松完成的一些工作的良好样本。这里涵盖了创建、更新和删除数据以及选择数据的基本知识,你可以看到执行任何操作都非常容易。所有对象都是强类型的,并且是生成的。你可以定义自定义视图和存储过程对象,以映射到数据库中的相应对象。使用 nHydrate 平台还有另一个好处,就是你可以生成多个 API 并访问同一个数据库。使用不同的模板,你可以创建 ADO.NET、Entity Framework、nHibernate、Code First 或任何其他 API,并从单个模型生成所有代码。所有 API 程序集必然会与数据库保持同步。
当此 API 与数据库安装程序一起生成时,你的数据库将始终保持同步并与你的 API 版本化。这可以节省数不清的编写 SQL 升级脚本和知道何时运行它们的时间。由于 API 是与数据库版本化的,因此 API 中永远不会有因数据库中缺少某些内容而导致的错误。