SQL Server 2005 中的 XML 数据类型技巧






4.75/5 (27投票s)
使用 XML 数据类型存储数据的技术。
引言
本文概述了在创建 VS2005 应用程序时,使用 SQL 2005 中提供的新 XML 数据类型的一些技术。这里的示例是用 VB.NET 编写的,但有 C# 版本的源代码。
背景
如果您使用面向对象 (OO) 语言(如 VB.NET 或 C#)进行开发,并将数据存储在关系数据库系统(如 SQL Server)上,经常出现的一个问题是如何处理 OO 的灵活性与数据库更严格的层次结构之间的差异。
例如,假设您的应用程序是一个在线电脑硬件商店,您需要存储一组“产品”的详细信息。通用字段,如 ProductID、StockCode、ProductName 等,相对容易映射到数据库结构。
然而,当字段要求根据产品性质而差异很大时,会发生什么?数码相机(像素数量、最大变焦、存储类型)的字段与硬盘(容量、接口类型、速度、外形尺寸)的字段会非常不同。您不能真正将这些作为列添加到表中,因为很快表就会变得非常大,并且有很多冗余字段。
一种传统方法是创建一个子表,例如“ProductAttributes”,并以不同的行存储属性。然后问题就变成了如何以通用方式存储不同的数据类型,例如字符串、日期、货币值、整数等。每次我们需要检查单个产品时,它还需要一个 SQL JOIN,以及更多的代码来处理、排序和显示这些信息。
使用 XML 以更“灵活”的方式存储此类数据是一种将值更接近对象而无需诉诸子表的方法。我们可以不使用子表,而只添加一个 XML 列 ProductAttributes 并存储一些带有属性的 XML。
起源
我在为数据库应用程序创建作业处理系统时开发了这项技术。作业处理“引擎”将从数据库表中加载待处理作业,并执行它们。“作业”本质上是 VS2005 中的一个基类,具有 JobID、JobName、RunAfter 等通用属性。问题是实际的作业实例类,如 ExportAccountsDataJob 或 SendCustomerEmailAlertsJob,都需要存储和检索非常不同类型的数据。
SQL 2005 的新功能之一是支持 XML 作为数据类型。我意识到,如果我将每个实例的数据存储在一个可以序列化和反序列化为 XML 的类中,我就可以使用 XML 字段将这些数据存储在 Job 表中。这使得 Job 表结构非常简单,并且每个实现的复杂性都将处理哪些数据进入 XML 字段。
如果您没有 SQL 2005,可以使用标准数据库文本字段,尽管您将无法使用 SQL 2005 的功能来查询 XML 数据。本文无意展示此数据类型的所有强大功能和灵活性(仅 XQuery 就需要一本书),而是展示其一种用法。
工作原理
- 首先,我们在表中创建一个 XML 字段来存储我们的数据
- 接下来,我们在 .NET 中定义一个可以序列化为/从 XML 的类,以便我们可以将 XML 转换为对象实例并返回
- 然后,为了使其更易于访问,我们在数据库对象中创建一个属性,这将在下面详细解释
- 然后您可以添加任何您需要的,例如该数据类型的可重用用户控件
地址示例
本文随附的代码演示了使用 XML 将客户地址存储在 SQL 2005 数据库字段中。传统上,您可能会将 Line1、Line2、Town 等作为 Customers 表中的数据库列。这适用于一个地址实例,但重用性不强。
例如,想象一下,您可能还需要在下订单时存储一个 DeliveryAddress,或者为客户添加一个可选的 InvoiceAddress。这会导致列名为 InvoiceAddressLine1、InvoiceAddressLine2 或 DeliveryAddressLine 1 等——您知道您经历过。然后,供应商、业务合作伙伴、股东、送货公司等地址也随之而来。
每次您都必须在每个新的数据库表中定义所有相同的字段(使用不同的名称),然后构建和绑定不同的控件来显示和验证它们。重用性不高!
数据库表设计
因此,我们不定义带有单独地址字段(Line1、Line2 等)的客户表,而是使用 XML 数据类型简单地创建一个单个字段。我将它命名为 `AddressXML`。这是创建我们的 Customers 表的 SQL 代码
/* SQL to create simple customers table (with XML datatype field) */
CREATE TABLE [dbo].[Customers](
[CustomerID] [int] IDENTITY(1,1) NOT NULL,
[CustomerName] [varchar](64) COLLATE Latin1_General_CI_AS NOT NULL,
[AddressXML] [xml] NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED
(
[CustomerID] ASC
)WITH (PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
在示例源代码中,您可以在 `App_LocalResources` 目录中找到 `CreateDatabase.SQL`,它为您设置示例数据库并创建表和登录名。
请注意,SQL 2005 XML 数据类型可以支持将 XML 架构 (XSD) 链接到特定 XML 字段以提供数据完整性检查和索引支持,但为了简化本示例,我们只使用非类型化 XML 列。
代码
首先,我们在代码中创建一个包含我们希望存储的公共属性(Line1、Line2、Line3 等)的 `Address` 类。请注意,由于我们使用 XML 序列化,我们可以存储任何类型的属性,包括其他类,前提是它们支持序列化。
App_Code 目录包含一个 `Customer` 类,它以这种方式将主表列映射到属性,我编写了一些简单的 ObjectDataSource CRUD 函数用于数据库访问。
注意:ObjectDataSource 函数在创建参数时不使用 `SqlClient.SqlDataTypes.XML` 类型,它们使用 Varchar
或 Text
。这是因为 SQL 2005 XML 数据类型以 UTF16 编码存储,而我们的类使用的字符串是 UTF8。如果您尝试使用 XML 数据类型,当 SQL 尝试转换时会收到错误。如果您传递带有 UTF8 的 varchar
/text
,SQL 2005 会自动为您转换。
为了使地址值在客户对象中更易于使用,我们有一个 `CustomerAddress` 属性,它将通过反序列化 `AddressXML` 字段的内容来加载地址。
Public ReadOnly Property CustomerAddress() As Address
Get
GetAddress()
Return _address
End Get
End Property
Private _address As Address = Nothing
'ensure _address has a valid value
Private Sub GetAddress()
If _address Is Nothing Then
'check for blank
If Me.AddressXML Is Nothing Then
'create new instance
_address = New Address()
Else
'deserialize existing XML field
_address = Address.Deserialize(Me.AddressXML)
End If
End If
End Sub
Public Sub SaveAddress()
If _address IsNot Nothing Then
'write to field
Me.AddressXML = Address.Serialize(_address)
End If
End Sub
这意味着我们不必写
myCustomer.Line1 = "123 High Street"
我们有一个更优雅和逻辑的方法
myCustomer.CustomerAddress.Line1 = "123 High Street"
我这样做是为了对 `CustomerAddress` 的重复引用只反序列化 `AddressXML` 一次(按需),然后保留此值。这种技术的缺点是,如果您以这种方式更新地址,则必须明确告诉 Customer 类将数据保存回 `AddressXML`(请参阅 `SaveAddress` 方法)。
Address 类还有一个示例属性 `AddressAsHTML`,它不会被序列化,因为它是只读的。这用于 FindCustomers 页面中,将地址以 HTML 形式显示在标签中。
由于我将序列化技术用于几个不同的类,我创建了一个泛型 `XMLserializer(Of T)` 类(C# 中的 `XMLSerializer
Private Shared _serializer As New XMLserializer(Of Address)
Public Shared Function Serialize(ByVal myobject As Address) As String
Return _serializer.Serialize(myobject)
End Function
Public Shared Function Deserialize(ByVal xml As String) As Address
Return _serializer.Deserialize(xml)
End Function
在 .NET 中创建 XML 序列化器需要在运行时创建和编译 .DLL,所以我们**真的**希望尽量减少执行此操作的次数。使用共享/静态变量是实现此目的的一种方法,因为它只为每个应用程序实例创建一次。
示例网页
为了演示这一点,应用程序有两个页面,`AllCustomers.aspx` 和 `FindCustomers.aspx`。第一个页面在 `GridView` 中显示所有客户,并允许您选择、插入、更新和删除它们。
显示客户的 `DetailsView` 控件非常简单,因为我们使用 `AddressControl.ascx` 来显示和编辑 `AddressXML` 值。
AddressControl.ascx
`AddressControl.ascx` 对数据库或字段名一无所知,它只与 Address 类交互。它有一个可绑定的属性 `AddressXML`,我们将其绑定到数据库字段。在我的示例中,它绑定到 `Customer.AddressXML`,但也可以轻松地以相同的方式绑定到 `Order.DeliveryAddressXML`。
当控件显示时,XML 会被传入(`AddressXML` 被设置)。这会反序列化 XML 并将值复制到控件。当请求 `AddressXML` 值时(例如,用于插入或更新),会读取控件值,创建 Address 对象,序列化并作为字符串返回。
XML 数据上的 SQL 查询
在 SQL 2005 之前,反对在数据库中使用 XML 的一个论点是,文本字段中的 XML 数据不易查询。SQL 查询
SELECT * FROM Customers WHERE Town = 'London'
在传统方法中非常简单高效。如果 XML 数据在文本字段中,要查找城镇为“London”的客户,则需要读取所有客户,反序列化地址并检查值——这显然效率会低得多。SQL 2005 支持查询 XML 数据(无论是类型化还是非类型化),因此这将转换为
SELECT * FROM Customers
WHERE AddressXML.value('(/Address/Town)[1]', 'nvarchar(1000)') = 'London'
“查找客户”页面展示了这种搜索方式的一个示例。您可以输入一个城镇并搜索具有该值的客户。数据源 `Customers.vb` 代码使用 XML 字段上的 XQuery 搜索。
请注意,我要求 Address 类提供实际的 XPath `Address/Town`,以防我们以后将“Town”更改为其他内容。这避免了将地址逻辑编码到 `Customer` 类中。
使用 XML 的好处
重用
将大部分地址结构和功能放在 Address 类中,我们现在可以实现重用。您的客户详细信息中需要发票地址吗?只需在数据库表中添加一个 `InvoiceAddressXML` 列,并在 Customer 类中添加支持代码以将其作为 `Customer.InvoiceAddress` 访问。
控件绑定等。
一个很酷的技巧是,我们可以将 `AddressXML` 视为一个单一属性,并将其传递给 `AddressControl` Web 用户控件。如果采用单独列的方法,我们将需要在地址控件中创建多个属性,并在数据绑定表单中使用控件时传递每个属性的值。
这是一个主要的好处,因为我以前不得不以这种方式创建一些非常复杂的用户控件,绑定 Line1、Line2 等真的非常耗时。
另一点是,Web 控件总是将值作为字符串传递。字符串的格式不正确(尤其是日期!)在区域设置可能导致问题时可能会成为一个真正的问题。XML 允许字符串数据通过并重新组装成安全的 .NET 数据类型。
灵活性
在这个例子中,我们只使用 `AddressXML` 来存储单一类型的数据。然而,理论上我们几乎可以将任何内容存储在 XML 字段中。回到我们之前的产品示例,我们需要为不同类型的产品存储不同的属性。假设我们的 `Products` 表可能包含 `ProductID`、`ProductName`、`ProductCategory` 和 `ProductAttributesXML`。
然后,我们将使用 `ProductCategory` 来决定在 `ProductAttributesXML` 中存储哪种类型的类。因此,对于 `ProductCategory`='数码相机',我们可以定义一个 `DigitalCameraAttributes` 类,其中包含 `Pixels`、`MaximumZoom`、`StorageType` 等字段,以及一个特殊的自定义控件来显示这些详细信息。同样,对于硬盘类别,可以定义一个名为 `HardDiskAttributes` 的类。
结论
我希望您觉得这种方法很有用。它提供了一种在使用关系数据库(如 SQL 2005)时处理面向对象代码的复杂性和灵活性的新方法。