DRY 数据库交互 (.NET 2.0)






3.83/5 (6投票s)
在与数据库打交道时,遵循“不要重复自己”(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.config 或 Web.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
对象列表。我们想要它,因为它能让我们干净地将参数与 SqlConnection
和 SqlCommand
对象分开。它还具有一些有用的重载构造函数,可以使使用它更加便捷。
存储过程
调用存储过程同样简单:
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.config 或 Web.config 文件中读取而创建的。如果你有其他需求,可以使用 AssignConnectionSting()
方法显式设置。
结束语
本文中的代码和附带的文件对我 DRY 化常见数据库代码非常有帮助。它帮助我更容易地始终做正确的事情,并且更难出错。这个好处也惠及了在我之后维护我代码的初级开发者。
附带源代码文件
附带的源代码文件设计得易于移植,并能适应其他项目和其他开发者的需求。请随意根据需要进行修改。
另外请注意,源代码文件中还包含其他数据库辅助例程。请查看它们,看看它们是否能帮助到你。