NHibernateDataSource:ASP.NET 2.0 的 DataSourceControl






4.88/5 (31投票s)
一个用于查询和绑定 NHibernate 实体以及其他有用功能的 DataSourceControl。
背景
我们已经使用 NHibernate 两年,使用 Hibernate 四年。所有这些都是使用 JSP / JSF 或 ASP.NET 的 Web 应用程序。在 Java 中,我们编写了一个拥有 300 个表、大量复杂关系、延迟加载和缓存的应用程序。它的性能比我们之前编写的任何东西都要好。令人惊讶的是,除了两个用于繁重 ETL 工作存储过程调用之外,不需要任何 SQL / JDBC。我们的 ASP.NET 应用程序规模从小到中等不等,此时我认为 NHibernate 几乎适用于任何规模的 Web 应用程序的持久化解决方案。
随着 .NET 2.0 的出现,我们对 ASP.NET 2.0 中新的 DataBoundControls
和双向绑定感到非常兴奋。然而,我们很快就厌倦了 ObjectDataSource
,并且也遇到了 2-way databinding 的一些令人恼火的限制。有一段时间,我们勉强使用 ODS,并通过 Mike 在 vaultofthoughts.net 上的 MyObjectDataSource
来解决一些问题。然而,我们最终意识到我们可以为 NHibernate 编写一个 DataSourceControl
,从而绕过我们为使用 ObjectDataSource
而编写的所有额外代码。当我们发现 Paul Wilson 为他的 ORM 做了同样的事情时,我们就开始编写 NHibernateDataSource
。
NHibernate 的优点
我不会过多详细介绍对象关系映射器 (ORM) 的优点,但我们看到 NHibernate 的主要优点是
- 抽象 SQL 实现。对于我们的大部分应用程序,我们不需要编写任何 SQL。模式更改变得轻而易举,我们的错误数量大大减少,因为糟糕的 SQL 和 ADO / JDBC 代码历史上是许多错误的根源。
- 级联选择和更新。大大简化了跨多个表的数据加载和保存。当然,我们仍然会偶尔使用存储过程来执行大型批量更新和其他繁重的工作。
- 性能提升。智能外连接 + 延迟加载 + 缓存 = 出色的性能。
本文的范围
本文假设读者熟悉 NHibernate。
如果您不熟悉 NHibernate,它可能仍然有用,因为它提供了
- NHibernate 的基本特性和优点的几个演示
- 一个用于将
IList
转换为DataTable
的有用类 DataSourceControl
控件的一个不错的示例
但是,如果您需要 NHibernate 教程,请另寻他处。
关于 UI 与 ORM 层解耦
文章的标题应该已经告诉您 UI 会意识到 NHibernate 是 ORM。如果您认为每个应用程序都必须将 UI 与任何数据访问实现细节(包括使用的 ORM)隔离,那么您将不喜欢这种方法。如果您想要数据层和 Web 层之间的严格分离,您可能想看看 Billy McAfferty 的文章。他创建了一个 GenericDAO
,可以通过 IGenericDAO
接口来创建数据访问层,使 UI 对幕后的实际 NHibernate 实现保持独立。您很可能可以相对轻松地将 ObjectDataSource
连接到它。
如果您不确定,请继续阅读。在文章结尾,我将总结我们决定耦合的理由。
概述
基础结构
这里展示的是我们小组开发并作为我们所有 Web 应用程序基础的框架的 NHibernate 组件。NHibernateDataSource
可以很容易地从我们编写的基础架构和实用程序类中解耦,但为了节省时间和实用性,我将包含其他几个有用的类。
NHibernateUtils
提供了一种静态加载和配置 NHibernate Session 工厂的方法,并支持从属性动态生成映射配置。SessionHolder
提供对 Session 对象的静态访问。可以为 Web 应用程序作用域为请求,为客户端应用程序作用域为线程。NHibernateModule
在请求结束时关闭会话。NHibernateTemplate
是一个小型实用程序类,提供了一些便捷的方法和简单的事务处理。- 模型基类(
IdentifiedObject
、NamedObject
)不是必需的,但可以用作实体的基类。它们只是重写了Equals()
和GetHashCode()
以获得更好的集合支持。
NHibernateDataSource
- 基本功能包括双向绑定、过滤和排序。
- 高级功能包括分页、嵌套双向绑定、生成
DataTable
、绑定到ValueType
、绑定到返回多个列的查询以及将实体绑定到多对一属性。
致谢
Northwind 模型,包括类文件和映射文档,最初是用 Object Mapper 生成的。
在我们编写库的过程中,我们还整合了一些由多位慷慨人士免费提供的代码,包括
- Simon Green 的 NHibernate.Helper。虽然我们已经分解了大部分代码,但这提供了
NHibernateModule
和SessionHolder
的基础。 - Paul Wilson 的 WilsonORMapper DataSource Control。
TypeUtils
使用 Peter Johnson 的一些 转换代码来处理 Nullable 的一些怪癖。
NHibernateDataSource
安装
默认情况下,NHibernateDataSource
使用我们编写的 ISessionFactory
和 ISession
管理基础架构,但 NHibernateDataSource
可以轻松地与之解耦。您所要做的就是提供 ISession
或 ISessionFactory
。
工作原理如下...
- NHibernateDataSource 需要能够获取
ISession
。您可以通过两种方式之一自行提供ISession
。您可以继承并重写CreateSession()
,或者处理CreatingSession
事件并将Session
属性设置为NHibernateDataSource
。否则,将使用SessionHolder.Current
。 - 如果使用
SessionHolder
,它需要一个ISessionFactory
。如果设置了SessionHolder.SessionFactory
,则会使用它。您还可以设置SessionHolder.Interceptor
(一个拦截器),该拦截器将在创建新的ISession
时添加。否则,它将使用NHibernateUtils
来尝试查找和加载配置。 - 如果使用
NHibernateUtils
加载Configuration
...它将首先在应用程序配置中查找配置。nhibernate.config
属性可以指定外部 nhibernate.config 文件的路径。否则,NHibernateUtils
通过调用configuration.Configure()
来使用 NHibernate 的默认配置处理。应用程序配置可以包含一个nhibernate.mapping.attributes.assemblies
属性,指定一个或多个包含属性的程序集,这些属性应用于生成映射配置。
类图
基本用法
与 GridView 一起使用
只需设置 TypeName
即可。
<cp:NHibernateDataSource runat="server" ID="dsProducts"
TypeName="Northwind.Model.Product" />
开箱即用,您可以获得排序、更新和删除。要指定默认排序,请设置 DefaultSortExpression
属性。
<cp:NHibernateDataSource runat="server" ID="dsProducts"
TypeName="Northwind.Model.Product"
DefaultSortExpression="ProductName ASC" />
演示页面:Products/Default.aspx。
与 FormView 一起使用
- 设置
TypeName
。 - 添加一个
QueryStringParameter
。由于AutoGenerateWhereClauseFromSelectParams
默认启用,数据源会自动生成并附加Where
子句片段"WHERE Id = :Id"
。
<cp:NHibernateDataSource ID="dsProduct" runat="server"
TypeName="Northwind.Model.Product">
<SelectParameters>
<asp:QueryStringParameter
Name="Id" QueryStringField="Id" />
</SelectParameters>
</cp:NHibernateDataSource>
现在我们只需将其连接到 FormView
。
演示页面:Products/Edit.aspx。
来自查询的 GridView
由于 TypeName
最终会驱动查询 "Select o from [DataObjectType]
" 的生成,您也可以通过设置 Query
属性来提供自己的查询,在这种情况下将忽略 TypeName
。
<cp:NHibernateDataSource ID="dsOrders" runat="server"
Query="select o from Northwind.Model.Order as o where o.Customer.Id = :id "
DefaultSortExpression="OrderDate">
<SelectParameters>
<asp:QueryStringParameter Name="id"
QueryStringField="Id" />
</SelectParameters>
</cp:NHibernateDataSource>
请注意,我们仍然不希望在查询中指定 ORDER BY
子句,因为我们希望 NHibernateDataSource
能够根据传递给它的排序参数来更改 ORDER BY
子句。
演示页面:Products/Query.aspx。
高级用法
我们已经涵盖了基础知识。您现在就可以停止了,目前为止的内容对于基本 CRUD 操作已经足够了。接下来是一些更高级、有点令人困惑、有点实验性的内容,但如果您需要,它会非常有用。
构建搜索/筛选屏幕
对我们来说,一个常见的用例是这样一种屏幕:它基本上由一个结果列表和一些控件组成,这些控件指定了过滤/搜索数据的条件。我们意识到,启用 FormView
绑定的那些功能也可以用来声明式地创建这样的屏幕。事实上,它们已经存在了。这是一个例子。
我有一个订单搜索屏幕,我想能够指定一个 OrderDate
来搜索。
步骤是
- 创建
GridView
和txtOrderDate
- 创建
NHibernateDataSource
- 设置
TypeName
- 向文本框添加
ControlParameter
<SelectParameters>
<asp:ControlParameter ControlID="txtOrderDate"
Name="OrderDate"
PropertyName="Text" />
</SelectParameters>
就像我们用 QueryString
中的 "Id"
绑定 FormView
时一样,OrderDate
控件参数将生成 Where
子句片段 "WHERE OrderDate = :OrderDate
"。
但是,如果用户没有指定 OrderDate
(即,她想查看所有订单)怎么办?查询将失败,因为会有一个 Where
子句但没有必要的参数。这可以通过 ExcludeEmptySelectParameters
属性来解决,该属性默认设置为 true。如果设置为 true,当参数值为空或 null 时,将不为其生成 WHERE
子句。
使用 ParameterExtender 获取更精细地控制
假设我想在日期范围内搜索,而不是搜索单个日期。我将使用两个控件而不是 txtOrderDate
:txtStartDate
和 txtEndDate
。
我的 SelectParameters
将如下所示
<SelectParameters>
<asp:ControlParameter ControlID="txtStartDate"
Name="StartDate"
PropertyName="Text" />
<asp:ControlParameter ControlID="txtEndDate"
Name="EndDate"
PropertyName="Text" />
</SelectParameters>
但是, there are a couple of problems
- 没有名为
StartDate
的属性,也没有EndDate
。 - 这是一个范围,所以我想让我的
Where
子句是 "WHERE o.StartDate >= :StartDate AND o.EndDate <= :EndDate
" 这样的。
这就是 ParameterExtender
的用武之地。
通过向 NHibernateDataSource
的 ParameterExtenders
集合添加项目,我可以添加关于我的参数的额外信息。
<ParameterExtenders>
<cp:ParameterExtender Name="StartDate"
WhereCompareOperator=">="
PropertyName="OrderDate" />
<cp:ParameterExtender Name="EndDate"
WhereCompareOperator="<="
PropertyName="OrderDate" />
</ParameterExtenders>
我的参数允许我将 StartDate
参数映射到 OrderDate
属性,并且还可以指定用于比较的操作符。
继续阅读以下两个部分,看看最终的数据源声明是什么样的。
分页
如果您熟悉 GridView
和 DataSource
控件,您就会知道 GridView
可以分页,它可以自行分页,或者它可以将分页传递回 DataSourceControl
控件,从而可能允许开发人员将分页完全推回到数据库,从而消除不必要的数据检索。
如果您熟悉 NHibernate,您就会知道 IQuery
可以分页其结果。
这表明将分页推回到 NHibernate 应该很容易,但有一个小问题
- 如果 NHibernate 对其结果进行分页,它无法返回总结果集的计数。这意味着,虽然
DataSource.CanPage
为 true,但如果启用了分页,则DataSource.CanRetrieveTotalRowCount
为 false。 - 在这种
DataSource.CanPage
为 true 但DataSource.CanRetrieveTotalCount
为 false 的情况下,GridView
将回退到自己的分页机制,该机制检索所有结果。 - 因此,将分页推送到 NHibernate 的唯一方法是我们发出第二个查询。(在我看来,这是 NHibernate 应该自动完成的事情,但我不是 NHibernate 开发者。)
我们通过查找查询中的 SELECT
子句并将其替换为 "Select count(*)
" 来解决问题。这在大多数情况下有效,但并非总是如此。
因此,此分页功能默认禁用。要启用它,只需将 EnablePaging
设置为 true
。一旦启用,AutoGenerateCountQuery
(默认设置为 true)将决定 NHibernateDataSource
是否应生成计数查询。如果您想自己创建计数查询,可以处理 Selecting
事件,然后查看 NHibernateSelectingArgs.ExecutingSelectCount
是否为 true,然后将 Query
属性设置为适当的计数查询。
将查询结果包装在 DataTable 中
NHibernateDataSource
可以将其结果包装在 DataTable
中,以简化复杂的绑定场景。只需将 GenerateDataTable
设置为 true
。这在涉及 ASP.NET 2.0 双向绑定机制限制的几个场景中非常有用。
将 GridView 绑定到多个 NHibernate 查询结果列
HQL 允许我们接收我们需要的确切列。这在需要呈现涉及多个实体/表的数据表,但又不希望承担完全加载所有这些实体成本的情况下很有用。例如,查询
Select o.OrderDate, o.Customer.CompanyName
from Northwind.Model.Order o
在这种情况下,NHibernate IQuery
将返回一个对象数组的 IList
,其中数组中的元素对应于返回的列。这是 NHibernate 的一个有用功能,但没有现成的方法可以将对象数组绑定到 GridView
。
解决方案是将 IQuery
返回的 IList<object[]>
转换为 DataTable
。GenerateDataTable
会自动完成此操作,根据数组索引自动生成列名,例如 "Column0"、"Column1" 等。
我们的最终声明如下所示
<cp:NHibernateDataSource runat="server" ID="dsOrders"
Query="Select o.OrderDate, o.Customer.CompanyName from Northwind.Model.Order o"
DefaultSortExpression="OrderDate DESC"
ExcludeEmptySelectParameters="True"
GenerateDataTable="True"
EnablePaging="True" >
<SelectParameters>
<asp:ControlParameter ControlID="txtStartDate"
Name="StartDate" PropertyName="Text" />
<asp:ControlParameter ControlID="txtEndDate"
Name="EndDate" PropertyName="Text" />
</SelectParameters>
<ParameterExtenders>
<cp:ParameterExtender Name="StartDate"
WhereCompareOperator=">="
PropertyName="OrderDate" />
<cp:ParameterExtender Name="EndDate"
WhereCompareOperator="<="
PropertyName="OrderDate" />
</ParameterExtenders>
</cp:NHibernateDataSource>
演示页面:Orders/Default.aspx。
将 DropDownList 绑定到原始值列表
在创建演示时,我希望能够按客户的国家/地区过滤客户列表。Northwind 中没有 Country 表,因此也没有 Country 实体。所以为了获取国家/地区列表,我们必须对 Customer
的 Country
属性执行 Select distinct
。使用 NHibernate 查询很容易做到这一点
Select distinct c.Country from Customer c order by c.Country
但是,将我们的 DropDownList
绑定到结果(类型为 IList<string>
)将存在问题。给定一个要绑定的 DataSourceID
,DropDownList
需要知道要绑定到哪个属性作为每个条目的值/文本。但是原始值类型(如字符串)没有这样的属性。这时 GenerateDataTable
就派上用场了。它将创建一个只有一列 "Column0" 的 DataTable
,其中包含每个字符串值。然后我们可以将 SelectedValue
绑定到 Column0
<cp:NHibernateDataSource runat="server" ID="dsCountries"
Query="Select distinct c.Country from Customer c order by c.Country"
GenerateDataTable="True"
/>
<asp:DropDownList ID="ddlCountry" runat="server"
DataSourceID="dsCountries"
DataValueField="Column0"
AutoPostBack="true"
EnableViewState="false">
</asp:DropDownList>
演示页面:Customers/Default.aspx。
对 WHERE 子句进行精细控制
到目前为止,NHibernateDataSource
一直从其 SelectParameters
生成 Where
子句。在后台,当 NHibernateDataSource
处理其 SelectParameters
时,它会为每个参数生成一个 QueryFragment
。稍后,当生成 HQL 时,QueryFragment
(如果存在)将被组合起来形成 WHERE
子句。
除了从 SelectParameters
生成这些 QueryFragment
s 之外,您还可以显式创建任何数量的 Where
子句,这些子句可能具有多个不同的参数依赖项。依赖项用于确定 QueryFragment
是否应包含在 WHERE
子句中,具体取决于它所依赖的参数是否存在。如果未设置命名参数,则依赖于该参数的任何 Where
子句片段都不会包含在查询中。您还可以使用否定(通过在前面加上 "!"),只有当参数依赖项不存在时,才会包含 where 片段。
没有对此的演示,但它对某些筛选屏幕非常有用。
非常高级/实验性用法
这些东西有点实验性,通常涉及 GenerateDataTable
的更复杂用法。
ObjectDataTableAdapter
NHibernateDataSource.GenerateDataTable
背后的实现是 ObjectDataTableAdapter
,它与 NHibernate 无关,并且本身就是一个有用的类。ObjectDataTableAdapter
值得单独写一篇文章,所以我们在这里不会过多详细介绍。
ObjectDataTableAdapter
的目的是接受一个 IList
的 object
并为其生成一个 DataTable
。它还提供了更新列表中数据的方法。
生成 DataTable
的策略取决于 IList
中元素的类型。
- 原始值/值类型 - 单个列 "Column0" 将具有与元素类型相同的
Type
,并包含元素的值。 object[]
- 将生成与数组索引对应的类型化列,例如 "Column0"、"Column1"。- 其他对象/实体 - 将通过反射类型和反射值填充的值来生成类型化列。
此外,ObjectDataTableAdapter
可以配置为生成附加列,这些列可以是复杂属性的别名,即 .NET DataBinding 通常无法访问的嵌套和索引属性。NHibernateDataSource
暴露了一个 DataTableConfig
属性,以允许在设计时配置规则,这些规则决定 ObjectDataTableAdapter
如何生成 DataTable
。
因此,我们不仅可以包装原始值或对象数组列表到 DataTable
中,还可以包装实体列表,并且可以为嵌套属性(如 Order.Customer.CompanyName
)生成列。这有一些有趣的应用程序。
绑定到嵌套属性
演示中的示例是一个相当荒谬的场景,但它展示了可以做什么。如果我正在编辑一个客户,并且出于某种愚蠢的原因,我想能够更改他第一个订单的日期,我可以将整个客户放入一个 DataTable
中,添加一个作为 Customer.Orders[0].OrderDate>
属性别名的列。
我的数据源如下所示
<cp:NHibernateDataSource ID="dsCustomer" runat="server"
TypeName="Northwind.Model.Customer"
GenerateDataTable="true"
>
<DataTableConfig>
<Columns>
<cp:PropertyColumn ColumnName="FirstOrderDate"
PropertyName="Orders[0].OrderDate"
TypeName="System.Nullable`1[System.DateTime]" />
</Columns>
</DataTableConfig>
<SelectParameters>
<asp:QueryStringParameter Name="Id"
QueryStringField="Id" Type="String" />
</SelectParameters>
</cp:NHibernateDataSource>
在我的 FormView
中,我只需将 TextBox
绑定到我的别名属性
<asp:TextBox ID="txtOrderDate" runat="server"
Text='<%# Bind("FirstOrderDate", "{0:d}") %>'></asp:TextBox>
演示页面:Customers/Edit.aspx。
绑定到多对一实体
使用域模型和 NHibernate 时,另一个常见用例是需要使用 DropDownList
等选择控件来设置多对一属性,即非原始类型的属性。例如,假设我想编辑一个产品并设置其 Category
。通常,这需要用值填充 DropDownList
,其中 SelectedValue
是类别的 Id
。然后,在回发时,我必须以编程方式找到相应的 Category
实体并将其设置为 Product.Category
属性。或者,您可以使 SelectedValue
实际存储整个 Category
实体,这需要将其序列化。有时这对小型“查找”对象可行,但对大型实体则不行。
我们的解决方案是向我们的 DataTableConfig
添加一个特殊的列映射选项。乍一看似乎有点奇怪,但它可以轻松解决多对一绑定问题。我们可以绑定到一个属性,然后解除绑定到另一个属性。在这种情况下,我们将创建一个名为 CategoryId
的别名列。当我们绑定(“获取”属性)时,它将使用 Product.Category.Id
。当我们解除绑定(“设置”属性)时,它将设置 Product.Category
属性,NHibernateDataSource
将足够智能地知道,当它被告知设置一个类型为已映射 NHibernate 实体的属性时,它应该假设该值是其 Id
并加载相应的对象。
数据源如下所示
<cp:NHibernateDataSource ID="dsProduct" runat="server"
TypeName="Northwind.Model.Product" GenerateDataTable="True">
<SelectParameters>
<asp:QueryStringParameter Name="Id" QueryStringField="Id" />
</SelectParameters>
<DataTableConfig>
<Columns>
<cp:PropertyColumn ColumnName="CategoryId"
PropertyName="Category.Id" UpdatePropertyName="Category" />
</Columns>
</DataTableConfig>
</cp:NHibernateDataSource>
DropDownList
声明如下所示
<asp:DropDownList ID="ddlCategory" runat="server"
DataSourceID="dsCategories"
DataTextField="CategoryName"
DataValueField="Id"
AppendDataBoundItems="True"
SelectedValue='<%# Bind("CategoryId") %>'>
<asp:ListItem></asp:ListItem>
</asp:DropDownList>
<cp:NHibernateDataSource ID="dsCategories" runat="server"
TypeName="Northwind.Model.Category">
</cp:NHibernateDataSource>
演示页面:Products/Edit2.aspx。
未涵盖的功能
事件
事件基于 ObjectDataSource
的事件,包含在内以保持完整性。但是,它们尚未经过严格测试。
依赖项
所有依赖库都包含在代码中。基本上是
- NHibernate 1.2.0 beta 2。这有许多运行时依赖项,所有这些都包含在演示中。
- Spring.NET-1.1.0-P3,它需要 Common.Logging。
其他版本的这些库可能也能正常工作,但我们尚未测试过。
Spring.NET 仅在 ObjectDataTableAdapter
需要对实体属性进行反射时才需要。它很可能很容易被另一个属性获取/设置实现所取代。
注意事项
- 原始代码很快、很粗糙、没有文档,而且有点丑陋。我后来清理了它并添加了几个功能,但它仍然是“alpha”代码,仍处于早期阶段。
- 我们在会话和事务管理方面可能不完全遵循“最佳实践”。也就是说,我们的方法没有遇到任何问题。
- 设计器支持不如 MS 数据源控件丰富。这是我希望添加的功能。最好在运行时添加模式访问,以便我们可以提供下拉列表中的属性并自动生成
FormView
模板。但是,这需要在设计时调用RefreshSchema
时实例化ISessionFactory
,这可能会很慢/令人讨厌。然而,另一位开发人员在 Hibernate 论坛上发表了一个有希望的评论,他说他创建了一个 NHibernateDataSourceControl
,它在设计时提供了 HQL 智能感知。非常酷的东西 - 希望他能开源它。 - 我们从未尝试过将此控件与 NHibernate 的版本支持一起使用。
- 我们仍在熟悉
DataSourceControl
,并希望在我们了解更多信息后进行以下更改 - 没有 ViewState - 目前该控件不将任何内容存储在 ViewState 中。
DataSourceView.OnDataSourceViewChanged
不会触发。
简短的观点:UI 与 NHibernate 的耦合
虽然我们非常支持良好的分层和松耦合,但我们看不到将 UI 与 NHibernate 解耦的足够好处。原因如下
- 我们编写 Web 应用程序,我们控制环境。因此,保持所有已部署实例同步很容易。如果情况并非如此,我们可能需要一个独立于实现的 DAO 接口层。
- 我们的主要关注点是开发速度和最少的缺陷。我们发现,确保这一点最好的方法通常是最小化复杂性,并编写简单、简洁、可读的代码。因此,我们尽量避免严格的分层(通常需要创建一个包含许多执行相同操作的类的命名空间 - 委托给实现)以及与之相关的代码生成,除非我们认为它特别有用。
- NHibernate 提供了极好的抽象,远离数据库模式和方言,这通常是我们所需的所有分层。
- 我们真的不认为需要将 NHibernate 替换为另一个持久化解决方案。只要我们是针对关系数据库进行开发,NHibernate 就是一个不错的选择。
- 如果我们选择用 DLINQ 等其他 ORM 解决方案替换 NHibernate,我们预计过渡将相当顺利。(我们可能会为新的 ORM 机制找到或编写一个可比较的 DataSourceControl,然后切换它们。)
- 如果我们选择用非 ORM 解决方案(如 Web 服务)替换 NHibernate,调整少量 UI 代码将是最小的担忧。
- 这一切归根结底是,对我们来说,更改 UI 代码的成本与严格分层的成本相比微不足道。
此外,DataSourceControl
是分隔数据访问的一种好方法。当我们编写 NHibernateDataSource
时,它大大减少了 UI 中的 NHibernate 代码。例如,在演示中,UI 中没有 NHibernate 代码。