从 XML 生成 SQL 数据库架构 - 第一部分(文件格式和 XSD)






4.95/5 (13投票s)
本系列文章共三部分,旨在介绍从 XML 文件格式生成 SQL 的项目。本文是第一部分,将介绍项目的背景,并讨论文件格式和 XSD 架构的开发。
引言
这是三篇系列文章中的第一篇,介绍了我为解决工作环境中的特定问题而进行的一个项目。该项目有两个核心成果:一种用于描述数据库架构的新 XML 文件格式,以及一个用于将新格式转换为 SQL 的可执行文件。在此过程中,我将分享一些设计过程中的想法,并介绍一些可能在此项目之外也有应用的支持性代码库。我假设读者对几种技术有基本了解,但也会提供链接和定义,希望能让每个人都能理解。
在本文(第一部分)中,我将介绍项目的背景,并讨论文件格式和 XSD 架构的开发。在第二篇文章中,我将重点介绍创建映射到文件格式的代码模型。第三篇也是最后一篇文章将介绍一种简单的代码生成方法,并总结所有遗留问题。
背景
我经常在团队环境中使用 Microsoft SQL Server(通常简称为 SQL Server)。当我在这些文章中谈到SQL时,我实际上指的是 Microsoft 的语言版本,称为T-SQL,尽管将来也可以轻松地处理该语言的其他版本。SQL 拥有悠久而辉煌的历史,并被数百万人用于包括我的项目在内的各种项目中,但这个项目之所以存在,是因为它存在一些让我感到沮丧的不足之处。
SQL 通常以两种不同的方式使用:作为DDL和用于CRUD查询。当创建一个新的数据库时,它不仅是空的,而且结构也是空的。在添加数据(例如,一个人的详细信息)之前,必须添加一个表来包含数据。在 SQL 中,可以使用如下语句来实现:
CREATE TABLE [dbo].[EXAMPLE_TABLE](
[PK] [int] IDENTITY(1,1) NOT NULL,
[EXAMPLE_COLUMN1] [varchar](50) NOT NULL,
)
类似的语句用于描述列的默认值、索引、关系、约束等等。所有这些定义统称为数据库架构。一旦定义了架构,就可以填充数据库数据,检索或修改数据,等等。对于查询目的,我没有意见,尽管越来越普遍的做法是避免使用原始 SQL 进行查询,而是“在后台”生成它,例如使用LINQ to SQL。
不,问题在于我不喜欢用 SQL 来定义架构。首先,我通常认为数据最好以声明式的方式描述。数据库的架构无疑是数据;实际上,它存储在数据库的“系统表”中。但 SQL 相对难以解析。要将其视为数据,您通常必须将其加载到实际的 SQL Server 实例中,然后使用编程接口(例如,DbConnection.GetSchema
)进行查询。这在我看来是错误的,所以我决定尝试解决这个问题。
那些经常使用数据库的人可能会抗议说,DDL 不仅仅是*创建*架构。一旦数据库充满了结构和数据,通常就不可能全部丢弃并从头开始!相反,管理变更变得必要。SQL 是一种完整的过程语言,因此可以根据特定条件进行更改,还可以用于操作数据、暂时放宽约束等。但是,这个项目的背景是我工作的地方有一个工具链,可以从使用简单 DDL 创建的空数据库快照更新实时数据库。我相信数据库架构的更改可以用声明式的方式更好地表示,这也是该项目的长期目标。但目前,该项目的范围仅限于从头开始创建一个新的空数据库。
“那么,您为什么想将数据库架构视为数据?”我听到您这样问。一个答案是用于代码生成。有几种现有的技术可以将数据库架构映射到自动生成的类,这种技术称为对象关系映射 (ORM)。简而言之,表和列被映射到类和属性,数据自动序列化到/从数据库到类实例。该领域中的两个主要参与者(Microsoft Entity Framework和LLBLGen Pro)采用从数据库实例更新其生成代码的方法。
这是一种完全合理的方法,但它假定数据库已经具有正确的架构,而在团队环境中情况并非总是如此。在团队中工作时,最好是每个人都能同时对数据库架构进行更改,然后稍后合并他们的更改。还希望有更改历史记录以及谁所做的更改。换句话说,我指的是源代码管理。现在,完全有可能在源代码管理下使用 SQL 定义数据库架构。但是,当一个开发人员获取另一个开发人员的更改时,他们必须记住在更新 ORM 工具生成的代码之前更新自己的本地数据库实例。
这种手动更新数据库实例仅是必需的,因为 ORM 工具依赖于它。否则,可以从应用程序代码内部自动更新数据库架构。碰巧,有一个名为NHibernate的项目,它不像这样依赖于数据库实例。在 NHibernate 中,代码类是起点,数据库架构是通过描述类如何映射到它的 XML 文件生成的。此外,还可以从 XML 映射文件生成代码。这更好:所有开发人员都可以编辑一个 XML 文件,并自动神奇地生成数据库架构和 ORM 代码。NHibernate 对我来说唯一的问题是映射文件以代码为中心。也就是说,它描述了如何将 NHibernate 兼容的代码类映射到数据库,而不是描述数据库本身。虽然数据库架构*隐含*在 NHibernate 映射文件中,但它*并不明确*。这对于许多应用程序来说都可以,但我并不使用 NHibernate 作为我的代码 ORM 解决方案,所以它对我不起作用。
我遇到的第二个主要问题是我不喜欢 SQL 作为一种语言来定义我的数据库架构。我发现它很难记住,语法奇怪且笨拙。说出来了。更改数据库架构是我不经常做的事情,以至于当我再次处理它时,我经常会查阅参考资料。另一方面,尽管有记忆衰退,但我永远不会忘记如何编辑 XML。当然,XML 可能有点丑陋,但至少它很规则,而且解析起来很容易。因此,我充分意识到我是在浪费自己的时间,我决定开发一种基于 XML 的文件格式来描述数据库架构,并开发某种解决方案来将新格式转换为 SQL。
更进一步,我意识到我最终希望能够以可视化方式编辑文件格式(类似于SQL Server Management Studio)。但可视化编辑器本身就是一个庞大的项目,所以我决定将其称为“阶段 2”,并在“阶段 1”中仅仅将其记在心里。同时,我的新文件格式必须易于手动编辑。
在本文的其余部分,我将描述新的文件格式以及其中一些设计决策。有时,这会非常详细,所以如果您对 XSD 完全不熟悉,可能值得简要了解一下 XSD。
要求
这是我为文件格式和转换过程提出的要求列表
- 描述数据库架构
- 可进行版本控制,并具有有用的历史记录
- 易于手动编辑
- 易于记忆的语法
架构:为了使项目易于管理,我决定不求全责备地描述数据库架构。相反,我的目标是完整地描述一个真实的、实际的客户数据库。这意味着
- 表格
- Columns
- Types
- 默认值
- 标识符规范
- 主键
- 唯一约束
- 索引
- 关系
- 存储过程
版本控制:显然,任何文件都可以进行版本控制。我希望一种文件格式可以由多个人编辑并合并。我还希望能够在文件的版本历史中清楚地看到更改。
编辑:尽可能地,我希望文件格式的结构与我思考数据库架构的方式相同,并且是以手动编辑为目的设计的。
语法:如果我一段时间不使用 SQL,我倾向于忘记细节,然后查阅参考资料。所以我想要一种文件格式,通过保持一致和自解释来指导我。
除了将文件格式转换为 SQL 的明显要求之外,我的主要关注点是便利性。该过程应易于通过快捷方式、批处理文件或构建步骤运行。将其部署到新机器上也应很方便。
文件格式实现
正如本文标题所示,我选择XML作为我的文件格式。它满足了要求,并且具有更广泛的优势,超出了本文的范围。特别是,如果附带一个有效的XSD,Visual Studio 在编辑文件时将提供智能感知。对于初学者来说,XML 文件就像数据库一样有架构,尽管 XML 文件本质上是分层的,但架构的形式非常不同。虽然不是创建有效 XML 文件所必需的,但架构很有用,因为它限制了文件中允许存在的内容,并向任何查看它的人或代码标识了格式。XSD 本身是用 XML 定义的,是为 XML 文件定义架构的最“标准”方法。Visual Studio(和其他编辑器)将解析 XSD 并使用它在编辑 XML 文件时提供智能感知。
我在此包含完整的 XSD 文件以供参考,但它相当长,所以您可能想将其折叠起来,直到您准备查看它。
<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="DatabaseSchemaModel"
targetNamespace="http://olduwan.com/DatabaseSchemaModel.xsd"
elementFormDefault="qualified"
xmlns="http://olduwan.com/DatabaseSchemaModel.xsd"
xmlns:dbsm="http://olduwan.com/DatabaseSchemaModel.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
<!--Columns-->
<xs:complexType name="parameterlessType"/>
<xs:complexType name="identity">
<xs:attribute name="seed"
use="optional" type="xs:integer"/>
<xs:attribute name="increment"
use="optional" type="xs:integer"/>
<xs:attribute name="notForReplication"
use="optional" type="xs:boolean"/>
</xs:complexType>
<xs:group name="withIdentity">
<xs:sequence>
<xs:element name="identity" minOccurs="0"
maxOccurs="1" type="identity"/>
</xs:sequence>
</xs:group>
<xs:attributeGroup name="withDefaultExpression">
<xs:attribute name="defaultExpression"
use="optional" type="xs:string"/>
</xs:attributeGroup>
<xs:complexType name="int">
<xs:group ref="withIdentity"/>
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default"
use="optional" type="xs:int"/>
</xs:complexType>
<xs:complexType name="bigint">
<xs:group ref="withIdentity"/>
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default"
use="optional" type="xs:long"/>
</xs:complexType>
<xs:complexType name="smallint">
<xs:group ref="withIdentity"/>
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default"
use="optional" type="xs:short"/>
</xs:complexType>
<xs:complexType name="tinyint">
<xs:group ref="withIdentity"/>
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default"
use="optional" type="xs:byte"/>
</xs:complexType>
<xs:complexType name="decimal">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="precision"
use="required" type="xs:integer"/>
<xs:attribute name="scale"
use="required" type="xs:integer"/>
<xs:attribute name="default"
use="optional" type="xs:decimal"/>
</xs:complexType>
<xs:complexType name="decimalScale0">
<xs:group ref="withIdentity"/>
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="precision"
use="required" type="xs:integer"/>
<xs:attribute name="default"
use="optional" type="xs:decimal"/>
</xs:complexType>
<xs:complexType name="bit">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default"
use="optional" type="xs:boolean"/>
</xs:complexType>
<xs:simpleType name="moneydefault">
<xs:restriction base="xs:decimal">
<xs:totalDigits value="10"/>
<xs:fractionDigits value="4"/>
<xs:minInclusive value="-922337203685477.5808"/>
<xs:maxInclusive value="922337203685477.5807"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="money">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default"
use="optional" type="moneydefault"/>
</xs:complexType>
<xs:simpleType name="smallmoneyDefault">
<xs:restriction base="xs:decimal">
<xs:totalDigits value="10"/>
<xs:fractionDigits value="4"/>
<xs:minInclusive value="-214748.3648"/>
<xs:maxInclusive value="214748.3647"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="smallmoney">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default"
use="optional" type="smallmoneyDefault"/>
</xs:complexType>
<xs:simpleType name="mantissaBits">
<xs:restriction base="xs:integer">
<xs:minInclusive value="1"/>
<xs:maxInclusive value="53"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="float">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="mantissaBits"
use="optional" type="mantissaBits"/>
<xs:attribute name="default"
use="optional" type="xs:decimal"/>
</xs:complexType>
<xs:complexType name="real">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default"
use="optional" type="xs:decimal"/>
</xs:complexType>
<xs:attributeGroup name="withStringDefault">
<xs:attribute name="default"
use="optional" type="xs:string"/>
</xs:attributeGroup>
<xs:simpleType name="fractionalSecondsPrecision">
<xs:restriction base="xs:integer">
<xs:minInclusive value="0"/>
<xs:maxInclusive value="7"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="variablePrecisionTime">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attributeGroup ref="withStringDefault"/>
<xs:attribute name="fractionalSecondsPrecision"
use="optional" type="fractionalSecondsPrecision" />
</xs:complexType>
<xs:simpleType name="charInteger">
<xs:restriction base="xs:integer">
<xs:minInclusive value="1"/>
<xs:maxInclusive value="8000"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="charLength">
<xs:union memberTypes="charInteger withMax"/>
</xs:simpleType>
<xs:complexType name="char">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attributeGroup ref="withStringDefault"/>
<xs:attribute name="length"
use="optional" type="charLength"/>
</xs:complexType>
<xs:complexType name="binary">
<xs:attribute name="length"
use="optional" type="charLength"/>
</xs:complexType>
<xs:simpleType name="withMax">
<xs:restriction base="xs:string">
<xs:enumeration value="max"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ncharInteger">
<xs:restriction base="xs:integer">
<xs:minInclusive value="1"/>
<xs:maxInclusive value="4000"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ncharLength">
<xs:union memberTypes="ncharInteger withMax"/>
</xs:simpleType>
<xs:complexType name="nchar">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attributeGroup ref="withStringDefault"/>
<xs:attribute name="length"
use="optional" type="ncharLength"/>
</xs:complexType>
<xs:simpleType name="GUID">
<xs:annotation>
<xs:documentation xml:lang="en">
The representation of a GUID.
</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="\{[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]
{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\}"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="uniqueidentifier">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default"
use="optional" type="GUID"/>
</xs:complexType>
<xs:complexType name="parameterlessStringType">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attributeGroup ref="withStringDefault"/>
</xs:complexType>
<xs:complexType name="column">
<xs:choice minOccurs="1" maxOccurs="1">
<!--Exact numerics-->
<xs:element name="bigint" type="bigint"/>
<xs:element name="numeric" type="decimal"/>
<xs:element name="numericScale0" type="decimalScale0"/>
<xs:element name="bit" type="bit"/>
<xs:element name="smallint" type="smallint"/>
<xs:element name="decimal" type="decimal"/>
<xs:element name="decimalScale0" type="decimalScale0"/>
<xs:element name="smallmoney" type="smallmoney"/>
<xs:element name="int" type="int"/>
<xs:element name="tinyint" type="tinyint"/>
<xs:element name="money" type="money"/>
<!--Approximate numerics-->
<xs:element name="float" type="float"/>
<xs:element name="real" type="real"/>
<!--Date and time-->
<xs:element name="date" type="parameterlessStringType"/>
<xs:element name="datetimeoffset" type="variablePrecisionTime"/>
<xs:element name="datetime2" type="variablePrecisionTime"/>
<xs:element name="smalldatetime" type="parameterlessStringType"/>
<xs:element name="datetime" type="parameterlessStringType"/>
<xs:element name="time" type="variablePrecisionTime"/>
<!--Character strings-->
<xs:element name="char" type="char"/>
<xs:element name="varchar" type="char"/>
<xs:element name="text" type="parameterlessStringType"/>
<!--Unicode character strings-->
<xs:element name="nchar" type="nchar"/>
<xs:element name="nvarchar" type="nchar"/>
<xs:element name="ntext" type="parameterlessStringType"/>
<!--Binary strings-->
<xs:element name="binary" type="binary"/>
<xs:element name="varbinary" type="binary"/>
<xs:element name="image" type="parameterlessType"/>
<!--Other data types-->
<xs:element name="rowversion" type="parameterlessType"/>
<xs:element name="hierarchyid" type="parameterlessStringType"/>
<xs:element name="uniqueidentifier" type="uniqueidentifier"/>
<xs:element name="sql_variant" type="parameterlessStringType"/>
<xs:element name="xml" type="parameterlessStringType"/>
</xs:choice>
<xs:attribute name="name"
use="required" type="xs:string"/>
<xs:attribute name="allowNulls"
use="optional" type="xs:boolean"/>
</xs:complexType>
<xs:complexType name="columnList">
<xs:sequence>
<xs:element name="column" maxOccurs="unbounded"
minOccurs="1" type="column"/>
</xs:sequence>
</xs:complexType>
<!--Constraints-->
<xs:simpleType name="sortOrder">
<xs:restriction base="xs:string">
<xs:enumeration value="Ascending"/>
<xs:enumeration value="Descending"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="constraintColumn">
<xs:attribute name="name" use="required" type="xs:string"/>
<xs:attribute name="sortOrder"
use="optional" type="sortOrder"/>
</xs:complexType>
<xs:complexType name="constraint">
<xs:sequence>
<xs:element name="column" maxOccurs="unbounded"
minOccurs="1" type="constraintColumn"/>
</xs:sequence>
<xs:attribute name="name" use="required" type="xs:string"/>
<xs:attribute name="clustered" use="required" type="xs:boolean"/>
<xs:attribute name="fillFactor" use="optional" type="xs:integer"/>
<xs:attribute name="padIndex" use="optional" type="xs:boolean"/>
</xs:complexType>
<xs:complexType name="primaryKey">
<xs:sequence>
<xs:element name="key" maxOccurs="1"
minOccurs="0" type="constraint"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="uniqueConstraints">
<xs:sequence>
<xs:element name="constraint"
maxOccurs="unbounded" minOccurs="0"
type="constraint"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="index">
<xs:complexContent>
<xs:extension base="constraint">
<xs:attribute name="unique"
use="optional" type="xs:boolean"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="indexes">
<xs:sequence>
<xs:element name="index" maxOccurs="unbounded"
minOccurs="0" type="index"/>
</xs:sequence>
</xs:complexType>
<!--Relationships-->
<xs:complexType name="relationshipColumn">
<xs:attribute name="name"
use="required" type="xs:string"/>
</xs:complexType>
<xs:complexType name="foreignKeyColumns">
<xs:sequence>
<xs:element name="column" maxOccurs="unbounded"
minOccurs="1" type="relationshipColumn"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="primaryKeyTable">
<xs:sequence>
<xs:element name="column" maxOccurs="unbounded"
minOccurs="1" type="relationshipColumn"/>
</xs:sequence>
<xs:attribute name="name"
use="required" type="xs:string"/>
</xs:complexType>
<xs:complexType name="relationship">
<xs:sequence>
<xs:element name="foreignKeyColumns"
maxOccurs="1" minOccurs="1"
type="foreignKeyColumns"/>
<xs:element name="primaryKeyTable" maxOccurs="1"
minOccurs="1" type="primaryKeyTable"/>
</xs:sequence>
<xs:attribute name="name"
use="required" type="xs:string"/>
</xs:complexType>
<xs:complexType name="relationships">
<xs:sequence>
<xs:element name="relationship" maxOccurs="unbounded"
minOccurs="0" type="relationship" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="table">
<xs:sequence>
<xs:element name="columns" maxOccurs="1"
minOccurs="1" type="columnList"/>
<xs:element name="primaryKey" maxOccurs="1"
minOccurs="1" type="primaryKey"/>
<xs:element name="uniqueConstraints" maxOccurs="1"
minOccurs="1" type="uniqueConstraints"/>
<xs:element name="indexes" maxOccurs="1"
minOccurs="1" type="indexes"/>
<xs:element name="relationships" maxOccurs="1"
minOccurs="1" type="relationships"/>
</xs:sequence>
<xs:attribute name="name"
type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="tables">
<xs:sequence>
<xs:element name="table" maxOccurs="unbounded"
minOccurs="0" type="table"/>
</xs:sequence>
</xs:complexType>
<!--Stored procedures-->
<xs:simpleType name="platform">
<xs:restriction base="xs:string">
<xs:enumeration value="SQLServer"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="procedure">
<xs:attribute name="name"
type="xs:string" use="required"/>
<xs:attribute name="path"
type="xs:string" use="required"/>
<xs:attribute name="platform"
type="platform" use="optional"/>
</xs:complexType>
<xs:complexType name="procedures">
<xs:sequence>
<xs:element name="procedure" maxOccurs="unbounded"
minOccurs="0" type="procedure"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="database">
<xs:sequence>
<xs:element name="tables" maxOccurs="1"
minOccurs="1" type="tables"/>
<xs:element name="procedures" maxOccurs="1"
minOccurs="1" type="procedures"/>
</xs:sequence>
<xs:attribute name="name"
type="xs:string" use="required"/>
<xs:attribute name="ExampleFolderPath"
type ="xs:string" use="optional"/>
<xs:attribute name="SQLServerOutputPath"
type ="xs:string" use="optional"/>
</xs:complexType>
<xs:element name="database" type="database"/>
</xs:schema>
我不会详细说明 XSD 中的每一个设计决策,但总的来说,我使用 SQL Server Management Studio 作为我的层级结构的指南,将其围绕表进行构建,并将索引和关系附加在其上。这是一个用新格式定义的一个非常简单的数据库架构。
<?xml version="1.0" encoding="utf-8" ?>
<database xmlns="http://olduwan.com/DatabaseSchemaModel.xsd" name="Simple">
<tables>
<table name="Customer">
<columns>
<column name="CustomerId">
<int>
<identity/>
</int>
</column>
<column name="Surname">
<nchar length="50"/>
</column>
</columns>
<primaryKey>
<key name="PK_Customer" clustered="true">
<column name="CustomerId"/>
</key>
</primaryKey>
<uniqueConstraints>
</uniqueConstraints>
<indexes>
<index name="IX_Customer_Surname" clustered="false">
<column name="Surname" sortOrder="Ascending"/>
</index>
</indexes>
<relationships>
</relationships>
</table>
<table name="Order">
<columns>
<column name="OrderId">
<int>
<identity/>
</int>
</column>
<column name="Details">
<nvarchar length="max"/>
</column>
<column name="Complete">
<bit default="false"/>
</column>
<column name="CustomerId">
<int/>
</column>
</columns>
<primaryKey>
<key clustered="true" name="PK_Order">
<column name="OrderId"/>
</key>
</primaryKey>
<uniqueConstraints>
</uniqueConstraints>
<indexes>
</indexes>
<relationships>
<relationship name="FK_Order_Customer">
<foreignKeyColumns>
<column name="CustomerId"/>
</foreignKeyColumns>
<primaryKeyTable name="Customer">
<column name="CustomerId"/>
</primaryKeyTable>
</relationship>
</relationships>
</table>
</tables>
<procedures>
</procedures>
</database>
这里是一个简化的数据库图,描述了它。
希望这种格式相当自明,如果您熟悉 SQL Server 的基本概念。我之所以选择使用 XSD,是因为我对此已经很熟悉,并且我知道它在 Visual Studio 中得到了很好的支持。只要 Visual Studio 知道在哪里可以找到 XSD(例如,XSD 文件存在于项目中),那么在编辑文件时,智能感知就会为您提供指导。经过一些思考,您可能无需任何参考就可以使用此格式定义自己的数据库。XSD 确实有两个局限性,这些局限性可以在其他 XML 架构格式(例如 RelaxNG)中解决,但要在 Visual Studio 中获得可用的智能感知是一个很高的优先级,所以我选择接受它们。
首先,无法根据属性的值来定义类型有效的属性/子元素。请注意,在列的定义中,类型是一个嵌套元素。
<column name="CustomerId">
<int>
<identity/>
</int>
</column>
可以说,将类型作为列的属性会更直观。
<column name="CustomerId" type="int">
<identity/>
</column>
但这样做的话,column
类型就必须支持描述任何类型所需的所有参数。例如,int
可以有标识符规范,但decimal
不行。使column
类型如此通用将严重影响其在智能感知方面的可用性。为了使这种方法奏效,我需要能够在我的架构中表达这个语句:
"column
类型可以有一个identity
子元素,如果type
属性是集合 [int, bigint, smallint, tinyint, decimalScale0] 中的一个"。
可能存在一些允许这种灵活性的 XML 架构格式,但我不知道,而且它可能在 Visual Studio 中支持不佳。
当我想验证关系中的表名和列名时,我遇到了 XSD 的第二个限制。在上面的示例中,您可以看到订单表和客户表之间的关系:FK_Order_Customer
。在column
元素中,name
属性必须是“外键”表(即包含此relationships
元素的表)中列的名称。在 XSD 中无法指定这一点。同样的情况也适用于primaryKeyTable
的name
属性,它必须是表之一的名称。虽然这可以在转换时进行验证,但如果 Visual Studio 在编辑文件时提供表/列名称的自动完成列表,并在无效条目下显示红色波浪线,那就更好了。
列类型
如前所述,列的类型用嵌套元素表示,在 XSD 中表示如下:
<xs:complexType name="column">
<xs:choice minOccurs="1" maxOccurs="1">
<!--Exact numerics-->
<xs:element name="bigint" type="bigint"/>
<xs:element name="numeric" type="decimal"/>
<xs:element name="numericScale0" type="decimalScale0"/>
<xs:element name="bit" type="bit"/>
<xs:element name="smallint" type="smallint"/>
<xs:element name="decimal" type="decimal"/>
<xs:element name="decimalScale0" type="decimalScale0"/>
Many more types...
这决定了一个类型为 column
的元素必须包含恰好 1 个元素,该元素可以是列出的任何类型。因此,每个 SQL Server 类型都匹配 XSD 中的一个复杂类型。每个适用于一个或多个 SQL Server 类型的参数集都有一个复杂类型。请注意,这不是一对一的对应关系,因为许多 SQL Server 类型共享相同的参数集。但是,我已尽可能尝试使参数具体化和验证。例如,这是 XSD 中的int
类型。
<xs:complexType name="int">
...
<xs:attribute name="default" use="optional" type="xs:int"/>
</xs:complexType>
请注意,default
属性(将转换为生成 SQL 中列的字面默认值)被强类型化,仅允许int
值。编辑文件时,这将有助于快速发现复制粘贴错误。其他类型的default
属性尽可能被强类型化。有时这需要定义进一步的简单类型;例如,SQL Server 中的money
类型存在一些内置 XSD 类型无法表达的限制。
<xs:simpleType name="moneydefault">
<xs:restriction base="xs:decimal">
<xs:totalDigits value="10"/>
<xs:fractionDigits value="4"/>
<xs:minInclusive value="-922337203685477.5808"/>
<xs:maxInclusive value="922337203685477.5807"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="money">
<xs:attributeGroup ref="withDefaultExpression"/>
<xs:attribute name="default" use="optional" type="moneydefault"/>
</xs:complexType>
在这里,moneyDefault
基于decimal
。这意味着decimal
类型的所有限制都适用,但更多限制也适用,例如 SQL Server 中money
类型的最小和最大有效值。
有时,默认列值是表达式而不是“字面量”。这通过defaultExpression
属性来表示,它只是一个字符串。在适当的时候,default
将在生成的 SQL 中被引用;但是,defaultExpression
是按原样复制的。对单个列同时使用default
和defaultExpression
没有意义。
在上面的money
类型的 XSD 中,您可以看到我使用了一个名为withDefaultExpression
的attributeGroup
元素。这为money
类型添加了defaultExpression
属性。属性组很有用,因为它允许您模块化设计和重用代码。您可以通过继承实现类似的效果,但 XSD 不支持多重继承,因此属性组是模拟它的好方法。
结论
如果您不熟悉 XSD,那么其中一些可能没有太多意义。如果是这样,我很抱歉,但重要的是您实际上不需要知道 XSD 如何工作就可以从中受益。如果您创建一个包含 XSD 和单个 XML 文件的 Visual Studio 项目,然后将第一个示例复制粘贴到 XML 中,那么您应该会发现,当您更改示例时,Visual Studio 会告诉您是否犯了错误。或者,您可以解压缩示例代码并使用我之前制作的解决方案。显然,这不会让您满意太久,因为您现在还不能真正用它做什么。但在下一篇文章中,我将介绍我如何开发了一个映射到新文件格式的 C# 代码模型,使您能够以简单且类型安全的方式以编程方式检查它。