65.9K
CodeProject 正在变化。 阅读更多。
Home

从 XML 生成 SQL 数据库架构 - 第二部分(代码模型)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (10投票s)

2010年11月29日

CPOL

11分钟阅读

viewsIcon

44068

downloadIcon

1142

系列文章的第 2 部分,共 3 部分,关于从 XML 文件格式生成 SQL。本部分描述了新文件格式的代码模型的开发。

引言

这是描述我为解决工作环境中的特定问题而进行的项目的三篇文章系列中的第二篇。第一篇文章解释了我如何不满意 SQL 的数据定义语言,并决定创建一个基于 XML 的替代方案。到文章结束时,我已经为新格式提出了一个 XSD。

我知道我需要某种方式将我的 XML 转换为 SQL,所以我做了一些谷歌搜索,最后我考虑了两种替代方案:XSLT 或定制程序。代码生成工具通常包含一个模板文件,其中包含某种“特殊”标记,用于划分嵌入代码的区域,例如在 ASP.NET aspx 文件中

<%=SomeProperty%> 

当页面被查看时,此标记将替换为页面类的`public`属性`SomeProperty`的值。XSLT 更极端,因为它包含被大量 XML 查询语言包围的输出片段。考虑这个 XSLT,它的设计目的正是我的项目所做的:将对象模型转换为 SQL 架构定义。我对这种代码生成方法的抱怨是,它本质上是只写代码。很难可视化预期的输出,因为它被嵌入代码所掩盖。Visual Studio 无法将代码片段识别为 SQL,因此它不会对其进行语法着色,如果您想生成包含任何 XML 特殊字符的代码,那么您必须对其进行转义。

因此,基于这些原因,我决定编写一个定制的命令行程序,它执行以下步骤

  • 读取命令行
  • 反序列化数据库架构
  • 构建代码模型
  • 根据模型生成 SQL

我将把所有其他内容留到最后一篇文章中,现在只讨论什么是代码模型,为什么它们是一个好主意以及我如何决定构建我的模型。

为什么要有一个代码模型?

在我使用它的意义上,“代码模型”只是指用某种编程语言编写的数据文件格式的表示。例如,网页上的脚本使用文档对象模型来动态检查和修改网页。当然,C# 已经有一个通用的XML 代码模型,我可以用它来读取我的文件格式,我甚至可以让它根据我的架构验证 XML。几行代码,我就有了整个文件在一个`XMLDocument`中,随时可以使用。

XmlDocument mySchema = new XmlDocument();
mySchema.Load("mySchema.xml");

然而,这种简单方法存在几个问题。当我编写代码时,为了使用我的数据,我必须知道我的 XML 文档的结构。这可能并不总是很明显,因为 XML 或 XSD 中存在怪癖和限制。我还必须知道 XML 文档中元素和属性的名称,并将它们作为常量字符串放在代码中的某个位置。例如

// Find an nchar column with a length > 10
foreach (XmlNode tableNode in mySchema.DocumentElement["tables"].ChildNodes)
{
	XmlElement tableElement = tableNode as XmlElement;
	if (tableElement != null)
	{
		foreach (XmlNode columnNode in tableElement["columns"].ChildNodes)
		{
			XmlElement columnElement = columnNode as XmlElement;
			if (columnElement != null)
			{
				// Remember that the type of a column is encoded 
				// in a child element rather than an attribute
				if (columnElement.FirstChild.Name == "nchar")
				{
					XmlElement ncharElement = 
						columnElement["nchar"];
					XmlAttribute ncharLengthAttribute = 
					ncharElement.Attributes["length"];
					int ncharLength = 1;
					if (ncharLengthAttribute != null)
					{
						ncharLength = int.Parse
						(ncharLengthAttribute.Value);
					}
					if (ncharLength > 10)
					{
						// Success
					}
				}
			}
		}
	}
}

此示例说明的下一个问题是类型转换。XML 代码模型不可能在编译时知道属性的类型,因此所有内容都必须作为`string`返回,并且必须手动转换。最终结果是冗长、模糊的代码,其中充满了假定的知识。以后也很难对文件格式进行任何更改,因为这些假设没有编译时检查。

代码模型通过隐藏所有假定知识并将其集中在一处来解决这些问题。它可以平滑 XML 中的结构工件,并将所有`string`转换为其 proper 类型。之后,对文件格式的更改可以通过保持代码模型不变并在加载时处理新格式来对客户端代码隐藏,或者作为在编译时中断的更改暴露给客户端代码,并且可以自信地修复。

代码模型实现

Microsoft 提供了一个名为*XSD.exe*的便捷工具,它将从 XSD 文件生成一个代码模型,该模型可以直接从任何使用该 XSD 的 XML 文件反序列化。出于以后会明确的原因,我的文件格式的 XSD 作为资源嵌入到程序集中(请参阅示例代码中的项目`DatabaseSchemaModelResource`)。此项目的构建后事件运行*XSD.exe*并生成文件*DatabaseSchemaModel.cs*,该文件包含在另一个项目`DatabaseSchemaModel`中。此项目被设置为`DatabaseSchemaModelResource`的依赖项,以便在编译`DatabaseSchemaModel`之前始终保持生成的代码文件最新。

请注意,XSD.exe 的位置可能因您的设置而异(请参阅此处),因此如果您在构建时遇到问题,这可能是原因。要解决此问题,您需要编辑 DatabaseSchemaModelResource 项目的构建后事件。

生成的代码在可能的情况下是类型安全的,并提供了一组很好的中间数据供您使用;比直接在 XML DOM 中摸索要好得多。但这对我来说仍然不够好。一方面,它仍然直接对应于 XML 的结构,另一方面,它对`xs:choice`处理得不是很好:只是将其视为`object`。最关键的是,在文件格式中,对象通过名称相互引用(例如,主键引用一个或多个列)。在代码模型中,如果对象直接链接(例如,如果主键对象直接引用一个或多个列对象),那会更好。所以我决定将这个生成的代码称为我的“原始”代码模型。我会把它对客户端隐藏起来,但它仍然可以为我节省大量的解析和验证工作,并提供一个类型安全的基础。

面向公众的代码模型的类都位于同名项目中的`DatabaseSchemaModel`命名空间中。模型的根类名为`Database`。在考虑如何从文件加载模型时,这个类可以直接从文件格式进行 XML 序列化似乎很自然。由于我想使用我的原始代码模型来完成繁重的工作,很明显我必须实现`IXmlSerializable`。以下是`Database`类的声明

...
[XmlRootAttribute(Namespace = http://olduwan.com/DatabaseSchemaModel.xsd, 
	IsNullable = false)]
[XmlSchemaProvider("MySchema")]
public class Database : IXmlSerializable
{
...

`XmlRootAttribute`告诉序列化程序如何写入文件的根节点,在这种情况下,它告诉它包含命名空间声明。`XmlSchemaProvider`属性是必需的,用于告诉序列化程序在读取 XML 时期望哪个架构。该架构实际上是通过一个`public static`方法提供给框架的,该方法在属性中引用。这就是我使用嵌入在`DatabaseSchemaModelResource`程序集中的 XSD 文件作为资源的地方

/// <summary>
/// This is the method named by the XmlSchemaProviderAttribute applied to the type.
/// </summary>
public static XmlQualifiedName MySchema(XmlSchemaSet xs)
{
	// This method is called by the framework to get the schema for this type.
	// The schema is embedded in an assembly as a resource.
	XmlSerializer schemaSerializer = new XmlSerializer(typeof(XmlSchema));
	XmlSchema s = (XmlSchema)schemaSerializer.Deserialize
		(new XmlTextReader(new StringReader
		(DatabaseSchemaModelResource.Resources.Schema)), null);
	xs.XmlResolver = new XmlUrlResolver();
	xs.Add(s);

	return new XmlQualifiedName("database", xmlNameSpace);
}

`ReadXml`的实现反序列化整个原始模型(由*XSD.exe*生成的代码),然后使用原始模型中的对象来构建代码模型。反序列化看起来像这样

public void ReadXml(System.Xml.XmlReader reader)
{
	// This is not very efficient but I don't expect performance to matter.
	// The idea here is to use the code that was generated from the xml schema
	// to actually read the xml, then extract the data into the hopefully more
	// usable data structures in this namespace.
	string outerXml = reader.ReadOuterXml();
	XmlSerializer serializer = new XmlSerializer(typeof(database));
	// Note this is a database object from the 'raw' code model
	database db = (database)serializer.Deserialize(new StringReader(outerXml));

	// Copy database attributes
	Name = db.name;
	if (db.ExampleFolderPath != null)
	{
		ExampleFolderPath = db.ExampleFolderPath;
	}
	if (db.SQLServerOutputPath != null)
	{
		SQLServerOutputPath = db.SQLServerOutputPath;
	}
	...

请注意,第一行将整个文件读取为`string`,然后将其传递给新的序列化器实例。这肯定效率不高,但我认为在实际用例中不太可能重要。这种方法的一个更大的问题是反序列化过程丢弃了有关源文件的任何信息,例如元素出现的行号。如果稍后在模型构建过程中发生任何错误,则无法在异常中描述问题的位置。我稍后会解释我如何处理这个问题,但为了继续`ReadXml`方法,下一步是遍历原始代码模型并为代码模型构建对象

...
    
// Walk raw code model and construct code model
ReadContext context = new ReadContext();

Tables = new IndexedList<string,Table>();
foreach (table rawTable in db.tables)
{
	Tables.Add(rawTable.name, new Table(context, rawTable));
}
Procedures = new IndexedList<string, Procedure>();
foreach (procedure rawProcedure in db.procedures)
{
	Procedures.Add(rawProcedure.name, new Procedure(rawProcedure));
}
...

正如您所看到的,表和过程存储在一个`IndexedList`中,它是一个有序容器,可以通过名称进行字典式查找。代码模型中的每个类都有一个`internal`构造函数,该构造函数将相应的原始模型类作为参数。例如,这是`Table`的内部构造函数的一部分

internal Table(
	...
	table raw
	)
{
	...
	Name = raw.name;
	foreach (column rawColumn in raw.columns)
	{
		Columns.Add(rawColumn.name, new Column(..., rawColumn));
	}
	...

构造函数从原始对象中获取其自身的描述,然后遍历原始`column`并创建代码模型`column`,通过传递原始数据。此模式在所有代码模型类中重复。为了查看所有内部构造函数共享的另一种模式,我将扩展相同的示例

internal Table(
	ReadContext context,
	table raw
	)
{
	context.ReadingTable = this;
	context.Stack.Push("Reading Table: " + raw.name);
	Name = raw.name;
	foreach (column rawColumn in raw.columns)
	{
		Columns.Add(rawColumn.name, new Column(context, rawColumn));
	}
	...
	context.ReadingTable = null;
	context.Stack.Pop();
}	

`ReadContext`类通过所有嵌套构造函数向下传递堆栈。通过在构造函数期间设置`ReadingTable`属性,所有嵌套构造函数都可以发现它们属于哪个`Table`。同样,`ReadContext`有一个用于报告进度的堆栈,每个嵌套构造函数都会向堆栈添加一些详细信息。如果在数据中发现错误,此堆栈的当前状态提供了一种向用户报告错误位置的方法。例如

Column does not exist: XXXXXXX
Reading Constraint: Primary
Reading Table: Categories

在构造函数结束时,`ReadingTable`属性被置空,并且堆栈上的详细信息被弹出,准备读取下一个`Table`。

为了完成本节,我将回到修复内部引用问题。问题是来自一个`Table`的关系可能引用尚未加载的另一个`Table`。解决这个问题的一种方法是将表的名称存储在类中。一旦整个模型加载完毕,就可以根据需要轻松地通过名称查找表,并将查找隐藏在属性获取器中。我倾向于不这样做,因为我不喜欢额外的数据成员四处悬挂,而且我不喜欢最终得到的稍微样板化的代码。相反,`ReadContext`类负责记住在反序列化期间需要解析的每个`Table`引用。例如,`Relationship`类有一个需要解析的“主键表”的引用。以下代码在其构造函数期间被调用

internal Relationship(
	ReadContext context,
	relationship raw
	)
{
	...
	context.ResolveTableReferences.Add(raw.primaryKeyTable.name, 
		delegate(Table table)
	{
		PrimaryKeyTable = table;
		foreach (relationshipColumn rawRelationshipColumn in 
			rawPrimaryKeyTableColumns)
		{
			PrimaryKeyColumns.Add(table.ResolveColumn
				(stackTrace, rawRelationshipColumn.name));
		}
	});
	...

`ResolveTableReferences`属性是一个`MultiDictionary`,这意味着它对于一个键可以有多个值。键是要解析的表的名称,这里添加的值是一个委托,它将在被调用时为表分配一个引用。使用匿名方法的优点是编译器会自动处理我使用`RelationShip`类的属性的事实,方法是存储一个指向`Relationship`实例的隐藏指针(它创建一个闭包)。稍后,在所有表都反序列化之后,以下代码在`Database.ReadXml`的末尾被调用

	...
	foreach (var resolveTableReferenceContainer in context.ResolveTableReferences)
	{
		Table table = Tables[resolveTableReferenceContainer.Key];
		foreach (var resolveTableReference in 
			resolveTableReferenceContainer.Value)
		{
			resolveTableReference(table);
		}
	}
}

每个键都是要解析的表名,对应的值是包含委托的容器。一旦查找了表,容器中的每个委托都会被调用,并将表作为参数传递。

除一个例外,代码模型中的所有类都映射到 XSD 中的类型,最终映射到数据库架构中的元素,因此我不会详细介绍它们。例外是在`Column`类中。在 XSD 中,有一个嵌套元素描述了列的类型特定属性(例如,`length`)。在代码模型中,我选择将这些属性“扁平化”到`Column`类中。这是一个有争议的决定,因为它确实意味着任何给定的`Column`实例中都有几个不相关的属性。然而,我的感觉是,当您使用列的类型特定属性时,您已经知道它们是什么(例如,`nchar`列具有`length`),因此这是一个可以容忍的折衷。以下是`Column`的内部构造函数的代码

internal Column(
	ReadContext context,
	column raw
	)
{
	Name = raw.name;
	Type = raw.ItemElementName.ToString();
	if (raw.allowNullsSpecified)
	{
		AllowNulls = raw.allowNulls;
	}
	switch (raw.Item.GetType().FullName)
	{
		case "DatabaseSchemaModel.Raw.bigint":
			bigint rawBigInt = raw.Item as bigint;
			if (rawBigInt.defaultSpecified)
			{
				Default = rawBigInt.@default.ToString();
			}
			else
			{
				Default = rawBigInt.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.int":
			@int rawInt = raw.Item as @int;
			if (rawInt.defaultSpecified)
			{
				Default = rawInt.@default.ToString();
			}
			else
			{
				Default = rawInt.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.smallint":
			smallint rawSmallInt = raw.Item as smallint;
			if (rawSmallInt.defaultSpecified)
			{
				Default = rawSmallInt.@default.ToString();
			}
			else
			{
				Default = rawSmallInt.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.tinyint":
			tinyint rawTinyInt = raw.Item as tinyint;
			if (rawTinyInt.defaultSpecified)
			{
				Default = rawTinyInt.@default.ToString();
			}
			else
			{
				Default = rawTinyInt.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.decimal":
			@decimal rawDecimal = raw.Item as @decimal;
			if (rawDecimal.precision != null)
			{
				Precision = int.Parse(rawDecimal.precision);
			}
			if (rawDecimal.scale != null)
			{
				Scale = int.Parse(rawDecimal.scale);
			}
			if (rawDecimal.defaultSpecified)
			{
				Default = rawDecimal.@default.ToString();
			}
			else
			{
				Default = rawDecimal.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.decimalScale0":
			decimalScale0 rawDecimalScale0 = raw.Item as decimalScale0;
			Scale = 0;
			if (rawDecimalScale0.precision != null)
			{
				Precision = int.Parse(rawDecimalScale0.precision);
			}
			if (rawDecimalScale0.defaultSpecified)
			{
				Default = rawDecimalScale0.@default.ToString();
			}
			else
			{
				Default = rawDecimalScale0.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.float":
			@float rawFloat = raw.Item as @float;
			if (rawFloat.mantissaBits != null)
			{
				MantissaBits = int.Parse(rawFloat.mantissaBits);
			}
			if (rawFloat.defaultSpecified)
			{
				Default = rawFloat.@default.ToString();
			}
			else
			{
				Default = rawFloat.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.real":
			@real rawReal = raw.Item as @real;
			if (rawReal.defaultSpecified)
			{
				Default = rawReal.@default.ToString();
			}
			else
			{
				Default = rawReal.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.variablePrecisionTime":
			variablePrecisionTime rawVariablePrecisionTime = 
				raw.Item as variablePrecisionTime;
			if (rawVariablePrecisionTime.fractionalSecondsPrecision != null)
			{
				FractionalSecondsPrecision = int.Parse
				(rawVariablePrecisionTime.fractionalSecondsPrecision);
			}
			if (rawVariablePrecisionTime.@default != null)
			{
				Default = Quote(rawVariablePrecisionTime.@default);
			}
			else
			{
				Default = rawVariablePrecisionTime.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.char":
			@char rawByte = raw.Item as @char;
			if (rawByte.length != null)
			{
				if (rawByte.length == "max")
				{
					MaxLength = true;
				}
				else
				{
					Length = int.Parse(rawByte.length);
				}
			}
			if (rawByte.@default != null)
			{
				Default = Quote(rawByte.@default);
			}
			else
			{
				Default = rawByte.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.nchar":
			nchar rawNChar = raw.Item as nchar;
			if (rawNChar.length != null)
			{
				if (rawNChar.length == "max")
				{
					MaxLength = true;
				}
				else
				{
					Length = int.Parse(rawNChar.length);
				}
			}
			if (rawNChar.@default != null)
			{
				Default = Quote(rawNChar.@default);
			}
			else
			{
				Default = rawNChar.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.bit":
			bit rawBit = raw.Item as bit;
			if (rawBit.defaultSpecified)
			{
				Default = rawBit.@default ? "1" : "0";
			}
			else
			{
				Default = rawBit.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.smallmoney":
			smallmoney rawSmallMoney = raw.Item as smallmoney;
			if (rawSmallMoney.defaultSpecified)
			{
				Default = rawSmallMoney.@default.ToString();
			}
			else
			{
				Default = rawSmallMoney.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.money":
			money rawMoney = raw.Item as money;
			if (rawMoney.defaultSpecified)
			{
				Default = rawMoney.@default.ToString();
			}
			else
			{
				Default = rawMoney.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.parameterlessStringType":
			parameterlessStringType rawParameterlessStringType = 
					raw.Item as parameterlessStringType;
			if (rawParameterlessStringType.@default != null)
			{
				Default = Quote(rawParameterlessStringType.@default);
			}
			else
			{
				Default = 
				    rawParameterlessStringType.defaultExpression;
			}
			break;

		case "DatabaseSchemaModel.Raw.uniqueidentifier":
			uniqueidentifier rawUniqueIdentifier = 
					raw.Item as uniqueidentifier;
			if (rawUniqueIdentifier.@default != null)
			{
				Default = Quote(rawUniqueIdentifier.@default);
			}
			else
			{
				Default = rawUniqueIdentifier.defaultExpression;
			}
			break;
	}

	withIdentity rawWithIdentity = raw.Item as withIdentity;
	if (rawWithIdentity != null && rawWithIdentity.identity != null)
	{
		Identity = new Identity(rawWithIdentity.identity);
	}
}

如您所见,`switch`用于识别原始代码模型中嵌套元素的实际类型。然后将类型特定属性复制到`Column`中,并且不相关的属性保持未初始化。此方法的末尾还有一件事值得注意。`withIdentity`是一个接口,对应于`withIdentity`XSD组。*XSD.exe*不生成此接口或承认在 XSD 中使用相同组的类型之间的关系。但是,它会生成`partial`类,这使得可以手动添加接口。这在“*DatabaseSchemaModelExtended.cs*”中完成

namespace DatabaseSchemaModel.Raw
{
	/// <summary>
	/// This interface corresponds to the 'withIdentity' group in the XSD
	/// </summary>
	public interface withIdentity
	{
		identity identity
		{
			get;
		}
	}

	public partial class bigint : withIdentity
	{ }

	public partial class tinyint : withIdentity
	{ }

	public partial class @int : withIdentity
	{ }

	public partial class smallint : withIdentity
	{ }

	public partial class decimalScale0 : withIdentity
	{ }
}

使用代码模型

与原始 XML 示例相反,使用代码模型加载数据库架构看起来像这样

XmlSerializer serializer = new XmlSerializer(typeof(Database));
Database database = (Database)serializer.Deserialize(new StreamReader("mySchema.xml"));

而查找长度大于 10 的 nchar 的( admittedly 相当人为的)示例如下所示

foreach (Table table in database.Tables.Values)
{
	foreach (Column column in table.Columns.Values)
	{
		if (column.Type == "nchar" && column.Length > 10)
		{
			// Success
		}
	}
}

我发现这样更具可读性,并且我相信意图更清晰。

结论

尽管构建代码模型需要适度的前期投入,但由此产生的客户端代码更简洁,更易于维护。在本系列的下一篇也是最后一篇文章中,我将通过一些 SQL 代码生成来演示这一点,并整合执行完整转换的控制台应用程序。

历史

  • 2010年11月29日:首次发布
© . All rights reserved.