在学习与数据库对话时,每个程序员必须做两件基本的事情:关闭连接和参数化查询。这些项目与运行 sql 语句和接收结果的实际过程是分开的,但它们仍然是绝对必要的。出于某种原因,互联网上提供的大多数教程只是掩盖了它们,甚至完全弄错了,这可能是因为对于任何高级到足以编写教程的人来说,这都是第二天性。我的目标是向您展示如何构建整个过程,包括这些额外的基础知识,以一种更容易做到这一点并且每次都做到正确的方式。
首先要意识到在一个方法中隐藏数据访问代码是不够的:我们实际上想为此构建一个单独的类(甚至是类库)。通过创建一个单独的类,我们可以在该类中将我们的实际连接方法设为私有,这样只有该类中的其他方法才能连接到数据库。这样,我们设置了一个看门人,强制程序中的所有数据库代码通过批准的通道运行。就我上面谈到的两个问题而言,正确获取看门人代码,您的整个程序也将始终正确。所以这是我们的开始:
public class DataLayer
{
private DbConnection GetConnection()
{
//This could also be a connection for OleDb, ODBC, Oracle, MySQL,
// or whatever kind of database you have.
//We could also use this place (or the constructor) to load the
// connection string from an external source, like a
// (possibly-encrypted) config file
return new SqlConnection("connection string here");
}
}
到目前为止,我们还没有真正解决引言中的任何一个基本问题。到目前为止,我们所做的只是让自己编写代码,使我们能够在以后实施良好的实践。所以让我们开始吧。首先,我们将担心如何强制关闭您的连接。我们通过添加一个运行查询、返回结果并确保完成后关闭连接的方法来做到这一点:
private DataTable Query(string sql)
{
var result = new DataTable();
using (var connection = GetConnection())
using (var command = new SqlCommand(sql, connection)
{
connection.Open();
result.Load(command.ExecuteReader(CommandBehavior.CloseConnection));
}
return result;
}
您可以添加其他类似的方法来返回标量数据或根本不返回数据(用于更新/插入/删除)。暂时不要太执着于这段代码,因为它仍然被破坏了。我会在一分钟内解释为什么。现在,让我指出这个方法仍然是私有的。我们还没有完成,因此我们不希望此代码可用于程序的其他部分。
我要强调的另一件事是using 关键字。此关键字是在 .Net 和 C# 中声明变量的强大方法。 using 关键字在变量声明下创建一个范围块。在范围块的末尾,您的变量被释放。请注意,这有三个重要部分。首先,这实际上只适用于非托管资源,如数据库连接;内存仍然以通常的方式收集。第二个是变量被释放即使抛出异常。这使得该关键字适用于对时间敏感或受严格限制的资源,例如数据库连接,而无需在附近使用单独的 try/catch 块。最后一点是关键字在 .Net 中使用了 IDisposable 模式。您现在不需要了解 IDisposable 的所有信息:只需知道数据库连接实现(认为:继承) IDisposable 接口,因此可以使用 using 块。
您不必在代码中使用using 关键字。但如果你不这样做,处理连接的正确方法如下所示:
SqlConnection connection;
try
{
connection = new SqlConnection("connection string here");
SqlCommand command = new SqlCommand("sql query here", connetion);
connection.Open();
SqlDataReader reader = command.ExecuteReader();
//do something with the data reader here
}
finally
{
connection.Close();
}
即使这仍然是简单的版本。您还需要在 finally 块中进行额外检查,以确保您的连接变量有效。 using 关键字是表达这一点的一种更简洁的方式,它确保您每次都能正确使用模式。我想在这里说明的是,如果您只是调用connection.Close(),而没有确保程序实际到达该行的保护,你已经失败了。如果您的 sql 代码在没有 try/finally 或 using 的保护的情况下抛出异常,您将永远无法到达 .Close() 调用,因此可能会使连接保持打开状态。经常这样做,您可以将自己锁定在数据库之外!
现在让我们构建一些公共的东西:你可以从其他代码中实际使用的东西。正如我之前所暗示的,您为应用程序编写的每个 sql 查询都将使用它自己的方法。下面是一个简单查询的示例方法,用于从 Employee 表中获取所有记录:
public DataTable GetEmployeeData()
{
return Query("SELECT * FROM Employees");
}
哇,这很简单……单行函数调用,我们就从数据库中返回了数据。我们真的在取得进展。不幸的是,我们仍然缺少一块拼图:你看,想要退回整张桌子是非常罕见的。通常,您会希望以某种方式过滤该表,并可能将其与另一个表连接起来。让我们更改此查询以返回名为“Fred”的虚构员工的所有数据:
public DataTable GetFredsEmployeeData()
{
return Query("SELECT * FROM Employees WHERE Firstname='Fred'");
}
仍然很容易,但这错过了我们正在努力实现的精神。您不想为每个可能的员工姓名构建另一种方法。你想要更多这样的东西:
public DataTable GetEmployeeData(string FirstName)
{
return Query("SELECT * FROM Employees WHERE FirstName='" + FirstName + "'");
}
哦哦。现在我们有一个问题。有那个讨厌的字符串连接,只是在等待有人出现并在您的应用程序的 FirstName 字段中输入文本';Drop table employees;--(或更糟)。处理这个问题的正确方法是使用查询参数,但这就是棘手的地方,因为前几段我们构建了一个只接受完成的 sql 字符串的查询方法。
很多人想要编写一个类似 Query 方法的方法。我认为几乎每个数据库程序员在其职业生涯的某个阶段都会受到这种模式的诱惑,不幸的是,在您添加一种接受 sql 参数数据的方法之前,这完全是错误的。幸运的是,有许多不同的方法可以解决这个问题。最常见的就是在方法中加一个参数,让我们传入sql数据来使用。为此,我们可以传递一个 SqlParameter 对象数组、一个键/值对集合,甚至只是一个对象数组。任何这些都足够了,但我认为我们可以做得更好。
我花了很多时间研究不同的选项,并缩小了我认为对于 C# 最简单、最有效、(更重要的是)最准确和可维护的选项的范围。不幸的是,它确实需要您了解 C# 中一种更高级的语言功能的语法:匿名方法/lambda(实际上是:委托,但我很快就会展示一个 lambda)。这个特性允许你做的是在另一个函数中定义一个函数,用一个变量保持它,将它传递给其他函数,然后在你空闲的时候调用它。这是一个有用的功能,我将尝试演示。以下是我们将如何修改原始 Query() 函数以利用此功能:
private DataTable Query(string sql, Action<SqlParameterCollection> addParameters)
{
var result = new DataTable();
using (var connection = GetConnection())
using (var command = new SqlCommand(sql, connection)
{
//addParameters is a function we can call that was as an argument
addParameters(command.Parameters);
connection.Open();
result.Load(command.ExecuteReader(CommandBehavior.CloseConnection));
}
return result;
}
注意新的Action<SqlParameterCollection> 参数。不要介意< > 部分。如果你不熟悉泛型,你现在可以假装它是类名的一部分。重要的是,这种特殊的 Action 类型允许我们将一个函数(在这种情况下,一个以 SqlParameterCollection 作为参数的函数)传递给另一个函数。这是从我们的 GetEmployeeData() 函数中使用时的样子:
public DataTable GetEmployeeData(string firstName)
{
return Query("SELECT * FROM Employees WHERE FirstName= @Firstname",
p =>
{
p.Add("@FirstName", SqlDbType.VarChar, 50).Value = firstName;
});
}
这一切的关键在于 Query() 函数现在可以将传递给它的父 GetEmployeeData() 函数的 firstName 参数连接到 sql 字符串中的 @FirstName 表达式。这是使用内置于 ADO.Net 和您的 sql 数据库引擎中的功能来完成的。最重要的是,它发生的方式可以防止任何 sql 注入攻击的可能性。同样,这种奇怪的语法并不是发送参数数据的唯一有效方式。仅发送您迭代的集合可能会更舒服。但我确实认为这段代码很好地将参数代码保持在查询代码附近,同时还避免了额外的工作构建,然后再迭代(重建)参数数据。
我将完成(终于!)两个简短的项目。第一个是不带参数调用新查询方法的语法:
public DataTable GetAllEmployees()
{
return Query("SELECT * FROM Employees", p => {});
}
虽然我们也可以将其作为原始 Query() 函数的重载来提供,但在我自己的代码中我不想这样做,因为我想与其他开发人员沟通,他们应该寻求参数化他们的代码,并且不要用字符串连接偷偷摸摸。
其次,此答案中概述的代码仍未完成。还有一些重要的弱点需要解决。一个例子是,使用数据表而不是数据读取器会迫使您将每个查询的整个结果集一次全部加载到内存中。我们可以做一些事情来避免这种情况。我们还没有讨论插入、更新、删除或更改,我们还没有解决如何组合复杂的参数情况,例如,我们可能想要添加代码来过滤姓氏,但前提是数据用户实际上可以使用姓氏过滤器。虽然这可以很容易地适应所有这些场景,但我认为此时我已经完成了最初的目标,所以我将把它留给读者。
最后,请记住您必须做的两件事:通过 finally 块关闭您的连接,并参数化您的查询。希望这篇文章能让你走上成功之路。