通用数据库访问






4.93/5 (16投票s)
通用数据库访问模型,该模型概括了与任何特定数据库系统的交互。
摘要
数据管理是一个常见需求,但实现方式因数据库系统和查询而异,这导致我们在软件开发过程中需要处理大量不同的情况。我们现在将通过引入一个通用数据库访问模型来解决这个问题,该模型将概括与任何特定数据库系统的交互。
所涉问题
1.1 问题
在软件开发过程中,相当多的时间都花在处理数据访问层上。这一层代表了主应用程序与特定数据库系统之间的交互功能。这一层应该能够从相关数据库发送和接收数据。
如果我们的应用程序需要支持不同的数据库系统,那么开发这样的层会变得更加困难。此外,命令本身的定义方式也使得它们需要接收不同数量的参数。因此,我们的代码需要相应地进行更改。
这意味着,对于每一个微小的更改和不一致,我们都需要复制大量的代码来解决新创建的问题。因此,代码量将随着我们打算执行的命令数量的增加而增加。
为了找到这个问题的通用解决方案,我们将提出以下解决方案。
1.2 解决方案
由于我们正在处理针对 .NET 平台的解决方案,我们将以 ADO.NET 框架作为起点,它是 .NET 平台用于与不同数据库系统交互的基础框架[1]。
此外,我们还将考虑到每个数据库系统都有其基于 ADO.NET 框架的自己的程序集,我们的应用程序使用该程序集与特定的数据库交互。因此,我们的解决方案将要求我们拥有特定于数据库的程序集才能正常运行。
下一步将是考虑所有可能执行的命令实例。现在让我们将所有可能的 SQL 命令分为两种类型:查询类型和非查询类型命令。
查询类型命令是任何查询数据库以返回数据库中存储的数据的命令。另一方面,非查询类型命令是所有其他不查询数据库数据,而是修改数据库中数据的命令。
此外,由于我们可以使用 ADO.NET 框架执行简单的基于文本的 SQL 命令,或者执行存储在数据库中的 SQL 存储过程,我们可以进一步将可能的命令分为以下几种:Select、Insert、Update 和 Delete 命令。
让我们提一下,Select 命令是查询类型命令,而其余的都是非查询类型命令。最后,我们可以说我们将使用 Select、Insert、Update 和 Delete 名称来表示简单的基于文本的 SQL 命令,而 Query 和 Non Query 名称将用于表示相应类型的 SQL 存储过程。
因此,我们的目标将是构建一个 .NET 程序集,该程序集能够执行上述命令。现在让我们来看看如何实现这一点。
审查解决方案
2.1 要求
上述解决方案需要满足以下五个先决条件:
- 与任何数据库交互
- 执行任何 SQL 命令
- 直接使用业务对象
- 无需重新编译即可配置
- 在一行代码中执行查询
要求 1
第一个要求显然是为了让我们能够与任何数据库系统交互。如果无法做到,我们的解决方案原则上就不是通用的。
要求 2
第二个要求是确保数据库中的数据可以完全提取和修改。换句话说,我们必须能够从数据库中查询任何数据,并且可以修改其中的任何数据。
要求 3
第三个要求是为了让我们的调用类保持最简单的形式。如果我们的类直接基于 SQL 命令可能采用的类型和参数数量,那么我们将需要创建可能无限数量的类来执行所有可能的 SQL 命令。因此,我们将转而使用业务对象作为单个参数,从中提取数据。
要求 4
第四个要求是为了确保第一个和第二个要求得到正确满足。如果程序集需要我们重新编译才能使用事先未考虑到的不同数据库系统,那么该程序集本身就不是通用的。不同 SQL 命令的执行也是如此。
要求 5
第五个也是最后一个要求是确保我们的解决方案简单,并且我们能够高效地执行命令,而无需对程序集类本身进行任何进一步修改。
在我们开始介绍将作为我们问题解决方案的通用模型之前,让我们先看看一个可能能够解决上述所有要求的候选方案。
此解决方案来自 Microsoft Enterprise Library 6.0[2],它是一组应用程序块,由多个程序集组成。这些程序集用于解决开发人员在软件开发过程中面临的重复问题。
该库中的一个块是数据访问应用程序块。此块用于与不同的数据库系统交互。现在让我们仔细看看库本身中的以下代码。
// (1.1)
private static SqlCommand CheckIfSqlCommand(DbCommand command)
{
SqlCommand sqlCommand = command as SqlCommand;
if (sqlCommand == null)
throw new ArgumentException(Resources.ExceptionCommandNotSqlCommand, "command");
return sqlCommand;
}
不幸的是,正如我们从前面的代码示例中可以看到的,数据访问应用程序块未能满足其作为通用数据库访问的第一个先决条件。通用模型的概念被显式使用特定于数据库的类型(即 SqlCommand
类型)所否定。
此类型仅用于表示 Microsoft SQL Server 数据库系统的 SQL 命令。如果我们需要使用另一个数据库,那么我们还需要指定另一个特定于数据库的类型。这将不允许我们在不实际重新编译代码的情况下,使用当前不存在但我们希望在将来可用时使用的数据库。
因此,数据访问应用程序块不适用于解决此问题。
2.2 通用模型
现在让我们转向通用模型本身。所涉模型可以简单地描述为一个程序集,它将拦截主调用应用程序的调用,选择适当的特定于数据库的程序集和类型以执行预定义的 SQL 命令,然后在正确的数据库上执行相关命令。
此操作可以通过以下图表表示。
我们可以观察到六个不同的节点,它们代表模型中程序流的各个点。第一个节点代表调用通用数据库访问程序集的代码点,是主应用程序,它将用于向程序集发送数据,并在程序集执行所需命令后从程序集接收数据。
第二个节点是通用数据库访问程序集本身,它由调用应用程序调用,由带有箭头的实线表示。该程序集现在将进行两次调用,我们可以通过观察图表的下部看到这一点。
调用将分支到特定于数据库的设置 XML 文件和 SQL 命令 XML 文件。然后,此信息将发送到在特定于数据库的设置 XML 文件中定义的特定于数据库的 ADO.NET 程序集,并且将执行在 SQL 命令 XML 文件中定义的命令。
然后,该命令将在预定义的数据库系统上执行,该数据库系统代表我们的最终节点,通用数据库访问程序集向其发出调用。
2.3 实现要求
为了实现前面定义的五个要求,我们需要利用一个系统,通过它我们可以以最简单的方式修改我们的工作程序集。这可以通过使用基于文本的,即 XML 配置文件来更改程序集的工作方式,具体取决于我们正在处理的情况。
为了更深入地理解这个概念,让我们提出第一个先决条件的解决方案,即允许我们的程序集与任何数据库系统一起工作。在此之前,让我们注意,为了真正被认为是通用的,程序集至少在理论上必须能够处理目前不存在的数据库系统。
换句话说,它应该与尚未存在但一旦创建,并且一旦为该数据库生成了适当的 ADO.NET 程序集,我们的程序集将需要无缝集成,而无需重新编译。
因此,现在让我们开始介绍前面提出的要求的解决方案。
解决方案 1
我们首先介绍将用于实现此要求的 XML 文件。
// (1.2)
<?xml version="1.0" encoding="utf-8"?>
<root>
<Connection>Data Source=XE; User Id=SYSTEM; Password=mypassword;</Connection>
<Prefix>:</Prefix>
<ParameterAssembly>Oracle.DataAccess.dll</ParameterAssembly>
<ParameterType>Oracle.DataAccess.Client.OracleParameter</ParameterType>
<Property>
<Name>Adapter</Name>
<Assembly>Oracle.DataAccess.dll</Assembly>
<Type>Oracle.DataAccess.Client.OracleDataAdapter</Type>
</Property>
<Property>
<Name>Connection</Name>
<Assembly>Oracle.DataAccess.dll</Assembly>
<Type>Oracle.DataAccess.Client.OracleConnection</Type>
</Property>
<Property>
<Name>Command</Name>
<Assembly>Oracle.DataAccess.dll</Assembly>
<Type>Oracle.DataAccess.Client.OracleCommand</Type>
<Data>
<Name>BindByName</Name>
<Value>True</Value>
</Data>
</Property>
<Property>
<Name>Builder</Name>
<Assembly>Oracle.DataAccess.dll</Assembly>
<Type>Oracle.DataAccess.Client.OracleCommandBuilder</Type>
</Property>
</root>
我们现在将看到特定于数据库的设置 XML 文件,该文件位于将用于存储我们的程序集的文件夹中的 GenericDataBaseAccess 文件夹中。此 XML 文件名为 GenericDataBase.xml,这是一个要求,因此将用作主要设置文件。
此外,让我们注意到这是一个 Oracle 数据库特定的设置,但要确切了解此 XML 文件对我们有什么用,让我们从观察其元素开始。
第一个元素是 Connection 元素,它将用于指定我们的程序集将用于连接数据库系统的连接字符串。第二个元素是 Prefix 元素,此元素将用于定义一个特定于数据库的字符,该字符将表示在数据库环境中用于表示 SQL 命令中的参数的前缀。
接下来的 ParameterAssembly 元素将用于定义一个程序集的名称,我们将从该程序集使用类型来为我们的 SQL 命令创建参数。
ParameterType 元素是前面提到的类型名称的定义位置。
我们还可以注意到我们有四个复杂的 Property 元素。这些元素包含程序集将用于与数据库交互的值。每个 Property 元素都以 Name、Assembly 和 Type 元素作为其子元素,并带有可选的 Data 元素。
Name 元素将指示在运行时将由实例加载的我们程序集中定义的哪个属性。我们程序集中属性的名称对应于 Property 元素的 Name 元素的名称。将从中提取类型的程序集在 Assembly 元素中定义,而在运行时将实例化的类型在 Type 元素中定义。
更具体地说,我们只需要提及 Adapter Property 用于定义将使用的数据适配器,Connection Property 将定义一个实例,该实例将用于表示与我们数据库的连接,而 Command Property 将表示用于存储基于文本的 SQL 命令或要执行的 SQL 存储过程名称的类型。
最后,Builder 属性将定义用于从存储过程派生参数的类型。
我们还可以注意到 Command Property 中定义了一个可选的 Data 元素。我们应该提到 Data 元素的数量是无限的。这些元素是根据所涉数据库系统的要求定义的。它们用于进一步修改在运行时实例化的类型,以便它们正确执行命令。
Data 元素的 Name 元素表示需要设置的属性,而 Value 元素表示要设置的值。由于这些表示布尔值,因此唯一可能的值是 True 或 False。
现在需要一些进一步的特定于数据库的信息。
由于 Microsoft Access 数据库不支持存储过程,因此 Builder Property 应完全留空。此外,如果未使用 Builder Property,则应删除上述 Oracle 设置中定义的 Data 元素。
最后,如果使用 PostgreSQL 数据库系统,我们应该完全留空 Builder Property,因为此数据库系统无法正确从存储过程中派生参数。此解决方案现在理论上能够与任何数据库系统交互。
它唯一的先决条件是存在适当的 ADO.NET 程序集,我们可以从中利用上述类型与数据库交互,从而成功满足了第一个要求。
解决方案 2
继续讨论第二个要求,即执行任何 SQL 命令的能力,我们将提供以下 XML 示例作为我们的解决方案。
// (1.3)
<?xml version="1.0" encoding="utf-8"?>
<root>
<Command>
SELECT * FROM Songs
WHERE Artist = 'Tina Turner'
OR Artist = 'Manu Chao';
</Command>
</root>
这个简单的 XML 结构将用于执行简单的基于文本的 SQL 命令或 SQL 存储过程。如果是一个简单的 SQL 命令,正如我们从上面的示例中看到的,Command 元素应该包含命令本身的文本。
如果不是这种情况,并且如果我们需要执行 SQL 存储过程,只需用我们需要执行的存储过程的名称填充 Command 元素即可。
通过这样做,我们也成功完成了第二个先决条件,因为我们将能够轻松地通过文本或存储过程,使用 XML 文件加载任何 SQL 命令,并从我们的程序集中执行它们。
解决方案 3
第三个先决条件是我们可以直接使用业务对象。这个先决条件实际上将允许我们通过定义业务对象中的属性来表示 SQL 命令中使用的参数,从而使用业务对象来表示我们的 SQL 命令。
因此,为了避免在代码中手动添加每个参数,我们将简单地使用业务对象来保存参数的值。由于业务对象将与程序集本身交互,因此将使用反射机制来提取业务对象中的每个属性,并将其作为参数添加到我们的 SQL 命令中。
为了使其正常工作,属性的名称需要与参数的名称相同,因此,这将使我们能够成功解决构建通用数据库访问模型的第三个先决条件。
此外,我们应该提到业务对象和命令 XML 文件应该具有相同的名称。此约定与数据交换机制[3]中已使用的约定相同。
解决方案 4
第四个要求是,如果我们需要与不同的数据库系统交互或执行不同的 SQL 命令,则无需重新编译我们的程序集。通过观察前两个解决方案,这很容易实现。
显然,XML 文件可以通过手动轻松修改,无需重新编译程序集。如果我们需要在另一个数据库系统上工作,我们可以轻松地使用适当的数据修改特定于数据库的设置 XML 文件,这些数据将使用反射在运行时加载。
同样,不同的 SQL 命令可以添加到用于存储 SQL 命令的 XML 文件中,也无需重新编译主程序集。这理论上将允许我们在未来甚至与当前不存在的数据库系统一起工作。
解决方案 5
第五个也是最后一个要求是在一行代码中执行每个 SQL 命令。这将使我们的程序集能够以最小的额外复杂性添加到程序中。
我们还可以说,通过观察第三个要求的解决方案,通过使用业务对象和使用反射来提取它们并将它们作为参数添加到 SQL 命令中,我们不需要手动添加参数。
由于两个不同的命令可以有不同数量的参数,我们无法定义一个单一的类来保存相关的参数。有些命令根本没有参数,因此这更是如此。
因此,我们决定用六个不同的类来表示所有可能的 SQL 命令。每个类都将业务对象的类型作为其通用参数,并将业务对象本身的实例作为其单个参数。
这将以以下定义的形式呈现。
// (1.4)
new GenericCommand<BusinessObject>(new BusinessObject() { Property = ... } );
这显然意味着任何此类命令都可以通过一行代码轻松执行。因为命令可以很容易地实例化,其通用定义是业务对象的类型,其参数的容器是业务对象的实例,形式为其包含的属性。
因此,通过所提出的五种不同的解决方案实现了所有五个要求,很容易得出我们事实上已经提出了一个通用数据库访问模型。既然如此,现在我们只需将该模型转化为一个可运行的机制,我们将据此进行。
通用数据库访问模型
3.1 执行过程的阶段
通用数据库访问执行 SQL 命令的过程分为四个阶段。让我们列出相关阶段,然后更详细地解释它们。1. - 查询类型确定
2. - 数据库特定类型确定
3. - 查询执行
4. - 参数创建
阶段 1
第一阶段涉及确定将用于执行相关 SQL 命令的类型。
阶段 2
第二阶段涉及确定将实际用于执行命令的特定于数据库的类型。
阶段 3
第三阶段是最复杂的阶段,一旦提供了将执行 SQL 命令的相关类型,我们就会实际启动 SQL 命令执行过程。
阶段 4
第四个也是最后一个阶段是参数创建过程,它在第三阶段本身内执行。这也是我们需要确定类型,并创建将由我们的程序集使用的参数实例的地方。
模型中 SQL 命令执行过程的上述阶段将在本章的以下段落中逐一详细说明。
3.2 查询类型确定
现在让我们转到执行的第一阶段,观察一个用于查询数据库的简单 Select 语句。现在我们将检查通用数据库访问如何处理此类命令的执行。换句话说,我们将逐步了解程序流程,描述用于执行通用 Select 命令的每个类的内部工作原理。因此,为了开始我们的阐述,我们转向在我们的通用数据库程序集中定义的
Select<T>
类。
// (1.5)
public class Select<T> : AbstractProcedure<DataSet, ISelect>
{
public Select(T c)
{
Results = Procedure.Invoke<T>(c);
}
}
Select<T>
类是我们的程序集入口点之一。它用于执行简单的 SQL Select 命令。泛型类型 T
代表我们的业务对象的类型,此类型也将用于获取包含需要执行的 SQL 命令文本的相应 XML 文件。此外,类型为 T
的参数 c
是我们的业务对象的一个实例,其中包含将在 SQL 命令中使用的参数。
我们还可以注意到,我们的类派生自 AbstractProcedure<T, K>
类,其泛型类型使用 DataSet
和 ISelect
类型封闭。
当前类定义的构造函数,正如我们清楚地看到的,只关心调用 Procedure
属性中定义的 Invoke<T>(T)
方法,并将该方法的结果添加到 Results
属性中。
由于 `Procedure` 属性是用于执行 SQL 命令的类的实例,并且我们通过调用 `Invoke<T>(T)` 方法并向其传递类型为 `T` 的业务对象来执行它,我们实际上所做的是执行了一个 SQL Select 命令,返回了已选择的数据并将其放入 `Results` 属性中。AbstractProcedure<T, K>
类负责前面操作的细节。通过使用 DataSet
和 ISelect
类型封闭其泛型类型,我们确定了我们执行命令的返回类型将是 DataSet
类型,并且用于执行此命令的类将派生自 ISelect
接口。
为了详细检查此过程,让我们仔细看看以以下方式定义的 **AbstractProcedure<T, K>
** 类。
// (1.6)
public abstract class AbstractProcedure<T, K>
{
public T Results { get; set; }
protected K Procedure { get; set; }
public AbstractProcedure()
{
Procedure = new Activate<K>(true).Instance;
}
很容易看出,此类的定义将向 `Procedure` 属性添加一个类型 `K` 的实例,该属性表示用于执行 SQL 命令的类。将添加到属性的实例将由 `Activate<T>` 类根据泛型类型 `K` 生成。
要了解这是如何实现的,让我们检查 Activate<T>
类。
// (1.7)
class Activate<T> : ActivateSource<T>
{
public Activate()
{
Instance = (T)Activator.CreateInstance(new TypeFor<T>().Type);
}
public Activate(params object[] parameters)
{
Instance = (T)Activator.CreateInstance(new TypeFrom<GenericDataBase>().Type, parameters);
}
public Activate(bool value)
{
Instance = (T)Activator.CreateInstance(new TypeFromResource<T>().Type);
}
Activate<T>
类有三个重载构造函数,并从 ActivateSource<T>
类派生其 Instance
属性。父类的泛型类型 T
负责确定 Instance
属性的类型。
另一方面,将根据类型 `T` 确定要实例化并添加到 `Instance` 属性的类型。我们现在应该注意到,为我们提供类型的机制已从数据交换机制[3]中的核心功能中获取并修改。
由于我们之前使用一个 `bool` 参数调用了构造函数,因此我们现在只关注这个构造函数。所以,正如我们所看到的,构造函数在被调用时将创建一个由 `TypeFromResource<T>` 类提供的类型的新实例,并将其值设置为 `Instance` 属性。
为了理解这种机制,我们来研究一下 TypeFromResource<T>
类。
// (1.8)
class TypeFromResource<T> : AbstractResourceType<T>
{
public TypeFromResource() : base(null, null) { }
protected override void GetDataFromElements(string assembly, string type)
{
Type = Assembly.GetExecutingAssembly().GetType(GetConfig("Type"));
}
}
TypeFromResource<T>
类用于从将在运行时实例化的资源中提取类型名称。
我们还可以看到此类的重写方法,该方法派生自其父类,名为 GetDataFromElements(string, string)
。在这里,类型直接由执行程序集提供。我们需要的特定类型由 GetConfig(string)
方法确定。
为了理解此方法,我们将查看 AbstractResourceType<T>
类。
// (1.9)
abstract class AbstractResourceType<T> : Config<ResourceConfig<T>>
{
public Type Type { get; set; }
public AbstractResourceType(string assembly, string type)
{
GetDataFromElements(assembly, type);
}
protected virtual void GetDataFromElements(string assembly, string type)
{
Type = new TypeFromAssembly(GetConfig(assembly), GetConfig(type)).Type;
}
}
`AbstractResourceType<T>` 类用于为我们提供需要在运行时实例化的类型。这些类型的名称位于资源中,因此需要提取。我们再次可以看到这里使用了 `GetConfig(string)` 方法。
这次它用于提供将在运行时加载的程序集的名称以及要实例化的类型。此方法在名为 `Config<T>` 的父类中定义,我们接下来将对其进行观察。
// (2.0)
abstract class Config<T> where T : IConfigurable, new()
{
XElement XmlConfig { get; set; }
public Config()
{
XmlConfig = new T().GetConfig();
}
protected string GetConfig(string source)
{
return XmlConfig.Element(source).Value;
}
Config<T>
类将为我们提供一个配置类,该类将用于从资源或位于通用数据库访问程序集之外的 XML 文件中获取类型。
由于泛型类型 `T` 派生自 `IConfigurable` 接口,它表示一个在运行时实例化类型所需数据的类。为了促进这一点,将在构造函数中执行适当的操作。将从泛型类型 `T` 的新实例中调用 `GetConfig()` 方法,该方法将返回 `XElement` 类型中包含的 XML 文件。
我们还可以看到 `GetConfig(string)` 方法,正如我们现在所理解的,它返回在构造函数中获取的 XML 文件,并返回其一个元素的值,该元素的名称与参数源的名称对应。
我们还应该记住,在当前情况下,我们对程序集资源中包含的类型感兴趣。
上述原因解释了为什么在前面的定义中,`Config<T>` 类中的泛型类型 `T` 已被 `ResourceConfig<T>` 类封闭,我们接下来将对其进行研究。
// (2.1)
class ResourceConfig<T> : IConfigurable
{
public XElement GetConfig()
{
var reader = new GetStream<T>().Reader;
var config = XDocument.Parse(reader.ReadToEnd()).Root;
reader.Close();
return config;
}
}
`ResourceConfig<T>` 类将用于封闭前一个类定义中的泛型类型 `T`,它用于获取指定的资源流(一个 XML 文件),解析其内容并将其作为 `XElement` 类型实例返回。
要了解如何提供正确的流,我们来研究 GetStream<T>
类。
// (2.2)
class GetStream<T>
{
public StreamReader Reader { get; set; }
public GetStream()
{
Reader = new StreamReader(GetType().Assembly.GetManifestResourceStream(new LastType<T>().Name));
}
}
简单地看一眼当前类的定义就足以告诉我们,流将由 `Reader` 属性提供,该属性通过访问当前类型并从程序集中返回流来加载。特定流由 `LastType<T>` 类及其 `Name` 属性确定。
为了理解我们所需的流名称是如何提供的,我们需要进一步检查 `LastType<T>` 类的定义,如下所示。
// (2.3)
class LastType<T>
{
public string Name { get; set; }
public LastType()
{
var name = typeof(T).FullName;
var index = name.IndexOf(".");
Name = name.Remove(index, name.IndexOf(".", index) - 1)
.Insert(index, ".Resources.") + ".xml";
}
}
类似于数据交换机制[3]的工作原理,相关的 XML 文件将通过转换 `FullName` 属性(即泛型类型 `T` 的命名空间和类名)从资源中提供,并将其转换为指向程序集内的资源文件夹。
此外,将在路径中添加后缀“.xml”以指示我们正在打开一个 XML 文件。因此,通过此机制,我们能够根据泛型类型 `T` 从资源中加载 XML 文件。
由于此处的类型 `T` 已在定义 (1.5) 中定义为类型 `ISelect`,因此我们将从资源中打开 *ISelect.xml* 文件。
ISelect.xml 文件定义如下。
// (2.4)
<?xml version="1.0" encoding="utf-8"?>
<root>
<Type>GenericDataBaseAccess.Generic.Commands.GenericSelect</Type>
</root>
该定义非常简单,包含一个 Type XML 元素,其中包含我们需要在运行时实例化以执行 Select SQL 命令的类型的完整名称。因此,有了这个最终定义,我们已经完成了阐述的第一部分。
我们已成功获取将用于执行 SQL 命令的类型名称。下一部分将涉及特定于数据库的类型及其在执行 SQL 命令中的作用。
3.3 数据库特定类型确定
我们现在进入执行过程的第二阶段。这是通用数据库访问内部工作原理的核心部分。第二阶段涉及确定执行 SQL 命令所需的特定于数据库的类型。因此,让我们仔细看看
GenericSelect
类。
// (2.5)
class GenericSelect : CoreQuery, ISelect
{
public DataSet Invoke<T>(T c)
{
SetParameters<T>(c, CommandType.Text);
Execute(() => new ExecuteQuery(Adapter, Command, Data));
return Data;
}
GenericSelect
类在运行时实例化,用于执行 Select SQL 命令。
我们可以看到,我们的类既派生自 CoreQuery
类,也派生自 ISelect
接口。它派生自 CoreQuery
类是因为 Select SQL 命令是 SQL 命令的查询类型,而 CoreQuery
类是包含执行此类命令所需的所有功能的类。
现在让我们注意到 `Invoke<T>(T)` 方法是在定义 (1.1) 中调用的。因此,它的泛型类型 `T` 代表我们的业务对象,参数 `c` 是它的实例。
派生方法 `SetParameters<T>(T, CommandType)` 用于设置 SQL 命令中使用的参数。通过将 `CommandType.Text` 枚举传递给该方法,我们表明该命令将采用文本形式,而不是存储过程。
另一方面,`Execute(Action)` 方法用于执行 SQL 命令。我们添加 `ExecuteQuery` 类的一个新实例,并向其添加执行 SQL 命令所需的所有必需参数。
最后,该方法将返回 `Data` 字段,其中包含从数据库中选择的所有数据。
接下来我们将关注 `ExecuteQuery` 类,并介绍其定义。
// (2.6)
class ExecuteQuery
{
public ExecuteQuery(DbDataAdapter adapter, DbCommand command, DataSet data)
{
adapter.SelectCommand = command;
adapter.Fill(data);
}
}
正如我们所看到的,这个类只关心用 `command` 参数中包含的值设置 `adapter` 参数,这将导致 `adapter` 执行一个特定的 Select 命令,该命令包含在 **`command`** 参数中。命令执行后,`data` 参数将填充从数据库中选择的数据。
现在让我们来看看这个类将在什么上下文下执行。为此,我们需要观察 CoreQuery
类。
// (2.7)
abstract class CoreQuery : GenericDataBase
{
protected DataSet Data = new DataSet();
protected void Execute(Action method)
{
try
{
method();
}
catch (Exception e)
{
new ActivateException(e);
}
}
}
此类的定义用作所有将用于执行查询类型 SQL 命令的类的模板。我们可以看到它包含一个名为 `Data` 的 `DataSet` 类型字段定义。这是从数据库中选择的数据的存储位置。
`Execute(Action)` 方法将被调用以执行指定的查询类型命令。它在 `try-catch` 块内的安全上下文中执行。在 `try` 块中,我们尝试执行一个包含 SQL 命令执行逻辑的委托。如果执行失败,我们将立即转移到 `catch` 块,在那里我们实例化一个 `ActivateException` 类型的类,并向其传递包含异常的参数 `e`。
由于此特定执行流程中有两种可能的结果,即成功和不成功,因此我们需要详细解释这两种流程。
我们首先走不成功的路径,即会引发异常的路径,解释之后,我们将从中断处继续,走成功的路径。
为了观察不成功的路径,需要满足引发异常的条件。在这种情况下,我们的通用数据库访问需要为我们提供通用的异常日志记录解决方案。而 `ActivateException` 类正好提供了这样的解决方案。
因此,现在让我们观察这个类的内部工作原理。
// (2.8)
class ActivateException
{
public ActivateException(Exception e)
{
new TypeOf<ActivateException>().Type.Activate(e);
}
}
让我们首先立即指出,用于为我们提供异常日志记录的机制也基于数据交换机制[3]中使用的机制,其目标是在运行时加载外部程序集并为我们提供包含异常日志记录逻辑的类型,该类型将在运行时实例化以处理异常。
因此,我们在此类的构造函数中观察到的一行代码将加载我们所需的程序集,返回所需的类型并在传递给它的参数 `e` 中包含的引发异常时实例化它。
执行前一操作所需的程序集和类型由 `TypeOf<T>` 类提供,其定义如下。
// (2.9)
class TypeOf<T> : AbstractType<T>
{
public TypeOf() : base("Assembly", "Type") { }
}
TypeOf<T>
类将简单地调用其父类的基构造函数,并向其传递两个 string
值,这些值将指示我们需要访问名为 Assembly 和 Type 的 XML 元素。
父类,即 AbstractType<T>
类,执行与定义在 (1.9) 中的类相同的功能,即 AbstractResource<T>
,唯一的区别是此类的值是从不位于资源中的 XML 中提取的,因此我们不再详细介绍。相反,我们将检查 OuterConfig<T>
类的工作原理,该类将为我们提供不包含在资源中的 XML 文件。
// (3.0)
class OuterConfig<T> : IConfigurable
{
public XElement GetConfig()
{
return XDocument.Load(new PathFor<T>().Path).Root;
}
}
应该很容易理解,此类的返回值将是 `XElement` 类型的实例,它表示从 XML 文件加载的特定 XML 元素,文件的路径由 `PathFor<T>` 类及其 `Path` 属性根据类型 `T` 提供。
为了了解我们如何获取路径,我们需要观察 PathFor<T>
类。
// (3.1)
class PathFor<T>
{
public string Path { get; set; }
public PathFor()
{
Path = new CodebasePath().Value + @"\" +
typeof(T).FullName.Replace(".", @"\") + ".xml";
}
}
PathFor<T>
类将简单地使用 CodebasePath
的 Value
属性中包含的值,并向其添加由泛型类型 T
的名称构造的相对路径。有关此机制的详细说明,应查阅数据交换机制[3]的文档。
前面提到的文档中未涵盖的部分将在接下来介绍。这涉及 CodebasePath
类的工作原理。
// (3.2)
class CodebasePath : GetPathFor<CodeBase> { }
此类的定义将为我们提供当前正在执行的程序集的路径,在我们的示例中是通用数据库访问程序集。我们可以看到,所涉父类是 GetPathFor<T>
类,其泛型类型 T
被 CodeBase
类封闭。
我们首先来看看 `GetPathFor<T>` 类以及它如何与 `CodeBase` 类交互。
// (3.3)
abstract class GetPathFor<T> where T : IPath, new()
{
public string Value { get; set; }
public GetPathFor()
{
Value = Path.GetDirectoryName(new T().Name);
}
}
显然,类定义将为我们提供指定目录的名称,该目录将由泛型类型 `T` 的实例通过其 `Name` 属性生成。我们应该记住,在我们的例子中,泛型类型 `T` 实际上是 `CodeBase` 类,因此让我们看看它为我们提供了什么样的信息。
// (3.4)
class CodeBase : IPath
{
public string Name { get; set; }
public CodeBase()
{
Name = GetType().Assembly
.GetName()
.CodeBase;
}
}
CodeBase
类将简单地提供当前正在执行的程序集的路径。这样做是为了使用于定义异常处理类型的所有相关 XML 文件都位于我们主应用程序的起始文件夹中,其子文件夹结构为 GenericDataBaseAccess/Exceptions。
我们还应该指出,如果我们使用 ASP.NET 项目,这种机制将允许我们将通用数据库访问程序集放入主应用程序文件夹中。
同样,如果我们使用 WCF 服务来调用将调用我们程序集的适当类,则该程序集将能够位于 WCF 程序集所在的文件夹中。
此外,由于定义 (2.8) 中的泛型类型 `T` 已被名为 `ActivateException` 的类封闭,因此相应的 XML 文件将具有相同的名称,并且名为 *ActivateException.xml*。此 XML 文件具有以下结构。
// (3.5)
<?xml version="1.0" encoding="utf-8"?>
<root>
<Assembly>ExceptionHandling.dll</Assembly>
<Type>ExceptionHandling.Handler</Type>
</root>
应该很容易理解,Assembly 元素包含定义将用于处理异常的类型的程序集的名称,而 Type 元素包含其名称。
因此,回到定义 (2.8),一旦我们拥有构建所需 XML 文件路径所需的所有数据,该文件将被打开,并返回其 Assembly 和 Type 元素中的值。完成此操作后,异常将得到处理。
通过完成此操作,我们已经完整描述了通用数据库访问使用的异常处理机制,从而完成了定义 (2.7) 中开始的描述,该描述解释了 SQL 命令执行失败的情况。
我们现在回到定义 (2.7),在那里我们将继续进行 SQL 命令的成功执行。
一旦在定义 (2.7) 中调用了 `method` 委托并且 Select 命令成功执行,`Data` 字段将填充从数据库中选择的数据。
3.4 查询执行
为了理解程序集中需要发生的复杂过程,我们现在必须观察核心类的定义,该定义在类定义层次结构中最高,并代表所有 SQL 命令和存储过程的模板,即 `GenericDataBase` 类。
因此,让我们继续执行过程的第三阶段和 `GenericDataBase` 类。
// (3.6)
abstract class GenericDataBase
{
protected DbDataAdapter Adapter { get; set; }
protected DbConnection Connection { get; set; }
protected DbCommand Command { get; set; }
protected DbCommandBuilder Builder { get; set; }
protected void SetParameters<T>(T c, CommandType type)
{
new ActivateProperties<GenericDataBase>(this);
new SetupProperties<T>(c, Command, Connection, Builder, type);
}
}
在更详细地查看类定义时,我们可以观察到它声明了四个属性,这些属性都是抽象类。这些属性表示将用于与数据库通信和执行所需 SQL 命令的实例。它们如下:
类型为 `DbDataAdapter` 的 `Adapter` 属性表示将执行指定 SQL 命令的实例。
接下来,类型为 `DbConnection` 的 `Connection` 属性表示将持有到数据库的连接字符串的实例。
类型为 `DbCommand` 的 `Command` 属性表示一个实例,它将确定我们是执行基于文本的 SQL 命令还是 SQL 存储过程,并相应地存储命令的文本或存储过程的名称。
类型为 `DbCommandBuilder` 的 `Builder` 属性表示将用于从存储过程派生参数的实例。
我们还可以进一步注意到,所涉属性都没有在构造函数中显式实例化。这将在 `ActivateProperties<T>` 类中执行。在属性填充非抽象数据库特定类型后,它们将在 `SetupProperties<T>` 类中使用,以便为所需的 SQL 命令创建参数。
因此,现在让我们转向 `ActivateProperties<T>` 类。
// (3.7)
class ActivateProperties<T>
{
public ActivateProperties(T source)
{
var properties = source.ProtectedProperties();
var dictionary = new FillDictionary<T>(properties).Dictionary;
new CreateProperties<T>(source, properties, dictionary);
}
}
当前正在阐述的这个类负责将对象引用添加到前面类定义中定义的属性。换句话说,这个类需要在运行时创建类型的实例,并将它们添加到相应的属性中。
我们首先应该注意到,由于我们使用了 `GenericDataBase` 类来封闭我们正在处理的泛型 `T` 类型,因此 `source` 参数是 `GenericDataBase` 类的一个实例,其属性将被提取到 `properties` 变量中。
然后,`FillDictionary<T>` 类将用于提取成功执行 SQL 命令所需的特定于数据库的相关值,并将其与相应属性进行匹配。
最后,`CreateProperties<T>` 类将用于创建提取类型的新实例并将其添加到相应的属性中。
为了理解数据库特定类型提取的确切过程,我们将介绍 `FillDictionary<T>` 类。
// (3.8)
class FillDictionary<T>
{
public Dictionary<string, Entry> Dictionary = new Dictionary<string, Entry>();
public FillDictionary(List<PropertyInfo> properties)
{
new SetupDictionary<T>(Dictionary, properties, new NoValue(), new NoData());
new SetupDictionary<T>(Dictionary, properties, new HasValue(), new HasData());
}
}
我们可以注意到,类定义包含一个名为 `Dictionary` 的 `Dictionary<string, Entry>` 类型的字段,该字段用于匹配定义在 (3.6) 中的 `GenericDataBase` 类中定义的属性名称和特定于数据库的设置 XML 文件中的 Property XML 元素。
另一方面,构造函数将为我们提供从类定义 (3.6) 中提取的属性列表。这些属性需要用特定于数据库的类型实例化。此过程将在 `SetupDictionary<T>` 类中执行。我们可以在构造函数中看到此类的两个实例。第一行代码将用于匹配在 Data XML 元素中没有额外数据的属性,而第二行代码用于设置具有某些额外值定义的属性。
为了完全理解这个过程是如何发生的,我们需要简要了解所有使用的类定义,然后我们将继续介绍 SetupDictionary<T>
类的定义。
因此,我们简要描述一下 Entry
类。
// (3.9)
class Entry
{
public string Assembly { get; set; }
public string Type { get; set; }
public List<NameValue> NameValues = new List<NameValue>();
public Entry(XElement element)
{
Assembly = element.Element("Assembly").Value;
Type = element.Element("Type").Value;
}
}
很容易看出,这是一个容器类型类,它只会为我们提供从特定于数据库的设置 XML 文件中提取的简单值。`Assembly` 属性将保存需要加载的特定于数据库的程序集名称。`Type` 属性将保存需要实例化的特定于数据库的类型。
另一方面,`NameValues` 列表将包含由特定于数据库的 XML 文件提供的所有值的集合,这些值包含在 Data XML 元素中。`NameValues` 类型是一个简单的容器类型,只包含 `Name` 属性,该属性将保存定义在将实例化的特定于数据库的类型之一中的特定于数据库的 `Boolean` 属性的名称,以便进一步配置实例。
它还将包含一个 `Value` 属性,该属性将简单地包含 True 或 False `string`,该 `string` 将被解析并用作 `Name` 属性指定的属性的布尔值。
接下来要观察的类是 NoValue
类。
// (4.0)
class NoValue : IValue
{
public bool Exists(string name, XElement element, List<PropertyInfo> properties)
{
return new PropertyElement().Exists(name, element, properties) &&
!new DataElement().Exists(element);
}
}
此类将测试 `properties` 参数中是否存在一个属性,其名称与 `element` 参数的某个元素中 `name` 参数指定的名称相同。此外,该类将测试 `element` 参数是否没有任何 Data XML 元素。
如果这是真的,则意味着特定于数据库的设置 XML 文件包含与定义 (3.6) 中定义的属性匹配的元素,并且在实例化特定于数据库的类型时无需考虑任何附加值。
因此,现在我们可以介绍在满足上述条件时使用的 `NoData` 类。
// (4.1)
class NoData : IData
{
public void SetValues(Dictionary<string, Entry> dictionary, XElement e)
{
dictionary.Add(e.Element("Name").Value, new Entry(e));
}
}
应该很容易理解,`dictionary` 参数将接收 `Dictionary<string, Entry>` 类型的一个实例,该实例将在定义 (3.8) 中提供。
类型为 `XElement` 的参数 `e` 将被提取其值,即,对应于 `Entry` 类属性名称的元素将由参数 `e` 中匹配的 XML 元素的值填充。完成此操作后,新实例将作为新条目放置在字典中。
如果在特定于数据库的设置 XML 文件中定义了一些 Data XML 元素,我们将需要查看其余定义,如下所示。
// (4.2)
class HasData : IData
{
public void SetValues(Dictionary<string, Entry> dictionary, XElement e)
{
var entry = new Entry(e);
e.Elements("Data")
.Elements("Name")
.Zip(e.Elements("Data").Elements("Value"), (n, v) => new NameValue(n.Value, v.Value))
.ToList()
.ForEach(x => entry.NameValues.Add(x));
dictionary.Add(e.Element("Name").Value, entry);
}
}
我们应该注意到,此类产生的功能与前一个类定义相同,只是增加了从 Data 元素的子元素 Name 和 Value 元素中提取值。
这些值将用于进一步修改将在运行时实例化的特定于数据库的类型。此外,`HasValue` 类的定义将不在此处呈现,因为与 `NoValue` 类相比,此类的唯一区别是 `HasValue` 缺少对第二个 `bool` 值的否定。因此,此类的测试是用于检查任何 Data XML 元素的存在。
我们现在可以继续 `SetupDictionary<T>` 类的定义。
// (4.3)
class SetupDictionary<T>
{
public SetupDictionary(Dictionary<string, Entry> dictionary,
List<PropertyInfo> properties, IValue value, IData data)
{
new XmlData<T>().XmlValues
.Elements("Property")
.Where(element => value.Exists("Name", element, properties))
.ToList()
.ForEach(element => data.SetValues(dictionary, element));
}
}
SetupDictionary<T>
类一旦接收到从定义 (3.8) 中所示的参数传递给它的适当值,就会加载特定于数据库的设置 XML 文件并提取其 Property 元素。
完成此操作后,它将测试 Name XML 元素中是否存在特定属性。如果这些特定属性存在,它们将与从加载的 XML 文件中的相应值进行匹配。
加载的 XML 文件由 `XmlData<T>` 类提供,其定义如下。
// (4.4)
class XmlData<T>
{
public XElement XmlValues { get; set; }
public XmlData()
{
XmlValues = XDocument.Load(new PathFor<T>().Path).Root;
}
}
很容易注意到,此类将为我们提供一个 XML 文件,其路径将由定义在 (3.1) 中的 `PathFor<T>` 类确定。
由于在这种情况下泛型类型 `T` 是 `GenericDataBase` 类型,因此将加载并呈现的 XML 文件将在包含 Generic Database Access 程序集的文件夹中的 GenericDataBaseAccess 文件夹中找到。而 XML 文件的文件名将是 *GenericDataBase.xml*。
一旦执行此操作,所有将用于执行 SQL 命令的属性都将与从加载的特定于数据库的设置 XML 文件中的相应值进行匹配。
因此,有了这个机制,我们继续演示的下一部分,即创建上述属性。因此,让我们继续定义 (3.7),并探索定义中调用的 `CreateProperties<T>` 类。
// (4.5)
class CreateProperties<T>
{
public CreateProperties(T control, List<PropertyInfo> properties, Dictionary<string, Entry> dictionary)
{
properties.Where(property => dictionary.Keys.Contains(property.Name))
.ToList()
.ForEach(property => new SetValue<T>(control, property, dictionary));
}
}
此类现在用于实例化前一步中所有选定的属性。首先,选择所有名称包含在字典参数中的属性,然后,对于所有此类属性,其值由 `SetValue<T>` 类设置,其定义如下。
// (4.6)
class SetValue<T>
{
public SetValue(T control, PropertyInfo info, Dictionary<string, Entry> dictionary)
{
var item = dictionary[info.Name];
if (!string.IsNullOrEmpty(item.Type))
{
info.SetValue(control, new ActivateDictionary(item).Instance, null);
}
}
}
我们可以直接看到,单个属性的名称将在 `dictionary` 参数中匹配,然后其返回值将返回到 `item` 变量中,并且如果 `item` 变量的 `Type` 属性不为空,我们将把该值设置为相应的属性。
此功能当然将由 `ActivateDictionary` 类提供,因此让我们进一步检查其定义。
// (4.7)
class ActivateDictionary : ActivateSource<object>
{
public ActivateDictionary(Entry entry)
{
Instance = new TypeFromAssembly(entry).Type.Activate();
new SetValueToInstance(entry, Instance);
}
}
所涉类将类型为 `Entry` 的单个参数作为其所有必需值,以推导我们属性需要实例化的所需类型。类型为 `object` 的派生属性 `Instance` 由 `TypeFromAssembly` 类公开的 Type 属性实例化,同时调用其扩展方法 `Activate()`。
我们现在应该记住定义 (4.2),其中我们提到了对数据库特定类型的进一步修改。这将在 `SetValueToInstance` 类中执行。
让我们首先看看 TypeFromAssembly
类以及它将如何帮助我们推导所需的类型。
// (4.8)
class TypeFromAssembly
{
public Type Type { get; set; }
public TypeFromAssembly(Entry entry)
{
Type = new LoadType(entry.Assembly, entry.Type).Type;
}
public TypeFromAssembly(string assembly, string type)
{
Type = new LoadType(assembly, type).Type;
}
}
这个相当简单的类定义将使用 `LoadType` 类加载适当的类型,并将其存储在 `Type` 属性中。在我们的例子中,我们将使用类型为 `Entry` 的简单参数调用构造函数。
我们现在应该继续 `LoadType` 类。
// (4.9)
class LoadType
{
public Type Type { get; set; }
public LoadType(string assembly, string type)
{
Type = new LoadAssembly(assembly).Assembly.GetType(type);
}
}
我们可以看到,`LoadType` 类为了让我们获取相关类型,将实例化 `LoadAssembly` 类并向其添加前面提到的值。这将产生加载程序集并从中提取指定类型的效果。
程序集究竟如何加载,我们将在 `LoadAssembly` 类的定义中探讨。
// (5.0)
class LoadAssembly
{
public Assembly Assembly { get; set; }
public LoadAssembly(string source)
{
Assembly = Assembly.LoadFrom(new UriPathForAssembly(source).Value);
}
}
通过使用 `UriPathForAssembly` 类提供其目标的路径来加载相关程序集。此类将相对路径转换为 URI 路径,以便找到我们的程序集。
为此,我们来看看下面的类定义。
// (5.1)
class UriPathForAssembly
{
public string Value { get; set; }
public UriPathForAssembly(string assembly)
{
Value = Path.Combine(new CodebasePath().Value, assembly).ToUri();
}
}
这个类将简单地获取我们之前指定的程序集名称,位于 `assembly` 参数中,连同由定义 (3.3) 中所示的 `CodebasePath` 提供的值,并将它们组合起来形成到相关程序集的相对路径。一旦生成此路径,我们只需调用 `ToUri()` 扩展方法,该方法将简单地生成到所述程序集的本地 URI 路径。
一旦此操作完成,我们回到定义 (4.7),并且我们的 `Instance` 属性已填充指定类型的适当值。
那么现在让我们继续 `SetValueToInstance` 类。
// (5.2)
class SetValueToInstance
{
public SetValueToInstance(Entry entry, object instance)
{
foreach (var nameValue in entry.NameValues)
{
instance.GetType()
.GetProperty(nameValue.Name)
.SetValue(instance, bool.Parse(nameValue.Value), null);
}
}
}
SetValueToInstance
类将简单地对 `entry` 参数的 `NameValues` 属性中的每个项执行以下操作。
从 `instance` 参数中,将提取名称与 `nameValue` 变量中的 `Name` 属性对应的属性,并根据 `nameValue` 变量中的 `Value` 属性中包含的值进行加载。
我们应该注意到,我们将只能设置布尔值以进一步修改我们的实例。当此过程结束时,我们拥有所有必需的特定于数据库的类型,这些类型已实例化,并相应地修改,并设置为 `GenericDataBase` 类中的相应属性。
回到定义 (3.6),其中实例化了 `SetupProperties<T>` 类的一个实例,我们将接下来检查此类的定义,并且我们还应该提到,有了这个最后的定义,所有先决条件都已准备就绪,可以最终开始执行 SQL 命令了。
为了了解这是如何完成的,让我们观察以下类定义。
// (5.3)
class SetupProperties<T>
{
public SetupProperties(T c, DbCommand command,
DbConnection connection, DbCommandBuilder builder, CommandType type)
{
connection.ConnectionString = new GenericConnection().Value;
command.CommandType = type;
command.CommandText = new GenericCommand<T>().Value;
command.Connection = connection;
new GenericParameters<T>(c, command, connection, builder);
}
}
SetupProperties<T>
类,正如我们所看到的,其第一个参数将是类型 `T` 的参数,它代表我们的业务对象,其中包含与 SQL 命令中的参数对应的属性。
其余参数将用于指定我们的命令将如何精确执行。`connection` 参数的 `ConnectionString` 属性,它表示我们将用于连接数据库的连接字符串,使用 `GenericConnection` 类填充。
接下来,`command` 参数的 `CommandType` 属性,我们用它来表示实际的 SQL 命令,由 `type` 参数填充。这将允许我们指示我们正在处理的是一个简单的 SQL 命令还是一个 SQL 存储过程。command
参数的 `CommandText` 属性将由 `GenericCommand<T>` 类加载。此方法将为我们提供基于文本的命令本身,或 SQL 存储过程的名称。由于我们正在执行一个简单的 Select 命令,因此我们在此处处理的是 SQL 命令的文本表示。
接下来,`command` 参数的 `Connection` 属性被设置为 `connection` 参数。这只是让 `command` 属性使用我们之前决定使用的连接字符串。
最后,我们看到我们已经实例化了 `GenericParameters<T>` 类,它将创建我们将在 SQL 命令中使用的参数。
为了能够继续进行,我们首先探讨 `GenericConnection` 类的定义,其形式如下。
// (5.4)
class GenericConnection : AbstractDataElement<GenericDataBase>
{
public GenericConnection() : base("Connection") { }
}
GenericConnection
类是 `AbstractDataElement<T>` 子系列类之一。这些特定类派生自 `AbstractDataElement<T>`,并向其基构造函数传递一个 `string` 参数,该参数表示特定于数据库的设置 XML 文件中特定 XML 元素的名称。这将允许父类访问指定元素的值。
在当前情况下,我们正在访问 Connection XML 元素并检索连接字符串,我们将通过该连接字符串连接到数据库。
为了更详细地理解此 XML 操作背后的过程,我们需要观察以以下方式定义的 `AbstractDataElement<T>` 类定义。
// (5.5)
abstract class AbstractDataElement<T>
{
public string Value { get; set; }
public AbstractDataElement(string element)
{
Value = new ValueFor<T>().From(element);
}
}
这个非常简单的类将以特定 XML 元素的名称作为其单个参数,并通过将其存储在 `Value` 属性中来返回其值。显然,要打开的 XML 文件的位置是根据泛型类型 `T` 确定的。
由于负责确定位置和检索相关数据的类是
ValueFor<T>
类,因此我们接下来将对其进行研究。
// (5.6)
class ValueFor<T>
{
public string From(string element)
{
return XDocument.Load(new PathFor<T>().Path)
.Root
.Element(element)
.Value;
}
}
我们理解当前呈现的复杂过程应该没有问题,因为它只是 `XDocument` 类用于打开一个 XML 文档,该文档由我们已在 (3.1) 中定义的 `PathFor<T>` 类定位。
一旦文档检索到,名为 `element` 的单个 `string` 参数将用于提取并返回它将为我们指定的 XML 元素中的值。
我们现在回到定义 (5.3) 并继续我们之前中断的地方。此过程中使用的下一个类是 `GenericCommand<T>` 类。由于这也是 `AbstractDataElement<T>` 系列类之一,因此我们在此不介绍其定义。
足以说明的是,此方法将从 Command XML 元素中提取 SQL 命令文本,然后将用于执行相关 SQL 命令。XML 文件将根据泛型类型 `T` 进行选择。
3.5 参数创建
第四个也是最后一个阶段执行过程从更复杂的类定义之一开始,即 `GenericParameters<T>` 类。
// (5.7)
class GenericParameters<T>
{
public GenericParameters(T data, DbCommand command, DbConnection connection, DbCommandBuilder builder)
{
var prefix = new ParameterPrefix().Value;
if (command.CommandType == CommandType.StoredProcedure)
{
if (builder == null)
{
command.Parameters.AddRange(new WithDirection<T>(data, prefix).Parameters.ToArray());
}
else
{
new TryConnection(() => new DeriveParameters(builder, command), connection);
new SetupParameters<T>(data, command);
}
}
else
{
var names = new ParameterNames(command, prefix).Names;
command.Parameters.AddRange(
new WithoutDirection<T>(data, prefix, names).Parameters.ToArray());
}
}
}
这个类定义负责处理参数创建过程。由于我们的 SQL 命令在某些情况下会使用参数,我们需要能够创建不同数量的不同类型参数,以忠实地执行 SQL 命令。此外,参数需要是特定于数据库的类型。
为了了解这是如何实现的,我们继续观察
ParameterPrefix
类。这个类将简单地从特定于数据库的 XML 文件中的 Prefix XML 元素返回预设的前缀,该前缀用于表示 SQL 命令中的参数,由于这个类是 AbstractDataElement<T>
类系列中的一个,我们不再详细讨论其细节。我们参数创建过程的下一步是在两种不同情况之间进行分支。在第一种情况下,如果 `if` 命令评估为 true,我们知道我们正在执行 SQL 存储过程。如果不是这种情况,我们将执行一个简单的基于 SQL 文本的命令。由于我们正在执行一个简单的 Select 命令,让我们继续代码块的 `else` 部分。
在 `names` 变量中,我们将存储将在我们的 SQL 命令中使用的参数名称列表。从命令本身提取这些参数的过程由 `ParameterNames` 类执行,我们将在接下来介绍。
// (5.8)
class ParameterNames
{
public List<string> Names { get; set; }
public ParameterNames(DbCommand command, string prefix)
{
Names = Regex.Matches(command.CommandText, @"\" + prefix + @"\w+")
.Cast<Match>()
.Select(x => x.Value.Replace(prefix, ""))
.ToList();
}
}
正如所预料的,这个简单的类将使用 `Regex` 类来提取所有以特定字符开头的单词。在我们的例子中,这个字符是特定于数据库的前缀字符,用于表示参数。
一旦参数被提取,它们将被剥离前缀并由 `Names` 属性返回。
提供参数名称后,我们使用 `WithoutDirection<T>` 类创建一个特定于数据库的参数集合,并使用 `AddRange(Array)` 方法将它们添加到 `command` 参数中。接下来将讨论此相关类。
// (5.9)
class WithoutDirection<T> : SetDirection
{
public WithoutDirection(T data, string prefix, List<string> names)
{
new NoDirection<T>(data, Parameters, prefix, names);
}
}
我们首先指出这个类派生自 `SetDirection` 类,并且它继承了类型为 `List<DbParameter>` 的 `Parameters` 字段。参数创建过程在 `SetDirection` 类中执行,而参数的值在 `NoDirection<T>` 类中添加。
现在让我们仔细看看参数是如何创建的,然后我们将了解它们是如何设置以便稍后使用的。
// (6.0)
abstract class SetDirection
{
public List<DbParameter> Parameters = new ActivateListOf<DbParameter>().Instance;
}
我们应该尝试理解此类的组成如下。由于 SQL 命令参数的数量可以介于零到数据库允许的任何数字之间,因此我们需要创建一个可以存储任意数量参数的列表。
此外,由于我们事先不知道我们正在处理的特定数据库系统,因此我们需要将此列表创建为 `DbParameter` 类型列表,因为所有特定于数据库的参数都派生自此类型。
我们现在可以使用 `ActivateListOf<T>` 类创建特定于数据库的参数并将它们存储在 `Parameters` 字段中。一旦我们使用 `DbParameter` 类型封闭泛型类型 `T`,我们就表示我们新创建的列表将派生自此特定类型。
为了了解这是如何实现的,让我们进一步研究 `ActivateListOf<T>` 类。
// (6.1)
class ActivateListOf<T>
{
public List<T> Instance { get; set; }
public ActivateListOf()
{
var listType = typeof(List<>).MakeGenericType(new TypeFrom<GenericDataBase>().Type);
Instance = Enumerable.ToList<T>((IEnumerable<T>)Activator.CreateInstance(listType));
}
}
这个类的定义将很容易理解,因为只有两行代码将执行创建相关参数的完整操作。
第一行将简单地获取泛型列表的类型,这是一个 `List<>` 类型。然后我们调用它的 `MakeGenericType(params Type[])` 方法,并向它传递由 `TypeFrom<T>` 类提供的类型。这将产生从参数中提供的指定类型创建泛型列表类型的效果。
第二行代码,将简单地将由 `Activator` 类提供的新创建实例转换为泛型列表,其中泛型类型是前一行代码中定义的类型。
让我们记住,我们现在已经创建了一个特定于数据库的参数的泛型列表。为了完成此过程,我们需要了解是如何确定需要实例化哪种类型的参数的。因此,让我们继续 `TypeFrom<T>` 类的定义。
// (6.2)
class TypeFrom<T> : AbstractType<T>
{
public TypeFrom() : base("ParameterAssembly", "ParameterType") { }
}
此类将简单地指定需要从中提取相关数据的两个 XML 元素。应该很容易理解,`string` "ParameterAssembly" 将返回需要从中提取类型的程序集,而 "ParameterType" 将返回将被实例化的参数类型。
此外,由于此类派生自 `AbstractType<T>`,它与已在 (1.9) 中定义的 `AbstractResourceType<T>` 类几乎相同,我们不再详细阐述。
相反,我们将不失一般性地继续下一个类 `NoDirection<T>` 的定义。
// (6.3)
class NoDirection<T>
{
public NoDirection(T data, List<DbParameter> parameters, string prefix, List<string> names)
{
data.PublicProperties()
.Where(property => new Name().Exists(property, names))
.ToList()
.ForEach(property => parameters.Add(new Parameter<T>(data, property, prefix).Instance));
}
现在让我们考虑类型为 `T` 的数据参数。
此参数是我们的业务对象,包含所有表示 SQL 命令中参数的属性。我们现在需要做的,就是从这些属性中获取值,并将它们放在前面创建的参数中。
为此,我们首先从业务对象中获取所有公共属性。接下来,我们只选择那些名称与参数名称匹配的属性。完成此操作后,我们只需将所有新参数添加到类型为 `List<DbParameter>` 的 `parameters` 构造函数参数中。
通过这样做,我们已成功将业务对象中的所有属性与之前创建的参数进行匹配,并且已在参数中设置所有值,这些值将把相关值传递给将执行的 SQL 命令。
我们现在只剩下两个类的定义,其中 `Name` 类是第一个要考虑的。
// (6.4)
class Name
{
public bool Exists(PropertyInfo property, List<string> names)
{
return names.ConvertAll(x => x.ToLower())
.Contains(property.Name.ToLower());
}
}
此类负责确定哪些属性将与哪些参数匹配。此过程背后非常简单的想法是,只需将所有表示属性和参数名称的元素转换为小写字母,然后按名称进行匹配。
所涉最后一个类是 Parameter<T>
类,用于创建新的特定于数据库的参数。该类的形式如下。
// (6.5)
class Parameter<T> : AbstractParameter<T>
{
public Parameter(T data, PropertyInfo property, string prefix) : base(data, property, prefix) { }
public Parameter(T data, PropertyInfo property, string prefix, ParameterDirection direction)
: base(data, property, prefix)
{
Instance.Direction = direction;
}
}
在我们的特定情况下,我们实例化了第一个构造函数,它没有为参数添加方向,这与第二个构造函数不同。因此,这个构造函数将简单地将其控制权传递给其基构造函数,该基构造函数在 `AbstractParameter<T>` 类中定义,我们接下来将对其进行观察。
这里我们更详细地考虑 `AbstractParameter<T>` 类的定义。
// (6.6)
abstract class AbstractParameter<T>
{
public DbParameter Instance { get; set; }
public AbstractParameter(T data, PropertyInfo property, string prefix)
{
Instance = new Activate<DbParameter>(prefix + property.Name, property.GetValue(data, null)).Instance;
}
}
这个类非常简单易懂,它只负责创建一个特定于数据库类型的新实例,并将其存储在 `Instance` 属性中,该属性的类型为 `DbParameter`。我们应该记住,使用 `DbParameter` 类型的原因是 ADO.NET 框架中用于表示参数的所有特定于数据库的类型都派生自 `DbParameter` 类型。
我们实例化参数的方式如下。我们只需创建 `Activate<T>` 类的一个新实例,并向其传递相关参数。
第一个参数将简单地是参数的名称,我们为其添加一个前缀,该前缀将表示用于表示参数的特定于数据库的前缀。第二个参数将从我们的业务对象中的特定属性中提取值,该属性包含在数据参数中。
我们应该注意到,提取值的属性是名称与相关参数名称匹配的属性。
至此,随着最后一道机制的到位,我们已成功构建了相关参数并将其置于要执行的 SQL 命令中。因此,执行 Select SQL 命令的完整过程已详细描述。
接下来我们将解释用于执行在此特定上下文中不需要的不同类型命令的其余机制。
3.6 非查询类型确定
对于 Insert SQL 命令,与 Select 命令不同,我们使用不同的类来执行它。现在我们将介绍为了执行 Insert 命令而遍历的程序流程,同时保留与之前执行上下文共享的类定义。
因此,让我们开始阐述 Insert<T>
类,它将是我们的程序集入口点,以便我们执行这种特定类型的命令。
// (6.7)
public class Insert<T> : AbstractProcedure<int, IInsert>
{
public Insert(T c)
{
Results = Procedure.Invoke<T>(c);
}
}
我们可以很容易地观察到 `Insert<T>` 和 `Select<T>` 类定义之间的相似性。在此实例中唯一需要注意的显著区别是,由于 Insert 命令是非查询类型命令,因此我们不需要查询数据,而是需要返回一个指示器,通知我们命令的执行情况。
因此,我们不使用定义 (1.5) 中使用的 `DataSet`,而是使用 `int` 类型来确定命令将返回的 `Result` 属性的类型。
一旦执行 Insert SQL 命令,存在两种可能的结果,具有三个可能的数值范围。根据执行命令的结果,有成功和不成功的结果。
在一种情况下,命令可以成功执行并返回受影响的行数,即插入的行数。因此,我们的结果是表示插入行数的整数值,该值大于零。
下一种情况是,我们成功执行了命令,但没有插入任何行。在这种情况下,我们返回整数值零。
而最后一种可能的结果是执行失败,在这种情况下,我们需要返回整数值 -1。
我们可以很容易得出结论,代码流与 `Select<T>` 类相同,直到我们接下来将介绍的 `GenericInsert` 类定义。
// (6.8)
class GenericInsert : CoreNonQuery, IInsert
{
public int Invoke<T>(T c)
{
SetParameters<T>(c, CommandType.Text);
Adapter.InsertCommand = Command;
return Execute(Adapter.InsertCommand.ExecuteNonQuery);
}
}
再次,我们看到了与定义 (2.5) 类似的类定义。唯一显著的区别是返回类型,这次是 `int` 而不是 `DataSet` 类型。显然,当 SQL 命令执行时,执行的成功将从 `Invoke<T>(T)` 方法返回。
另一个区别是,该类派生自 CoreNonQuery
类,这很自然,因为插入类型命令是非查询类型命令。因此,接下来我们来看看 CoreNonQuery
类的定义。
// (6.9)
abstract class CoreNonQuery : GenericDataBase
{
protected int RowsAffected { get; set; }
protected int Execute(Func<int> method)
{
try
{
Connection.Open();
RowsAffected = method();
}
catch (Exception e)
{
RowsAffected = -1;
new ActivateException(e);
}
finally
{
Connection.Close();
}
return RowsAffected;
}
我们可以通过观察参数来注意到该类与在 (2.5) 中定义的类之间的区别。
在这种情况下,我们有一个 Func<int>
类型的委托。其原因是,一旦执行 SQL 命令,它将需要返回一个 int
类型的指示器,该指示器将通知我们命令是如何执行的。这个值方便地存储在 RowsAffected
属性中。
我们还可以注意到构造函数中的 try-catch-finally 块。在 try 块中,我们首先打开与数据库的连接,然后通过调用方法委托执行插入 SQL 命令。如果命令成功完成,受影响的行数将存储在 RowsAffected
属性中。
另一方面,如果发生异常,我们将进入 catch 块,在该块中我们将 RowsAffected
属性设置为 -1,以表示执行失败。然后,我们按照 (2.8) 中定义的方式处理异常。
无论如何,在 finally 块中,连接都将被关闭。
通过介绍这最后一个类定义,我们已经解释了查询类型 SQL 命令执行过程和非查询类型之间的所有相关区别。接下来,我们转向执行查询类型 SQL 存储过程。
3.7 存储过程确定
除了简单的基于文本的 SQL 命令之外,我们还需要能够执行 SQL 存储过程。由于存储过程有两种类型,即查询类型和非查询类型,因此我们还需要两个不同的类定义来表示它们。因此,让我们从查询类型存储过程开始。
// (7.0)
public class Query<T> : AbstractProcedure<DataSet, IQuery>
{
public Query(T c)
{
Results = Procedure.Invoke<T>(c);
}
用于执行查询类型存储过程的 Query<T>
类定义,正如所料,与 Select<T>
类定义非常相似。因此,我们不会再花时间去研究它。相反,我们将简单地指出,这两个类之间的命令流也是相同的,直到我们接下来要讨论的点为止。
所讨论的这个类是以下列方式定义的 GenericQuery
类。
// (7.1)
class GenericQuery : CoreQuery, IQuery
{
public DataSet Invoke<T>(T c)
{
SetParameters<T>(c, CommandType.StoredProcedure);
Execute(() => new ExecuteQuery(Adapter, Command, Data));
new AddValuesToParameters<T>(c, Command);
return Data;
}
}
我们可以注意到,SetParameter<T>(T, CommandType)
方法是使用 CommandType.StoredProcedure
值作为其第二个参数调用的。这就是我们如何确定我们确实是在处理 SQL 存储过程,而不是基于 SQL 文本的命令。
在当前类定义中,我们应该指出的唯一新颖之处是 AddValuesToParameters<T>
类的实例,它将为表示从存储过程中返回某些值的参数的特定属性添加值。
将为其添加值的属性是与任何定义为 Output、Return 或 InputOutput 的参数匹配的属性。这些参数是从存储过程中返回特定值的参数。
要了解如何实现这一点,我们应该继续查看 AddValuesToParameters<T>
类定义。
// (7.2)
class AddValuesToParameters<T>
{
public AddValuesToParameters(T c, DbCommand command)
{
foreach (DbParameter parameter in command.Parameters)
{
foreach (var property in c.PublicProperties())
{
if (parameter.HasValueFor(property))
{
new Fill(parameter.IsDBNull()).Method.SetValue(c, parameter, property);
}
}
}
}
}
所讨论的类定义将执行以下操作。
它将从表示要执行的 SQL 存储过程的命令参数中提取所有参数。然后,对于每个参数,它将从参数
c
中提取所有属性,参数 c
仅仅是我们的业务对象。在此过程中,我们将简单地使用
HasValueFor(PropertyInfo)
扩展方法测试某个属性是否与某个参数匹配。如果此评估为 true,则我们使用 Fill
类填充业务对象中的相应属性,以便它包含参数从存储过程中返回的值。要了解如何实现这一点,我们首先从观察
ValueExtension
类中定义的 HasValueFor(PropertyInfo)
扩展方法开始。
// (7.3)
static class ValueExtension
{
public static bool HasAttributes(this PropertyInfo property)
{
var attributes = property.GetCustomAttributes(true);
return attributes.OfType<OutputAttribute>().Any() ||
attributes.OfType<InputOutputAttribute>().Any() ||
attributes.OfType<ReturnAttribute>().Any();
}
public static bool HasValueFor(this DbParameter parameter, PropertyInfo property)
{
return parameter.CompareName(property.Name) &&
parameter.IsNotNull() &&
property.HasAttributes();
}
}
所讨论的此类定义了我们目前感兴趣的
HasValueFor(PropertyInfo)
扩展方法。此方法将测试三个真值,如果所有三个都评估为 true,它也将返回 true。第一行将确定参数的名称是否与属性的名称匹配。第二行将测试参数是否包含任何值。而第三行将调用上面定义的扩展方法,该方法将简单地确定所讨论的属性是否已用
OutputAttribute
、InputOutputAttribute
或 ReturnAttribute
类型中的任何自定义属性进行修饰。如果其中任何一个语句返回 true,则
HasAttribute()
方法也将返回 true。接下来我们来看
Fill
类。
// (7.4)
class Fill : Dictionary<bool, IParameterMethods>
{
public IParameterMethods Method { get; set; }
public Fill(bool value)
{
Add(true, new ParameterDBNull());
Add(false, new ParameterNotDBNull());
Method = this[value];
}
}
这个类将负责确定如何用所需的值填充属性。由于这个类派生自
Dictionary<T, K>
类,它本身就是一个字典类。用于关闭其泛型类型的类型是 bool
和 IParameterMethods
。在构造函数中,我们将简单地添加两个新的键值对,其中键对应于所有可能的
Boolean
值,即 true 和 false。而值将表示将在参数包含或不包含 null 值的情况下使用的类型实例。这些类型派生自
IParameterMethods
接口,用于用所需的值填充属性。一旦将
Boolean
值传递给此类的实例,它将通过将其存储在 Method
属性中来返回相应的类实例,以用值填充属性。现在我们来观察
ParameterNotDBNull
类的定义。
// (7.5)
class ParameterNotDBNull : IParameterMethods
{
public void SetValue(object c, DbParameter parameter, PropertyInfo property)
{
property.SetValue(c, parameter.Value, null);
}
}
由于实现 IParameterMethods
接口的唯一先决条件是实现其单个方法 SetValue(object, DbParameter, PropertyInfo)
,我们可以看到当前类定义完全符合该接口。
其构造函数将采取的唯一操作是它将为所讨论的属性设置值,并使用相应参数中的值。ParameterDBNull
类也将执行相同的操作,唯一的例外是,我们不传递参数值,而是将 null 值传递给属性,表示不会返回任何值。
完成此操作后,我们可以继续查看下一个类定义,该类定义在 Select<T>
类采用的程序流程中有所不同。因此,我们回到定义 (5.7),即 GenericParameters<T>
类。这一次,第一个 if 命令将评估为 true,因为我们正在处理 SQL 存储过程。
第二个 if 命令将测试 builder
参数是否为 null。builder 参数的类型是 DbCommandBuilder
。此类用于从数据库系统中存在的存储过程中提取参数定义。
如果 if 语句评估为 true,那么 WithDirection<T>
类将为我们提供所需的参数,然后我们将其添加到 command
参数的 Parameters
属性中。
另一方面,如果 if 命令评估为 false,那么我们将从存储过程本身派生参数,然后相应地设置它们。
首先让我们看看如果调用 WithDirection<T>
类,它将如何处理这个问题。
// (7.6)
class WithDirection<T> : SetDirection
{
public WithDirection(T data, string prefix)
{
new Direction<T, InputAttribute>(data, Parameters, prefix, ParameterDirection.Input);
new Direction<T, OutputAttribute>(data, Parameters, prefix, ParameterDirection.Output);
new Direction<T, InputOutputAttribute>(
data, Parameters, prefix, ParameterDirection.InputOutput);
new Direction<T, ReturnAttribute>(data, Parameters, prefix, ParameterDirection.ReturnValue);
}
如果我们不使用 (3.6) 中定义的 Builder
属性从存储过程中派生参数,我们需要使用属性修饰业务对象中的属性以表示其方向。这些属性如下:InputAttribute
用于修饰与存储过程中的 IN 参数对应的属性。OutputAttribute
用于修饰与存储过程中的 OUT 参数对应的属性。InputOutputAttribute
用于修饰与存储过程中的 INOUT 参数对应的属性。而 ReturnAttribute
用于表示返回参数。
我们可以清楚地看到,就像 WithDirection<T>
类一样,这个类也派生自 SetDirection
类。唯一的区别是,这个类实例化了四个 Direction<T, K>
类来设置参数及其方向。
每个实例化的 Direction<T, K>
类都将使用不同的属性类型关闭其泛型类型 K
,以便可以相应地提取所有属性并将其与参数匹配。
// (7.7)
class Direction<T, K>
{
public Direction(T data, List<DbParameter> parameters, string prefix, ParameterDirection direction)
{
data.PublicProperties()
.Where(property => new AttributeOf<K>().ExistsIn(property))
.ToList()
.ForEach(property => parameters.Add(new Parameter<T>(
data, property, prefix, direction).Instance));
}
}
}
Direction<T, K>
类与 NoDirection<T>
类非常相似,区别在于 direction
参数将采用 ParameterDirection
枚举类型来定义参数方向。有了这最后一点,我们就可以结束本章了,因为所有类型的 SQL 命令的完整程序流程都已成功解释。
配置应用程序
4.1 数据库特定设置
在开始描述配置应用程序之前,我们应该指出此应用程序本质上是可选的。换句话说,通用数据库访问程序集的工作独立于配置应用程序本身。该程序集可以在任何项目中使用,而无需使用辅助应用程序。
配置应用程序旨在提供更简单的通用数据库访问程序集使用,但所有上述 XML 文件都可以手动设置,无需辅助应用程序。
因此,让我们从配置应用程序本身开始。
该应用程序由三个主要的选项卡项组成。它们代表“设置项”、“命令项”和“异常项”。我们将从“设置项”开始,通过单击“工具”菜单项并选择“设置”即可到达。
我们可以观察到三行主要的文本框控件。这些控件代表数据库特定设置 XML 文件中的 XML 元素。在这个特定的例子中,我们看到了一个 Oracle 特定的设置配置。
现在让我们更仔细地查看这些控件。
连接字符串
此控件用于输入将用于连接到数据库的连接字符串。
前缀
此控件用于定义将用于表示 SQL 命令参数的前缀。由于我们正在处理特定于 Oracle 的设置,因此我们使用“:”字符来表示参数。
程序集
此控件用于定义将从中提取所有相关数据库特定类型的程序集文件。
参数
此控件用于定义将用于创建将在 SQL 命令中使用的参数的类型。
Builder
此控件用于定义将用于从 SQL 存储过程派生参数的类型。
适配器
此控件用于定义将负责执行 SQL 命令或 SQL 存储过程的类型。
Connection
此控件将用于定义表示与数据库连接的类型。
命令
此控件用于定义将表示我们将执行的 SQL 命令或 SQL 存储过程的类型。我们还可以注意到,命令控件还定义了两个附加控件。这些控件表示我们的数据 XML 元素。由于最后一行的所有三个控件都可以进一步修改,因此它们顶部都有一个“+”和“-”号。这些控件将用于添加或删除名称和值控件对。
值得一提的是,加载某个数据库特定设置 XML 文件最简单的方法是简单地将其拖放到应用程序窗口中。
4.2 SQL 查询
此选项卡项控件表示用于处理 SQL 命令和 SQL 存储过程的命令控件。通过单击“工具”菜单项,然后选择“命令”来访问该控件。
在这里我们可以观察到命令和查询控件。左侧的列表表示我们所有包含基于文本的 SQL 命令或 SQL 存储过程名称的 XML 文件。
在这个特定的例子中,我们看到了一个名为 Cars 的 XML 文件中基于文本的 SQL 命令实例。该文本可以轻松加载、编辑并保存回文件。
需要注意的是,加载所有包含相关命令的 XML 文件最简单的方法是简单地选择多个文件或文件夹,然后将其拖放到列表本身上。
选定的文件夹可能包含非 XML 文件,或不符合前面定义 (1.3) 中定义的命令 XML 文件的 XML 文件。然而,应用程序将简单地搜索所有正确的文件,并将其从其他文件中筛选出来,以便在命令控件上显示它们。
与手动处理 XML 文件相比,使用应用程序的优势之一是基于文本的 SQL 命令需要保存和格式化以符合 XML 标准。
这意味着某些字符,例如“<”或“>”,将被转录为“<”或“>”,这显然会使编写此类 SQL 查询变得困难。所讨论的应用程序在幕后自动处理此类转换,并且我们得到了一个适当的字符,如上面所示的图像所示。
4.3 异常处理
最后一个选项卡项是用于处理异常的。我们可以通过单击“工具”菜单项,然后选择“异常”轻松到达此控件。
图 4. – 配置应用程序的异常处理选项卡项。
我们可以看到这里只有两个文本框控件,它们用于表示将从中加载类型的程序集名称(该类型将用于处理异常)以及类型本身的名称。
“编辑”项,就像数据库特定“编辑”项一样,允许将当前使用的 XML 文件用作主配置文件。
4.4 快捷键
下表列出了我们辅助应用程序中可用的所有快捷键,以使工作尽可能简单。
我们可以看到需要按下的键及其操作说明在“快捷键”列中,而右侧的“控件”列则代表前面描述的三个控件。
数据库查询示例
5.1 测试设置
现在我们将对某些数据库系统执行一些查询示例,以展示我们新创建的通用数据库访问的工作原理。因此,让我们简要概述一下我们的测试设置,它包含以下几点。
- 主测试应用程序
- 数据库源数据
- 数据库特定和 SQL 命令 XML 文件
- 数据库特定程序集
- 异常处理
第1点
我们的主测试应用程序是一个简单的 WPF 应用程序,旨在在各种数据库系统上执行 SQL 命令并将其呈现给用户,并按以下图像所示进行相应设计。
理论上,通用数据库访问能够与任何存在 ADO.NET 程序集的数据库系统进行交互。尽管如此,我们只会选择在目前正在使用的几种最流行的数据库系统上测试该程序集[4]。
因此,从上图中我们可以看到,测试将在 Microsoft SQL Server、Oracle、Microsoft Access、MySQL 和 PostgreSQL 数据库系统上进行。
第2点
我们的测试设置数据是数据库源数据。这是将由我们的程序集查询的数据。
这些数据以四个 SQL 脚本的形式提供,需要针对上面提到的每个数据库系统执行,以创建相应的表和数据。对于 Access 数据库,将提供一个示例数据库文件。
第3点
我们的设置是数据库特定 XML 设置预设的集合。该解决方案将包含上述所有数据库的五种不同预设。这些预设位于 GenericTest\bin\Debug\GenericDataBaseAccess 文件夹中。
同样,SQL 命令也将提供在解决方案中,并位于 Solutions\Examples\Scripts 文件夹中。
此外,我们的配置应用程序 DataBaseConfig.exe 将与我们的预设一起位于同一文件夹中。
第4点
我们的示例解决方案中将包含通用模型正常工作的明确先决条件,即所有相关数据库特定 ADO.NET 程序集的集合。这些程序集如下。
- Microsoft SQL Server 和 Access – System.Data.dll
- Oracle – Oracle.DataAccess.dll
- MySQL – MySql.Data.dll
- PostgreSQL – Npgsql.dll 和 Mono.Security.dll
这些程序集将位于 GenericTest\bin\Debug 文件夹中。
第5点
最后一个设置点涉及异常处理。一个名为 ExceptionHandling.dll 的示例程序集也将提供在上述文件夹中。
此外,我们的测试程序集所需的相应 XML 文件将位于 GenericTest\bin\Debug\GenericDataBaseAccess\Exceptions 文件夹中,并命名为 ActivateException.xml
现在我们已经完全定义了测试设置中的所有相关点,让我们开始查询第一个数据库系统。
5.2 查询 SQL Server 数据库
第一个示例将使用 Microsoft SQL Server 2008 R2 数据库。它将展示如何将单个字符串参数与存储过程一起使用。用于执行所有相关测试(包括第一个测试)的代码如下所示。
// (7.8)
public partial class TestWindow : Window
{
Database Database = new Database();
public TestWindow()
{
InitializeComponent();
}
void Students_Click(object sender, RoutedEventArgs e)
{
Grid.ItemsSource = Database.Query<Students>(new Students() { Major = "Architecture" });
}
void Countries_Click(object sender, RoutedEventArgs e)
{
Grid.ItemsSource = Database.Query<Countries>(new Countries() { p_values = OracleDbType.RefCursor });
}
void Cars_Click(object sender, RoutedEventArgs e)
{
Grid.ItemsSource = Database.Select<Cars>(new Cars() { Date = new DateTime(1990, 1, 1) });
}
void Songs_Click(object sender, RoutedEventArgs e)
{
Grid.ItemsSource = Database.Select<Songs>(new Songs());
}
void Movies_Click(object sender, RoutedEventArgs e)
{
var count = new MovieCount();
Database.Query<MovieCount>(count);
Database.Insert<Blockbuster>(new Blockbuster(count.Count, "Police Academy"));
Grid.ItemsSource = Database.Select<Movies>(new Movies());
}
}
Database 类对我们来说并不是特别重要,我们只需要知道它只是我们一些类的包装器,这些类将用于执行示例,并确保空数据不会产生异常。
我们看到的第一个方法将用于从 SQL Server 数据库中获取所有专业不是“建筑学”的学生。
正如可以很容易看到的,我们已经执行了一个 SQL 存储过程来查询数据库并返回相关数据。特定的 Students 业务对象也用作泛型类型,其实例用作参数。
此外,其属性 Major
将填充“建筑学”string
,并将用作由我们的通用数据库访问程序集创建的参数中的值。
5.3 查询 Oracle 数据库
对于我们的第二个示例,我们将使用 Oracle 11g 数据库系统。我们可以从之前的定义中看到,我们正在使用 Oracle 的 Ref Cursor 来获取相关数据,因此,本示例将展示我们如何使用存储过程、Ref Cursor 和输出参数来执行对 Oracle 数据库的查询。
我们的业务对象 Countries 需要按照以下方式定义。
// (7.9)
class Countries
{
public OracleDbType p_values { get; set; }
}
存储过程将接受一个名为 p_values
的 OUT 参数。一旦 p_values
属性填充了 OracleDbType.RefCursor
,它将作为我们存储过程的相关参数。
5.4 查询 Access 数据库
下一个示例将使用 Microsoft Access 2010 数据库执行。这个非常简单的测试将使用 DateTime
类型来指示应该从数据库中选择哪些汽车。只显示指定日期之后生产的汽车。
由于 Access 数据库不支持存储过程,因此此特定测试将展示如何执行简单的基于文本的 SQL 命令。
5.5 查询 MySQL 数据库
在选择下一个要查询的数据库系统时,我们决定使用 MySQL 5.5.28 版本。这个特定的测试将显示命令 XML 中定义的所有相关歌曲。该测试将显示,即使业务对象没有属性,也可以在基于文本的 SQL 命令中指定多个参数。
因此,此 SQL 命令将返回所有由“Tina Turner”或“Manu Chao”演唱的歌曲。这些值在 Songs.xml 文件中的 SQL 命令本身中明确定义。
5.6 查询 PostgreSQL 数据库
对于我们的最后一个示例,我们决定展示对 PostgreSQL 9.2 数据库系统的查询。这一次,从最终方法中可以看出,我们将测试多个命令,其中包括一个插入命令,它是一种非查询类型的命令。
此外,由于 PostgreSQL 数据库无法正确地从其存储过程中派生参数,因此我们将数据库特定预设中的 Builder 元素留空。
此方法的目的是每次点击时向数据库添加一部新电影,然后显示所有大片电影,即预算为 50,000,000 及以上的电影。
为了执行此操作,代码的第一行创建了我们的业务对象实例。下一行将执行存储过程并检索数据库中已有的电影数量。该数量应存储在我们的业务对象中,其定义方式如下。
// (8.0)
class MovieCount
{
[Output]
public long Count { get; set; }
}
显然,Count
属性已用 OutputAttribute
属性进行修饰,以检索所需的计数。
下一个命令是一个简单的基于文本的 SQL Insert 命令。此命令将新电影添加到数据库中。此命令中使用的 Blockbuster
业务对象将填充上述返回的计数,其定义方式如下。
// (8.1)
class Blockbuster
{
public long ID { get; set; }
public string Title { get; set; }
public double Budget { get; set; }
public Blockbuster(long count, string title)
{
ID = count;
Title = title + " " + count;
Budget = new Random().Next(50000000, 90000000);
}
}
此定义用于定义一部新的大片电影。之前检索到的计数将用于 ID 参数。电影的标题将是“Police Academy”以及计数。此外,为了表明它是一部大片电影,将生成一个介于 50,000,000 到 90,000,000 之间的随机预算。
最后,一旦这部电影插入到数据库中,所有电影都将被选中并显示,至此,我们已经结束了对我们的通用数据库访问程序集可以执行的示例查询的阐述。
参考文献
- David Talbot, Mahesh Chand: Applied ADO.NET Building Data-Driven Solutions; Apress (2003年3月18日)
- https://entlib.codeplex.com/
- Mario Stopfer: The Data Exchange Mechanism (2012年8月29日)。
- http://db-engines.com/en/ranking