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

DRY 数据库交互 (.NET 2.0)

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.83/5 (6投票s)

2009年10月15日

CPOL

5分钟阅读

viewsIcon

33438

downloadIcon

133

在与数据库打交道时,遵循“不要重复自己”(DRY)原则。简单易懂的代码,可以减少常见错误的发生。

引言

DRY 编程

"DRY"(Don't Repeat Yourself,不要重复自己)编程原则可以很好地概括为“不要重复自己”。《卓越程序员》(The Pragmatic Programmer)一书中对此有更精确的简短描述:“系统中的每一份知识都必须有一个单一、明确、权威的表示。”

在这里,我将“DRY化”一些常见的代码,并分享一种简单、低成本的方式,让简单的数据库交互更加清晰、便捷,且不易出错。

更新 - 2009 年 10 月 16 日

细心的读者指出,本文中的解决方案与早期版本的 Microsoft 数据库访问应用程序块(Database Access Application Block)相似。我之前从未研究过它,于是我查看了一下。令我非常惊讶的是,我得出了与他们相同的结论和解决方案。这对我来说是好消息,也是坏消息。我很高兴看到自己独立地得出了“最佳实践”项目所提供的相同解决方案。但我也很失望地发现我的工作是重复的。

对于 3.5 框架及更高版本的应用程序,我建议使用 Patters & Practices - Enterprise Library 的数据访问应用程序块。本文中的解决方案是为一个仍停留在 .NET 2.0 框架的应用程序而创建的。它基本上是 Microsoft DAAB v2 的一个“精简版”。

问题背景 - 约束和目标

值得一提的是,很少有解决方案是完全没有约束的。本解决方案设计用于 C# Web 服务或 Web 应用程序。虽然它具有灵活性,但它假定连接字符串存储在 App.configWeb.config 文件中。它还假定连接字符串会定义连接池,因此创建和关闭数据库连接的成本很低。这意味着当一个连接关闭时,它实际上并没有真正关闭;它被返回到 .NET 受管连接池中,可供下一个连接请求使用。

我还想提一下,通常我更喜欢使用一个好的 ORM(对象关系模型)框架来与数据交互。然而,这段代码是为一个现有的、小型项目创建的,引入 ORM 会显得“杀鸡用牛刀”。

说了这么多,让我们开始吧。

标准 - 样板数据库代码

你在各地都会看到类似下面的代码。它是正确的,并且能完美工作。问题在于,只有大约 4 到 5 行代码实际上是用来解决我们的业务需求的。其余的都是样板代码。看看吧:

// conn and reader declared outside try
// block for visibility in finally block
SqlConnection conn   = null;
SqlDataReader reader = null;

string inputCity = "London";
try
{
    // instantiate and open connection
    conn =  new SqlConnection("Server=(local);DataBase=Northwind;" + 
                              "Integrated Security=SSPI");
    conn.Open();

    // Declare command object with parameter
    SqlCommand cmd = 
      new SqlCommand("select * from Customers where city = @City", conn);

    // Add new parameter to command object
    cmd.Parameters.AddWithValue("@City", inputCity);

    // Get data reader
    reader = cmd.ExecuteReader();

    // write each record
    while(reader.Read())
    {
        Console.WriteLine("{0}, {1}", 
          reader["CompanyName"], reader["ContactName"]);
    }
}
finally
{
    // close reader
    if (reader != null)
    {
        reader.Close();
    }

    // close connection
    if (conn != null)
    {
        conn.Close();
    }
}

如果这是为了一个一次性的解决方案,那就没问题。然而,当项目中多次重复这种类型的代码时,就会变得浪费且危险。为什么危险?如果有人遗漏了其中一个 Close() 语句怎么办?如果他们忘记检查变量是否已赋值怎么办?如果他们不使用 try..finally 块怎么办?关键在于,所有的“样板代码”仍然很重要,并且必须正确执行才能防止问题。

如果我们能够移除大部分样板代码,同时又能确保清理工作不会被遗漏或执行不正确怎么办?如果这意味着我们不必编写太多代码来解决业务问题,那又会怎样?

DRY 解决方案

让我们直接开始,看看我们等效的 DRY 代码是什么样的:

string inputCity = "London";

// Track our parameter
CommandParams insertParams = new CommandParams("@City", inputCity);

// Declare the query and execute
using (SqlDataReader reader = 
       SqlClientHelper.ExecuteReaderSelect(
       "select * from Customers where city = @City", insertParams))
{
    // write each record
    while(reader.Read())
    {
        Console.WriteLine("{0}, {1}", 
          reader["CompanyName"], reader["ContactName"]);
    }
} // returned reader is cleaned up.

我们移除了样板代码,只剩下解决我们业务问题的关键代码。所有重要的事情仍然在做,只是现在重要的样板代码不会被搞砸了!

注意CommandParams 是一个通用的 SqlParameter 对象列表。我们想要它,因为它能让我们干净地将参数与 SqlConnectionSqlCommand 对象分开。它还具有一些有用的重载构造函数,可以使使用它更加便捷。

存储过程

调用存储过程同样简单:

CommandParams paramValues = new CommandParams();
paramValues.Add("@CityName", city);
paramValues.Add("@State", state);
using (SqlDataReader reader = SqlClientHelper.ExecuteReader(
        "My_Stored_Procedure", paramValues, CommandType.StoredProcedure))
{
    // if there is anything to read...
    if (reader.Read())
    {
        // Take some action on the stored procedure results
    }
}// connection will close and dispose here

在存储过程示例中,我们直接调用 ExecuteReader 并提供 CommandType。此外,在此示例中,我们不期望得到一组行,只期望得到一行。因此,如果 Read() 成功,我们可以采取一些措施。

真正的工作在哪里发生?

所有真正的工作和主要节省都发生在 ExecuteReader 方法中:

public static SqlDataReader ExecuteReader(string commandText, 
              CommandParams paramValues, CommandType cmdType)
{
    SqlConnection connection = null;
    SqlDataReader dataReader = null;
    SqlCommand command = null;
    try
    {
        connection = new SqlConnection(GetConnectionString());
        command = new SqlCommand(commandText, connection);
        command.CommandType = cmdType;
        // Add the given params (if any) to the command object
        command.Parameters.AddRange(paramValues.ToArray());
        // Open the DB connection.
        connection.Open();
        // After executing the command, immediately close the connection.
        // This helps to ensure that connections are not unintentionally left open.
        dataReader = command.ExecuteReader(CommandBehavior.CloseConnection);
    }
    catch (SqlException se)
    {
        if (dataReader != null)
            dataReader.Dispose();
        // If errors occur, ensure connection is closed.
        if (connection != null)
            connection.Close();
        throw se;
    }
    return dataReader;
}

你是否认出了很多样板代码?另一个特别的说明是使用了 CommandBehavior.CloseConnection。这意味着当返回的读取器关闭时,它将自动关闭关联的数据库连接。仅这一点就很方便。现在,当与 using() 块结合使用时,这一切都会自动为我们完成!

using (SqlDataReader reader = SqlClientHelper.ExecuteReaderSelect(
       "select * from Customers where city = @City", 
       new CommandParams("@City", cityName)))
{
    // some work
} // returned reader is cleaned up.

当执行到达 using 块的结束大括号时,读取器的 Dispose 方法会自动调用。由于 CommandBehavior 的设置,这将关闭读取器的连接以及关联的 SqlConnection。我们只是让“做正确的事”对我们自己以及将来维护我们代码的人来说变得简单。这也意味着我们大大降低了在这段代码中犯错的可能性。

你可能还注意到在创建 SqlConnection 对象时使用了 GetConnectionString() 调用。这是假定连接字符串可以从 App.configWeb.config 文件中读取而创建的。如果你有其他需求,可以使用 AssignConnectionSting() 方法显式设置。

结束语

本文中的代码和附带的文件对我 DRY 化常见数据库代码非常有帮助。它帮助我更容易地始终做正确的事情,并且更难出错。这个好处也惠及了在我之后维护我代码的初级开发者。

附带源代码文件

附带的源代码文件设计得易于移植,并能适应其他项目和其他开发者的需求。请随意根据需要进行修改。

另外请注意,源代码文件中还包含其他数据库辅助例程。请查看它们,看看它们是否能帮助到你。

© . All rights reserved.