创建数据库查询方法

我不确定是否已被删除,但我想要做的是创建一个返回查询结果的方法,以便我可以重用连接代码。 据我了解,查询返回一个对象,但我如何传回该对象? 我想将查询作为字符串参数发送到方法中,并让它返回结果,以便我可以使用它们。 这就是我在黑暗中刺伤的东西,它显然不起作用。 这个例子是我尝试使用查询结果填充列表框; 工作表名称为Employees,字段/列是名称。 我得到的错误是“Complex DataBinding接受IList或IListSource作为数据源。”。 有任何想法吗?

public Form1() { InitializeComponent(); openFileDialog1.ShowDialog(); openedFile = openFileDialog1.FileName; lbxEmployeeNames.DataSource = Query("Select [name] FROM [Employees$]"); } public object Query(string sql) { System.Data.OleDb.OleDbConnection MyConnection; System.Data.OleDb.OleDbCommand myCommand = new System.Data.OleDb.OleDbCommand(); string connectionPath; //build connection string connectionPath = "provider=Microsoft.Jet.OLEDB.4.0;Data Source='" + openedFile + "';Extended Properties=Excel 8.0;"; MyConnection = new System.Data.OleDb.OleDbConnection(connectionPath); MyConnection.Open(); myCommand.Connection = MyConnection; myCommand.CommandText = sql; return myCommand.ExecuteNonQuery(); } 

嘿! 试试这个,如果您只想将所有员工的姓名显示在listBox中,这应该可以。 我刚编辑了代码中的一些行…

 Form1() { InitializeComponent(); openFileDialog1.ShowDialog(); openedFile = openFileDialog1.FileName; lbxEmployeeNames.DataSource = Query("Select [name] FROM [Employees$]"); lbxEmployeeNames.DisplayMember = "name"; // The column you want to be displayed in your listBox. } // Return a DataTable instead of String. public DataTable Query(string sql) { System.Data.OleDb.OleDbConnection MyConnection; string connectionPath; //build connection string connectionPath = "provider=Microsoft.Jet.OLEDB.4.0;Data Source='" + openedFile + "';Extended Properties=Excel 8.0;"; MyConnection = new System.Data.OleDb.OleDbConnection(connectionPath); MyConnection.Open(); System.Data.OleDb.OleDbDataAdapter myDataAdapter = new System.Data.OleDb.OleDbDataAdapter(sql, MyConnection); DataTable dt = new DataTable(); myDataAdapter.Fill(dt); return dt; } 

在学习与数据库交谈时, 每个程序员必须做两件基本事情:关闭连接并参数化查询。 这些项与运行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关键字在变量声明下创建一个范围块。 在范围块的末尾,处理您的变量。 请注意,这有三个重要部分。 首先,这实际上只适用于非托管资源,如数据库连接; 记忆仍以通常的方式收集。 第二个是即使抛出exception也会处理变量。 这使得关键字适合与时间敏感或紧密约束的资源(如数据库连接)一起使用,而无需在附近使用单独的try / catch块。 最后一部分是关键字利用.Net中的IDisposable模式。 您现在不需要了解所有关于IDisposable的信息:只知道数据库连接实现(想想:inheritance)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代码抛出exception而没有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 + "'"); } 

哦,哦。 现在我们遇到了问题。 有那种令人讨厌的字符串连接,只是等待某人出现并输入文本';Drop table employees;-- (或更糟)进入应用程序中的FirstName字段。 处理这个的正确方法是使用查询参数,但这是它变得棘手的地方,因为我们构建了一个只接受完成的sql字符串的查询方法。

很多人都想编写一个像Query方法一样的方法。 我认为几乎每个数据库程序员都会在他们职业生涯的某个阶段被这种模式所诱惑,不幸的是,在你添加一种接受sql参数数据的方法之前,这是完全错误的。 幸运的是,有许多不同的方法来解决这个问题。 最常见的是向方法添加一个参数,允许我们传入要使用的sql数据。 为此,我们可以传递一个SqlParameter对象数组,一组键/值对,甚至只是一个对象数组。 任何这些都足够了,但我认为我们可以做得更好。

我花了很多时间研究不同的选项,并且我已经缩小了我认为最简单,最有效,(更重要的)最准确和可维护的C#选项。 不幸的是,它确实需要你理解C#中一个更高级的语言特性的语法:anonymous methods / lambdas(真的:委托,但我很快就会展示一个lambda)。 这个function允许你做的是在另一个函数中定义一个函数,用变量保持它,将它传递给其他函数,并在闲暇时调用它。 这是一个有用的function,我将尝试演示。 以下是我们将如何修改原始Query()函数以利用此function:

 private DataTable Query(string sql, Action 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参数。 不介意< >部分。 如果您不熟悉generics,那么您现在可以假装它是类名的一部分。 重要的是这个特殊的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数据库引擎内置的function完成的。 最重要的是,它以防止sql注入攻击的任何可能性的方式发生。 同样,这种奇怪的语法并不是发送参数数据的唯一有效方法。 发送一个迭代的集合可能会更舒服。 但我确实认为这段代码可以很好地保持参数代码靠近查询代码,同时避免额外的工作构建,然后再迭代(重建)参数数据。

我将完成(最后!)两个短项目。 第一个是调用没有参数的新查询方法的语法:

 public DataTable GetAllEmployees() { return Query("SELECT * FROM Employees", p => {}); } 

虽然我们也可以将它作为原始Query()函数的重载提供,但在我自己的代码中我不想这样做,因为我想与其他开发人员沟通,他们应该寻求参数化他们的代码,而不是偷偷摸摸用字符串连接。

其次,这个答案中概述的代码仍未完成。 还有一些重要的弱点尚待解决。 例如,使用数据表而不是数据引导程序会强制您将每个查询的整个结果集一次性加载到内存中。 我们可以采取一些措施来避免这种情况。 我们还没有讨论插入,更新,删除或更改,我们还没有解决如何组合复杂参数情况,我们可能希望添加代码以过滤姓氏,但仅限于数据对于姓氏filter实际上是从用户可用的。 虽然这可以很容易地适应所有这些场景,但我认为在这一点上我已经完成了原始目标,因此我将把它留给读者。

总而言之,请记住您必须做的两件事:通过finally块关闭您的连接,并参数化您的查询。 希望这篇文章能帮助你做好准备。

请尝试使用ExecuteReader 。 它返回一个对象,然后可以像文件一样读取以获得结果:

 OleDbDataReader myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection); while(myReader.Read()) { Console.WriteLine(myReader.GetString(0)); } 

发布它时代码的巨大问题是无法正确地参数化查询。 在调用函数之前,你必须进行字符串连接,这使你对sql注入攻击开放。 您需要在代码中使用一种方法来允许查询参数与sql字符串分开。

您的示例中的一些其他问题包括未正确关闭连接(如果您的查询抛出exception,它将保持挂起)并调用错误的ADO方法。

我已经付出了很多努力来实现这一目标,我认为我有一些接近理想模式的东西,因为你想要在我对另一个问题的答案中得到很好的指导:
SQL Server最快的插入,更新和选择方法

基本上,当您调用ADO函数实际运行查询时,您将返回DbDataReader 。 我使用迭代器块将该数据读取器转换为IEnumerable ,它与linq和其他代码配合使用,并使用Action 来鼓励正确的查询参数化。 因此,您将连接代码抽象为如下方法:

 private static IEnumerable Retrieve(string sql, Action addParameters) { using (var cn = new SqlConnection(ConnectionString)) using (var cmd = new SqlCommand(sql, cn)) { addParameters(cmd.Parameters); cn.Open(); using (var rdr = cmd.ExecuteReader()) { while (rdr.Read()) yield return rdr; rdr.Close(); } } } 

并在代码中使用它来实现这样的实际查询:

 public IEnumerable GetSomeDataById(int MyId) { return Retrieve( "SELECT * FROM [MyTable] WHERE ID= @MyID", p => { p.Add("@MyID", SqlDbType.Int).Value = MyId; } ); } 

请注意,这可以让您正确地参数化查询,始终正确关闭和处理您的连接对象,设置您在3层或服务体系结构中的每个层之间进行流水线操作(使其快速),并使用最小的代码开销。