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

面向对象程序员的 ADO.NET – 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (93投票s)

2006 年 1 月 10 日

CPOL

16分钟阅读

viewsIcon

412775

downloadIcon

3313

本文将介绍如何实现这些目标——将 ADO.NET 用作薄型数据传输层,同时仍能利用 .NET 用户界面控件的数据绑定功能。事实证明,这非常容易。

引言

在 .NET 的所有组成部分中,我一直觉得 ADO.NET 最令人费解。这可能因为我喜欢面向对象编程。ADO.NET 似乎与面向对象的方法不符,尽管它本身也包含一些面向对象的功能。我一直弄不清楚如何将 ADO.NET 融入到对象设计中。

然后有一天晚上,在我去和家人吃晚饭的路上,我突然有了顿悟。一个声音从不知道哪里冒出来说:“ADO.NET 不适用于对象设计,因为它根本就不应该和对象一起使用!”太好了,我想,我开始听到幻觉了。但事实证明那只是我的妻子,她自己也是 .NET 的佼佼者。

“ADO.NET 是旧版 VB6 做法的延续,”她说。“Visual Basic 是快速组装东西的好方法。只要这些东西不太复杂,它就一直都很有效。ADO.NET 允许同样的做法处理更复杂的问题,但它本质上仍然是一种‘组装式’技术。你永远无法用它来打造一块瑞士手表,而用对象就可以。”

好吧,这对我来说确实很有道理。在继续交谈的过程中,我们得出结论,在面向对象设计中使用 ADO.NET 的最佳方法是根本不使用它——或者至少尽量少用。毕竟,在“对象世界”里,我们到底需要 ADO.NET 来做什么?无非就是将数据从数据库传输到我们的对象,再从对象传输回数据库。这意味着我们只需要 ADO.NET 提供功能中的十分之一。

好吧,所以我们发誓不再使用数据集(无论是强类型还是其他类型),并承诺将 ADO.NET 用作对象和数据存储之间的薄型传输层。但我们为此付出了代价。.NET 用户界面控件是围绕数据绑定概念构建的,如果我们能利用它,那么将我们的对象模型连接到应用程序其余部分的繁琐工作就会变得容易得多。我们想要的其实是两全其美。

本文将介绍如何实现这些目标——将 ADO.NET 用作薄型数据传输层,同时仍能利用 .NET 用户界面控件的数据绑定功能。事实证明,这非常容易,并且可以使面向对象程序员免于阅读一本又一本书,徒劳地试图弄懂 ADO.NET。

AdoNetDemo 应用程序

在本文中,我们将使用一个名为 AdoNetDemo 的应用程序来阐述我们将要讨论的原理和技术。AdoNetDemo 是一个极简的应用程序,旨在演示 ADO.NET 与简单的对象模型在典型的 CRUD(创建、检索、更新和删除)操作中的交互。

AdoNetDemo 构建在一个项目经理可能使用的对象模型之上。该对象模型围绕一个 ProjectList 对象构建,该对象包含一个 ProjectItem 对象集合。每个 ProjectItem 都有几个原生属性和一个名为 Steps 的集合属性。Steps 属性的类型为 StepList,它包含一个 StepItem 对象集合。

以下是 ProjectList 及其子对象的图形视图

本文的重点将放在 CRUD 操作上,因此为了保持简单,我们将对对象设计做一些调整。我们不会构建数据访问层;为了保持简单,我们将把所有演示方法都放在 Form1 中。我们的用户界面将相当原始——仅仅足够演示我们要讨论的点。

为了保持简单,我们将不采取通常期望的安全措施。例如,我们将使用动态 SQL 查询,并且不会对其进行转义。显然,在生产环境中,您会想使用存储过程,或者至少对所有查询字符串进行转义。

ADO.NET 作为查询引擎

好了,废话少说,让我们从基础开始。作为对象程序员,我们真正需要 ADO.NET 来做什么?我们只需要它作为一个查询引擎,一个处理我们 SQL 查询的对象。ADO.NET 被设计为一个功能齐全的数据管理器。因此,它可以组织数据、维护事务等。但是,我们对象程序员会将这些功能封装到我们的业务对象模型中。

顺便说一句,要学习如何做到这一点,请参阅 Rocky Lhotka 的出色著作 **Business Objects**…(有 VB 和 C# 版本)。

这意味着我们不需要,甚至不想要 ADO.NET 的大部分功能。它们的存在是为了支持数据驱动的编程方法,而不是我们偏爱的面向对象的方法。所以,让我们抛弃我们不需要的 ADO.NET 的 80%,专注于对我们有用的 20%。由于我们只需要一个基础数据引擎,我们将把 ADO.NET 剥离到最基本的状态。剩下的,看起来更像过去 ADO 的样子。

为了我们的目的,ADO.NET 的工作方式如下:

  • 我们要连接到数据库,所以我们需要一个 Connection 对象。
  • 我们要将 SQL 查询传递给数据库,所以我们需要一个 Command 对象。
  • 如果我们的查询返回结果集,我们需要一个用于存放返回记录的容器。所以我们需要 DataReader(用于扁平结果集)或 DataSet(用于包含层次结构)。
  • 如果使用 DataSet,我们还需要一个 DataAdapter 对象。
  • 最后,如果我们执行一个不返回结果集的命令(例如 INSERT、UPDATE 或 DELETE 查询),我们就不需要容器或数据适配器。我们只需在 Command 对象上调用 Execute 方法。

换句话说,在大多数情况下,我们只需要三个或四个对象。这真的非常简单。

我们如何使用这些对象也非常简单。我们连接到数据库,获取一个 DataReader 或填充一个 DataSet,然后关闭连接。或者,我们连接到数据库,执行一个命令,然后关闭连接。总而言之,这是一个非常直接的模型,与老式 ADO 相比,复杂性并没有增加多少。

加载扁平数据

让我们从一个简单的例子开始:我们想将单个表加载到一个对象集合中——一个“扁平”数据加载。打开 AdoNetDemo,然后单击“Flat ‘Select’ Query”按钮。您应该会看到以下内容:

应用程序显示它已从数据库加载了三个项目到 ProjectList 集合中。Results 框显示了每个项目的 ID 和名称。请注意,它没有显示每个项目的步骤。这是因为我们没有加载它们——我们只是对项目本身进行了扁平数据加载。

以下是扁平数据加载在代码中的样子:

public ProjectList GetProjects()
{
    // Create a Projects list
    ProjectList projects = new ProjectList();

    // Set SQL query to fetch projects
    string sqlQuery = "Select * from Projects";

    // Create and open a connection
    SqlConnection connection = new SqlConnection(m_ConnectionString);
    connection.Open();

    // Create a Command object
    SqlCommand command = new SqlCommand(sqlQuery, connection);

    // Use the Command object to create a data reader
    SqlDataReader dataReader = command.ExecuteReader();

    // Read the data reader's rows into the ProjectList
    if (dataReader.HasRows)
    {
        while (dataReader.Read())
        {
            // Create a new project
            ProjectItem project = new ProjectItem();
            project.ID = dataReader.GetInt32(0);
            project.Name = dataReader.GetString(1);

            // Add it to the Projects list
            projects.Add(project);
        }
    }

    // Close and dispose
    command.Dispose();
    connection.Close();
    connection.Dispose();

    // Set return value
    return projects;
}

首先,该方法创建一个 ProjectList 来保存我们的项目。然后,它创建一个简单的 SQL 查询来获取所有项目。接下来,它创建一个 Connection 对象并打开它以连接到数据库。

请注意,在 AdoNetDemo 中,连接字符串存储在 Form1 代码的 Declarations 部分作为一个成员变量。在生产应用程序中,您会想从配置文件中读取连接字符串,并进行适当的安全处理。

现在我们有了查询和数据库连接,该方法使用这些项创建一个 Command 对象。然后,该方法调用该对象的 ExecuteReader() 方法,该方法返回一个 DataReader 对象。

DataReader 是与 Command 对象关联的 SQL 查询匹配的记录的仅向前导出。它通常被称为“firehose cursor”,这个描述非常贴切。它的优点是非常快速,并且对它使用的资源要求不高。这使其非常适合将记录简单地传输到对象集合。

DataReader 通常使用 while 循环来处理,而不是 foreach 循环。这是因为 DataReader 没有迭代器或行集合。它只能一次一行地从上到下遍历结果表。当它到达最后一行时,其 Read() 方法将被设置为 false。

方法的其余部分是创建每个行的一个对象并将字段值从行加载到对象的直接代码。在读取完行之后,新的项目被添加到项目列表中。添加完最后一行后,代码会关闭 Connection 对象并释放它以及我们使用的 Command 对象。

这个“关闭并释放”步骤非常重要,特别是如果您计划将大量表加载到大量集合中。Connection 和 Command 对象都是非托管对象,必须显式关闭并释放它们,以便垃圾收集器可以释放它们使用的资源。

请注意,为了保持代码简单,我们没有将其包装在 try-catch 块中。在生产环境中,您应该始终这样做。将传输代码包装在“try”块中,并将错误处理代码包装在“catch”块中。最后,将关闭和释放代码包装在“finally”块中。这样,无论获取成功还是失败,您都可以确信连接已得到释放。

加载层次化数据

那么,我们如何加载包含层次结构呢?项目本身对我们来说作用不大——我们实际上想要每个项目及其步骤。这时 ADO.NET 内置的一些数据管理功能就派上用场了。

要查看层次化数据加载的结果,请打开 AdoNetDemo 并单击“Hierarchical ‘Select’ Query”按钮。您应该会看到与扁平数据加载类似的结果,但每个项目的步骤都列在项目下方。

从数据库加载包含层次结构曾经是一项痛苦的任务。基本上,我会将项目加载到 ProjectList 中。然后我会遍历 ProjectList;对于列表中的每个项目,我都会对数据库运行一个查询来加载每个项目的步骤。这意味着要为列表中的每个项目运行一个不同的数据库查询。一度,我尝试在 ADO 下编写“层次查询”,但那充其量只是一次令人沮丧的尝试。

ADO.NET 将所有这些简化为相当简单的事情:将父表和子表加载到 DataSet 中,并在表之间创建“数据关系”。下面是代码中的样子:

public ProjectList GetProjectsAndSteps()
{
    // Create a Projects list
    ProjectList projects = new ProjectList();

    // Set SQL query to fetch projects
    string sqlQuery = 
      "Select * from Projects; Select * from Steps";

    // Create dataset
    DataSet dataSet = new DataSet();

    // Populate dataset
    using (SqlConnection connection = 
           new SqlConnection(m_ConnectionString))
    {
        SqlCommand command = new SqlCommand(sqlQuery, connection);
        SqlDataAdapter dataAdapter = new SqlDataAdapter(command);
        dataAdapter.Fill(dataSet);
    }

    // Set dataset table names
    dataSet.Tables[0].TableName = "Projects";
    dataSet.Tables[1].TableName = "Steps";

    // Create a data relation between projects
    // (parents) and steps (children)
    DataColumn parentColumn = 
       dataSet.Tables["Projects"].Columns["ProjectID"];
    DataColumn childColumn = 
       dataSet.Tables["Steps"].Columns["ProjectID"];
    DataRelation projectsToSteps = 
       new DataRelation("ProjectsToSteps", 
       parentColumn, childColumn);
    dataSet.Relations.Add(projectsToSteps);

    // Create a Projects collection from the data set
    ProjectList projectList = new ProjectList();
    ProjectItem nextProject = null;
    StepItem nextStep = null;
    foreach (DataRow parentRow in dataSet.Tables["Projects"].Rows)
    {
        // Create new project 
        nextProject = new ProjectItem();

        // Fill in its properties
        nextProject.ID = Convert.ToInt32(parentRow["ProjectID"]);
        nextProject.Name = parentRow["Name"].ToString();

        /* Read in other fields from the record... */

        // Get its steps
        DataRow[] childRows = 
          parentRow.GetChildRows(dataSet.Relations["ProjectsToSteps"]);

        // Create StepItem objects for each of its steps
        foreach (DataRow childRow in childRows)
        {
            // Create new step
            nextStep = new StepItem();

            // Fill in its properties
            nextStep.ID = Convert.ToInt32(childRow["StepID"]);
            nextStep.Date = Convert.ToDateTime(childRow["Date"]);
            nextStep.Description = childRow["Description"].ToString();

            // Add new step to the project
            nextProject.Steps.Add(nextStep);
        }

        // Add new project to the Projects list
        projectList.Add(nextProject);
    }

    // Dispose of the DataSet
    dataSet.Dispose();

    // Set return value
    return projectList;
}

代码的开始与之前的示例非常相似。它创建了一个 ProjectList 和一个用于填充列表的 SQL 查询。但是 SQL 查询略有不同——它实际上是两个独立的查询(注意查询字符串中间的分号,它分隔了两个查询)。正如我们将看到的,这个查询将 Projects 表和 Steps 表从数据库读取到 DataSet 中,DataSet 也在方法的顶部声明。这些表在 DataSet 中保持分离;它们是数据库对应表的镜像。

读取的数据设置方式与第一个示例略有不同。与之前一样,我们声明了一个标准的 SQL Server 连接字符串。但接下来的这行是新的:

using (SqlConnection connection = new SqlConnection(m_ConnectionString))

这个“using”语句告诉 .NET,我们将使用声明的连接来处理花括号之间的所有内容。.NET 会自动打开连接,并在完成后执行自动的连接和 Command 对象关闭释放,即使获取抛出异常。

由于我们有多个表,我们将使用 DataSet 对象来保存结果集。DataSet 没有内置的数据库连接机制,因此除了 Connection 和 Command 对象之外,我们还创建一个 DataAdapter 对象来进行连接。我们使用数据适配器来“填充”数据集。

在“using”块的末尾,DataSet 与数据库断开连接,我们使用的 Connection、Command 和 DataAdapter 对象会被 .NET 自动关闭并释放。这是使用“using”块的主要好处之一。现在,我们可以使用我们断开连接的 DataSet,并将其配置为进行层次化读取。

我们需要做的第一件事是命名 DataSet 表。出于某种原因,ADO.NET 不会自动将数据库表名映射到 DataSet,也不提供指定自动映射的选项。相反,它将第一个表命名为“Table”,第二个表命名为“Table1”,依此类推。当然,我们可以在配置 DataSet 时使用这些默认名称,但如果我们将数据库表名映射到 DataSet 表,我们的代码的可读性会更好。因此,我们手动设置这些名称。

一旦我们为 DataSet 表命名,我们就可以创建一个 DataRelation 对象。数据关系的功能远不止 SQL 的 Join 子句——它在两个表之间创建父子关系。一旦我创建了一个数据关系,我就可以查看特定父行下的所有子行。换句话说,我可以遍历 Projects 表,看到属于每个 Project 的步骤,而无需运行单独的查询。

这正是我们在代码后面部分所做的。我们遍历 Projects 表,为表中的每一行创建一个新的 ProjectItem。一旦我们填充了 ProjectItem 的原生属性,我们就会通过数据关系获取项目的子行(来自 Steps 表)。我们为每个子行创建并填充一个新的 StepItem,并将其添加到项目的 StepList 中。在将步骤添加到项目完成后,我们将它添加到项目列表中。在将最后一个项目添加到项目列表后,我们返回列表。简单直接,毫不费力!

您可能已经注意到本例中的 DataSet 与我们在上一个示例中使用的 DataReader 有一些区别。一方面,我们可以使用 foreach 循环来迭代 DataSet。另一方面,我们可以通过名称而不是索引号来引用表列。

newStep.Description = childRow["Description"].ToString();

正如您可能想象的那样,这些功能都需要资源。是的,DataSet 是一个非托管对象,它有一个 Dispose() 方法。“using”块无法处理 DataSet 的关闭和释放,因为 DataSet 在“using”块结束后仍然存在。因此,在完成使用 DataSet 后,请调用 Dispose(),以便 .NET 垃圾收集器可以回收这些资源。

插入数据

插入操作相当直接。关于它们的唯一技巧是,我们通常需要获取我们添加到数据库的新记录的标识值(记录 ID)。

创建最终要持久化到数据库的新对象时,可以采取多种方法。例如,有人可能会创建对象,然后在对象被修改并保存时将新记录插入数据库。AdoNetDemo 在创建新对象时创建一个新记录,并用默认数据填充记录和对象。

打开 AdoNetDemo 并单击“Insert”Query 按钮。应用程序将向列表中的第一个项目添加一个新步骤。新步骤将位于列表末尾,其描述为“[New List]”。

“Insert”查询的工作原理如下:

public StepItem CreateStep(int projectID)
{
    // Build 'Insert' query
    string sqlQuery = String.Format("Insert into Steps (ProjectID, "
        + "Description, Date) Values({0}, '[New Step]', '{1}'); “
        + “Select @@Identity", 
        projectID, DateTime.Today.ToString("yyyy-MM-dd"));

    // Create and open a connection
    SqlConnection connection = new SqlConnection(m_ConnectionString);
    connection.Open();

    // Create a Command object
    SqlCommand command = new SqlCommand(sqlQuery, connection);

    // Execute the command
    int stepID = Convert.ToInt32((decimal)command.ExecuteScalar());

    // Close and dispose
    command.Dispose();
    connection.Close();
    connection.Dispose();

    // Create new step to match the record we just created
    StepItem newStep = new StepItem();
    newStep.ID = stepID;
    newStep.Date = DateTime.Today;
    newStep.Description = "[New Step]";

    // Set return value
    return newStep;
}

和以前一样,该方法首先构建一个 SQL 查询来执行我们需要完成的任务。并且,就像我们的第二个示例一样,查询字符串包含两个查询,它们由分号分隔。

第一个查询是常规的 SQL INSERT 查询。第二个查询值得解释:

Select @@Identity

此查询将返回添加到数据库的最后一个记录的标识值。我们需要将此值传递给为该数据库创建的伴随对象,以便该对象在保存时知道要更新哪个记录。

通常,SELECT 查询返回一个结果集,这意味着我们需要一个 DataReader 或 DataAdapter 来获取结果。但是,由于“SELECT @@Identity”查询返回一个标量值,我们可以使用 Command.ExecuteScalar() 方法来执行更新、获取标识并向我们返回标识值。

“SELECT @@Identity”查询有一个相关的怪癖——它的结果以 SQL decimal 值(没有小数!)的形式返回。因此,我们将结果强制转换为 decimal,然后将其转换为 int 以用于我们的 stepID 变量。

更新数据

更新数据与插入数据类似——我们需要执行一个操作查询,而不是返回结果集的查询。打开 AdoNetDemo 并单击“Update”Query 按钮。应用程序会将第一个项目的名称从“Project Southbury”更改为“Project NameChanged”。您可以通过单击“Undo Update”按钮将项目名称改回。

执行更新的代码非常简单:

public void UpdateProjectItem(ProjectItem project)
{
    // Build an 'Update' query
    string sqlQuery = 
        String.Format("Update Projects Set Name = '{0}' "
        + "Where ProjectID = {1}", 
        project.Name, project.ID);

    // Create and open a connection
    SqlConnection connection = 
      new SqlConnection(m_ConnectionString);
    connection.Open();

    // Create a Command object
    SqlCommand command = new SqlCommand(sqlQuery, 
                                     connection);

    // Execute the command
    command.ExecuteNonQuery();

    // Close and dispose
    command.Dispose();
    connection.Close();
    connection.Dispose();
}

这里没有什么真正的技巧,到目前为止,这个模式应该感觉很熟悉了。我们构建查询,创建并打开连接,创建 Command 对象并用查询配置它,执行命令,然后关闭并释放。到目前为止,您可能能在睡梦中完成它。

删除数据

最后,我们来到了最后一个操作,即删除。它不过是执行方式与我们刚才看到的 UPDATE 查询几乎相同的另一个操作查询。

要查看 AdoNetDemo 的功能,请打开它并单击“Delete”Query 按钮。该应用程序将删除您添加到数据库的任何新记录,并报告已删除的记录数。如果报告零条记录被删除,则表示您没有添加任何记录。

此代码也非常直接:

public int DeleteNewSteps()
{
    // Set SQL 'Delete' query
    string sqlQuery = "Delete from Steps" + 
           " Where Description = '[New Step]'";

    // Create and open a connection
    SqlConnection connection = 
       new SqlConnection(m_ConnectionString);
    connection.Open();

    // Create a Command object
    SqlCommand command = new SqlCommand(sqlQuery, connection);

    // Execute the command
    int numRowsDeleted = command.ExecuteNonQuery();

    // Close and dispose
    command.Dispose();
    connection.Close();
    connection.Dispose();
    
    // Set return value
    return numRowsDeleted;
}

此代码遵循与上一个示例几乎相同的模式。唯一的区别在于查询,以及我们利用了 ExecuteNonQuery() 返回的值。此函数始终返回受其执行的查询影响的行数。

结论

第一部分到此结束。如果您只需要知道如何将数据在数据库和对象模型之间来回移动,那么您应该已经掌握了所需的大部分知识。

在第二部分中,我们将深入探讨 .NET 用户界面中的数据绑定,这可以大大减少您需要编写的将用户界面连接到对象模型的“管道”代码量。我们还将探讨“DAO 模式”,这是当今最广泛使用的数据访问模式。我们将重构我们在第一部分编写的代码,使其更具面向对象性、更灵活且更易于维护。

© . All rights reserved.