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

扩展 C# 以支持编译时 SQL 语法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (28投票s)

2005年6月13日

CPOL

13分钟阅读

viewsIcon

150165

downloadIcon

877

你是否曾希望无需使用字符串或后期绑定就能真正地将 SQL 功能嵌入到你的 C# 代码中?想象一下,完全用 C# 编写复杂的 Where 子句。

背景

我经常收到评论,询问为什么在 LINQ 和 Entity Framework 已经存在的情况下我还要创建这个代码。需要注意的是,本文的发布日期是2005年中期,远在 LINQ 公布之前。因此,实际项目已不再需要,因为它已被 C# 和 VB.NET 的发展所取代。然而,该项目和文章仍然作为一种说明性示例,展示了如何扩展语言,使其超越大多数人的想象。而且,由于自2005年以来语言已经发展了许多其他功能,如今的可能性甚至更大。

引言

你是否曾希望无需使用字符串或后期绑定就能真正地将 SQL 功能嵌入到你的 C# 代码中?想象一下,完全用 C# 编写复杂的 Where 子句。

xQuery.Where = 
(CustomerTbl.Col.NameFirst == "Chad" | CustomerTbl.Col.NameFirst == "Hadi")
& CustomerTbl.Col.CustomerID > 100 & CustomerTbl.Col.Tag != View.Null;

仔细看。这是 C# 代码,不是 SQL。它在编译时解析和绑定,但在运行时评估。在本文中,我将介绍这种方法以及完整的源代码供你使用。

C Omega

C Omega 是微软的一项研究性编程语言。C Omega 用于测试和尝试 C# 未来版本的新想法。C Omega 支持许多与 Indy.Data 提供的功能类似的项目。但是,Indy.Data 提供了 C Omega 中没有的一些功能。最重要的是,Indy.Data 现在可用,并且可以用于生产代码。C Omega 是一个研究项目,最多只会集成到 C# 的未来版本中。

事实上,如果我们看一下 C Omega 的一些示例代码

struct {
    SqlString CustomerID; 
    SqlString ContactName;
}* res
= select CustomerID, ContactName from DB.Customers;
foreach( row in res ) {
    Console.WriteLine("{0,-12} {1}", row.CustomerID, row.ContactName);
}

它看起来非常像 Indy.Data

[Select("select `CustomerID`, `ContactName` from `Customers`")]
public class CustomerRow : View.Row {
public DbInt32 CustomerID;
public DbString ContactName;
}
using (Query xCustomers = new Query(_DB, typeof(CustomerRow))) {
    foreach (CustomerRow xCustomer in xCustomers) {
        Console.WriteLine("{0,-12} {1}", row.CustomerID, row.ContactName);
    }
}

Indy.Data 的语法并不完全像 C Omega,但有了 C Omega,微软拥有更改语言的完全灵活性。而 Indy.Data 则受限于 C# 的扩展方式。幸运的是,C# 的扩展能力比大多数人意识到的要大得多。

未来的文章

主程序集名为 Indy.Data,依赖于 System.Data (ADO.NET)。我为围绕这个库的许多文章准备了笔记和计划,包括技术方面、用法以及方法论。然而,为了首先发布“想法”,我正在写这篇基础介绍性文章。我知道本文中有很多内容没有涵盖,但别担心,它们将在未来的文章中出现。

既非鱼也非肉

严格来说,Indy.Data 既不是 O/R 映射器也不是代码生成器。相反,Indy.Data 是一种不同的、同时又相似的东西。在本文中,这一点尚不完全清楚,因此,对于本文而言,请将 Indy.Data 仅视为一个数据访问库 (DAL)。

Indy.Data 支持代码生成,以使 DAL 对象与数据库保持同步,但它不会创建业务逻辑的映射,也不会在生成过程中生成自己的 SQL。

Indy.Data 也不是 O/R 映射器。Indy.Data 更灵活,因为它不限于参数化查询或存储过程。Indy.Data 也不提供查询构造和映射,而是依赖数据库中现有的对象或提供的 SQL。

Indy.Data 为数据库表、视图、存储过程或 SQL 语句提供一对一的对象包装器。这些对象可以随时重新生成,以与数据库更改保持同步。在未来的文章中,我将解释 Indy.Data 如何以略微不同的方式执行代码生成器或 O/R 映射器的相同功能。然而,在本篇文章的范围内,请假设 Indy.Data 是 ADO.NET command 对象的先进实现。Indy.Data 适用于所有可以使用 ADO.NET command 对象的地方。

极限数据库

ADO.NET 是一个很好的数据库连接库。但是,使用 ADO.NET 仍然采用标准方法,即数据连接不是类型安全的,并且与数据库的绑定是松散的。字段通过字符串字面量或数字索引进行绑定。所有类型都将被转换为所需的类型。数据库的更改将导致应用程序中的错误。然而,由于松散绑定,这些错误直到运行时才会发现。除非可以执行每一个执行点和逻辑组合的测试,否则错误将不会出现,直到客户发现它们。

因此,作为开发人员,我们已经习惯于永远不要更改数据库。这导致数据库效率低下,包含过时数据,包含重复数据,以及为了添加新功能而包含许多 hack。事实上,这是一种错误的方法,但我们都已习惯于接受这是开发中的一个事实。

然而,如果我们像处理其他代码一样使用紧密绑定的方法,我们可以升级和更新我们的数据库,使其与我们的系统共同成长。只需更改数据库,然后重新编译。您的系统将在编译时找到所有新创建的冲突,然后可以轻松地更改功能以满足新的需求。我称之为“极限数据库”或 XDB,这与极限编程或 XP 一致。

使用内置的 ADO.NET 命令从查询中读取如下所示

IDbCommand xCmd = _DB.DbConnection.CreateCommand();
xCmd.CommandText = "select \"CustomerID\", \"NameLast\", \"CountryName\""
    + " from \"Customer\" C"
    + " join \"Country\" Y on Y.\"CountryID\" = C.\"CountryID\""
    + " where \"NameLast\" = @PNameLast1 or \"NameLast\" = @PNameLast2";
xCmd.Connection = _DB.DbConnection;
xCmd.Transaction = xTx.DbTransaction;

IDbDataParameter xParam1 = xCmd.CreateParameter();
xParam1.ParameterName = "@NameLast1";
xParam1.Value = "Hower";
xCmd.Parameters.Add(xParam1);
IDbDataParameter xParam2 = xCmd.CreateParameter();
xParam2.ParameterName = "@PNameLast2";
xParam2.Value = "Hauer";
xCmd.Parameters.Add(xParam2);
using (IDataReader xReader = xCmd.ExecuteReader()) {
    while (xReader.Read()) {
        Console.WriteLine(xReader["CustomerID"] + ": " + xReader["CountryName"]);
    }
}

使用 Indy.Data 编写的相同代码如下所示

using (CustomerQry xCustomers = new CustomerQry(_DB)) {
    xCustomers.Where = CustomerQry.Col.NameLast == "Hower"
                    | CustomerQry.Col.NameLast == "Hauer";
    foreach (CustomerQry.Row xCustomer in xCustomers) {
        Console.WriteLine(xCustomer.CustomerID + ": " + xCustomer.CountryName);
    }
}

首先,让我们忽略它更短的事实。标准的 ADO.NET 也可以被包装在一个对象或方法中。我想指出的是,标准的 ADO.NET 需要使用文本字符串。事实上,上面列出的代码有一个错误。“@NameLast1”应该是“@PNameLast1”。标准 ADO.NET 代码中的错误直到代码执行时才会被检测到,并且追溯起来会更困难。而 Indy.Data 代码中的任何错误都会立即被编译器捕获,并精确定位。这使得数据库可以在开发过程中轻松演进,并且在编译时发现和定位错误,从而大大减少了 bug。

这是否可以通过代码生成或 O/R 映射器来完成?是的,但通常它们提供以下之一:

  1. 方法调用,这会产生一个不代表 Where 子句的笨拙语法,尤其是在遇到复杂子句时。
  2. 字符串参数,这又使我们回到了后期绑定的接口,导致运行时发现 bug。
  3. 数据库参数 - 如果被正确解释,参数可以提供类型安全和早期绑定,许多工具不仅做到这一点,而且依赖于这种行为。问题在于,参数严重限制了 Where 子句的灵活性,并且经常导致创建许多版本的单个对象,带有许多参数变体。

通过 Indy.Data 的方法,可以根据需要保留完整的 Where 子句形成自由度,同时保留类型安全、早期绑定和清晰语法的全部好处。

一种语言

传统上,构建数据库系统的开发人员不仅要学习一门开发语言(C#),还要精通 SQL 和存储过程语言(如 T-SQL)。要开发复杂的系统,开发人员不仅必须精通这三者,还必须在这三种语言之间分割系统逻辑。即使你有一个 DA 编写所有的存储过程和 SQL,它仍然会将你的系统逻辑分成多种语言。

使用 Indy.Data,仍然会使用 SQL,但是使用的 SQL 被简化为基本形式并隔离成离散的块(查询和视图)。然后,所有的系统逻辑都可以用 C# 编写。因为 C# 代码现在可以用来修改基本的 SQL 构建块。总之,现在你可以使用一种语言 C# 来开发你的系统。这是通过扩展 C# 语言来处理 Where 子句和 SQL 语言的其他变体来实现的。

视图

Indy.Data 的主要对象是 View

视图类型

表/视图

视图可以存在于数据库中的表或视图。

存储过程

对返回结果集的存储过程的支持尚未实现。对这些类型的存储过程的支持即将推出。

查询

自定义 SQL 可以创建并作为视图或存储过程存储在数据库中。然而,在开发过程中,管理此类视图,特别是修改视图,可能会非常困难。在这种情况下,Indy.Data 还允许开发人员指定外部于数据库的本地 SQL 语句,这些语句随后被嵌入到 View 中。但是,SQL 保持与代码分离和隔离。

生成

视图类由外部实用程序生成。该实用程序配置为检查数据库并扫描数据库中的表、视图、存储过程和外部 SQL 语句。从中,它生成视图基类。从这些基类中,它还生成用户扩展的默认 shell。

由于生成器将类分为两部分,因此可以在不覆盖自定义代码的情况下重新生成基类。该生成器目前正在移植和更新以支持 Firebird 和 SQL Server。我预计将在几天内发布。

示例

在这第一篇文章中,所有的例子都基于一个简单的数据库。我将在以后的文章中扩展这些例子。这些例子展示了对单个表的各项操作。Indy.Data 可以接受视图以及 SQL 语句。

许多例子都来自 NUnit 测试项目。因此,您将在示例代码中看到大量的断言和其他检查。

数据库

对于这些例子,使用了以下数据库。

CREATE TABLE "Customer" (
"CustomerID" INTEGER NOT NULL,
"NameFirst" VARCHAR(40) NOT NULL,
"NameLast" VARCHAR(40) NOT NULL,
"Tag" INTEGER
);
CREATE TABLE "Country" (
"CountryID" "KeyDmn" NOT NULL,
"CountryName" VARCHAR(50) NOT NULL,
"ISOCode" CHAR(2) CHARACTER SET ASCII NOT NULL,
CONSTRAINT "PK_Country" PRIMARY KEY ("CountryID"),
CONSTRAINT "UNQ_Country_1" UNIQUE ("ISOCode"),
CONSTRAINT "UNQ_Country_2" UNIQUE ("CountryName")
);

代码示例

从视图读取

从视图读取的基本形式如下

using (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
    xCustomers.SelectAll();
    foreach (CustomerTbl.Row xCustomer in xCustomers) {
        Console.WriteLine(xCustomer.NameFirst + " " + xCustomer.NameLast);
    }
}

一旦选择了视图,就可以使用 foreach 循环遍历结果集中的行。行不会加载到内存中,而是在需要时逐个获取。

SelectAll 是 Indy.Data 内置的一个安全机制。在读取视图之前,必须选择某些内容。此规则已添加到 SelectAll 方法中,因为我们的系统中出现了许多 bug,开发人员忘记选择数据并执行了完整的查询集扫描。因此,如果从一个未被选择的视图读取,Indy.Data 会抛出异常。在这种情况下,我们希望扫描整个视图,因此调用了 SelectAll

手动读取

using (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
    xCustomers.SelectAll();
    CustomerTbl.Row xCustomer1 = (CustomerTblRow)xCustomers.Read();
    CustomerTbl.Row xCustomer2 = (CustomerTblRow)xCustomers.Read();
    Console.WriteLine(xCustomer1.NameFirst + " " + xCustomer2.NameFirst);
}

使用 Read 方法而不是 foreachRead 在到达结果集末尾时返回 null,并且也可以与 while 或其他循环机制一起使用。

由于行类与读取器分离,因此可以保存和缓存行。例如,您可能希望比较当前行与上一行。使用 Indy.Data,这很容易,您只需要存储对前一行的引用。

访问列

在前面的示例中,可以轻松访问列

string s = xCustomer1.NameFirst

NameFirst 是一个 string,与列匹配。其他列可能是 intdecimal 或任何其他类型。但是,每列都有更多属性。现在让我们看一下 Tag 列。

using (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
    xCustomers.SelectAll();
    foreach (CustomerTbl.Row xCustomer in xCustomers) {
        if (!xCustomer.Tag.IsNull) {
            Console.WriteLine(xCustomer.Tag);
        } 
        else {
            Console.WriteLine(xCustomer.NameFirst + " " + xCustomer.NameLast);
        }
    }
}

请注意,xCustomer.Tag 返回一个 int,但 xCustomer.Tag.IsNull 返回一个 bool。对于那些使用过 ADO.NET DataSets 的人来说,Indy.Data 提供的行和列的清晰隔离将是受欢迎的。

Null (空值)

“Null 是一种状态,而不是一个值”。

幸运的是,所有主要数据库都遵循这一点。然而,几乎所有的数据访问层都将访问 null 值视为错误。这迫使开发人员用 if 语句包装所有此类访问,并导致许多 bug。Null 确实是一种状态,然而 Indy.Data 在存在 null 时会返回一个默认值。但是,您可以显式检测 null 的值。例如,如果 Tagnull

Tag == 0
Tag.IsNull == true

因此,您可以在不引发异常的情况下读取 Tag 的值,但仍可检测 null 作为一种状态。数字返回 0,而字符串返回 ""。在我看来,这极大地减少了 bug,但并未降低 null 的有用性,也没有违反 null 是一种状态的事实。这是 Indy.Data 中一个有意的设计特性,而不是一个意外的副作用或妥协。

插入一行

using (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
    CustomerTbl.Row xCustomer = new CustomerTbl.Row();
    xCustomer.CustomerID = xCustomers.NewKey();
    xCustomer.NameFirst = "First";
    xCustomer.NameLast = "Last";
    xCustomers.Insert(xCustomer);
}

更新一行

using (CustomerTbl xCustomers = new CustomerTbl(_DB)) {
    xCustomers.SelectFirstName("Chad");
    CustomerTbl.Row xCustomer = xCustomers.Read();
    // SQL Server can only have one data command per 
    // connection so we must close before update
    xCustomers.Close(); 
    xCustomer.NameLast = "Hauer";
    xCustomers.Update(xCustomer);
}

更新一行类似于插入。但是,要更新一行,我们必须首先以某种方式从数据库获取该行。在此更新中,选择了姓氏为 Chad 的第一行。然后更改其姓氏。在更新期间,仅更新姓氏。Indy.Data 检测到已更新哪些列,并发出高效的 SQL 来仅更新这些列。

过滤数据

在更新 Row 时,调用了 SelectFirstName。这是一个在 CustomerTbl 的用户代码部分添加的方法。Indy.Data 可以允许开发人员在此处指定 Where 子句的代码,但出于方法论原因(将在未来的文章中解释),它将它们声明为 protected。这意味着所有过滤条件必须隔离到方法中,并添加到视图的用户代码中。SelectFirstName 的代码如下

Where = Col.NameFirst == aName;

请注意,Where 子句是纯 C# 代码,而不是动态 SQL。这是一个非常简单的例子,但它可以做得更复杂。请注意,甚至支持括号分组。

Where = 
(CustomerTbl.Col.NameFirst == "Chad" | CustomerTbl.Col.NameFirst == "Hadi")
& CustomerTbl.Col.CustomerID > aMinID & CustomerTbl.Col.Tag != View.Null;

甚至 LIKE 运算符也被映射到了 %

Where = Col.NameFirst % aName;

Where 子句甚至可以动态构建。

Where = CustomerTbl.Col.CustomerID > aMinID;
if (aNullTagsOnly) {
    _Where = _Where & CustomerTbl.Col.Tag == View.Null;
}

当然,括号和其他运算符仍然适用。在上面的代码中也可以使用 &|,以及括号分组。

其他操作

Indy.Data 支持许多其他操作和功能。本文仅为介绍,我将在不久的将来撰写其他文章来扩展这些内容。

DataBindings

使用 WinForms 或 WebForms 进行 DataBindings?该库也可以支持。它甚至可以用于填充 DataSet 以用于多层应用程序。我将在另一篇文章中介绍。

支持的数据库

该库被设计为数据库无关的,并且可以使用 ADO.NET 与任何 SQL 数据库配合使用。目前,该库已在 Firebird 和 SQL Server 上进行了测试。该库应与 Oracle、Interbase 等配合使用,只需少量修改。

实施说明

Indy.Data 的根源可以追溯到我最初于 1998 年在 Delphi 中构建的一个库。当然,因为 Delphi 不支持隐式转换器、运算符重载以及我使用的许多其他功能,所以它远不如 Indy.Data 那么流畅。在 .NET 中的第一个实现创建于 2003 年。

.NET 2.0

该库目前是用 .NET 1.1 编写的。在许多地方,它将极大地受益于泛型、部分类和 ADO.NET 数据提供程序工厂的使用。当 .NET 2.0 发布时,Indy.Data 将更新以利用这些功能。

C#

该库是为 C# 设计的。它也可以用于其他语言,但是像 Visual Basic .NET 这样的语言不支持隐式转换和其他 C# 语言功能,而这些功能是该库所使用的。

开源

Indy.Data 是开源的。如果您有兴趣使用或贡献 Indy.Data,我们已建立了一个 Yahoo Group,您可以在其中 加入

© . All rights reserved.