【问题标题】:creating a database query METHOD创建数据库查询方法
【发布时间】:2011-04-29 11:24:15
【问题描述】:

我不确定我是否被迷惑了,但我想做的是创建一个返回查询结果的方法,以便我可以重用连接代码。据我了解,查询返回一个对象,但我如何将该对象传回?我想将查询作为字符串参数发送到方法中,并让它返回结果以便我可以使用它们。这是我在黑暗中刺伤的东西,它显然不起作用。这个例子是我试图用查询结果填充一个列表框;工作表名称是员工,字段/列是名称。我得到的错误是“复杂的 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();


        }

【问题讨论】:

  • 一发布,我就注意到 executenonquery 是错误的使用方法。但我还是不知道去哪里

标签: c# object methods return


【解决方案1】:

在学习与数据库对话时,每个程序员必须做两件基本的事情:关闭连接和参数化查询。这些项目与运行 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&lt;SqlParameterCollection&gt; 参数。不要介意&lt; &gt; 部分。如果你不熟悉泛型,你现在可以假装它是类名的一部分。重要的是,这种特殊的 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 块关闭您的连接,并参数化您的查询。希望这篇文章能让你走上成功之路。

【讨论】:

  • +1 这肯定会比大多数教程更好:)
  • +1 - 很好的解释!在这里,我将在您的书中添加第 2 章:“了解您在第 1 章中阅读的所有内容很好,但只需使用 NHibernate 代替。” :)
  • @Scott Whitlock - 啊,我看到你已经阅读了我对另一个问题的完整答案,然后:stackoverflow.com/questions/2862428/…
  • 谢谢。这不是我用的,但我肯定会用这个。谢谢你,这是我见过的最好的解释。
  • @thats_how_i_feel 您需要保持 Query() 私有,因为如果它以使您的应用程序容易受到攻击的方式公开,它将被滥用。我在生产代码中经常做的是让数据层包含一个像“GetAllEmployeeData()”这样的方法。在名称中添加“Data”是因为它返回原始 IDataRecords,而不是 Employee 对象,以保持层分离。
【解决方案2】:

嘿!尝试这个, 如果您只想将所有员工的姓名显示到列表框中,这应该可以。 我刚刚从您的代码中编辑了一些行...

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;
}

【讨论】:

  • 这是我尝试的第一件事!大声笑,但是当我这样做时,列表框会填充对对象的引用而不是实际内容。我忘记的是显示成员!谢谢:)
【解决方案3】:

改用ExecuteReader。它返回一个对象,然后可以像文件一样读取该对象以获取结果:

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

【讨论】:

  • 说“指定的演员表无效”
  • 是的,我知道,哈哈。但这就是它所说的!我认为它是从类型“oledbdatareader”到字符串的隐式转换......
【解决方案4】:

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

您的示例中的其他一些问题包括未正确关闭连接(如果您的查询引发异常,它将挂起)和调用错误的 ADO 方法。

我已经做了大量工作来做到这一点,我认为我有一些接近理想模式的东西,我在这里对另一个问题的回答中很好地确定了你想要的东西:
Fastest method for SQL Server inserts, updates, selects

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

private static IEnumerable<IDataRecord> Retrieve(string sql, Action<SqlParameterCollection> 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<IDataRecord> GetSomeDataById(int MyId)
{
    return Retrieve(
        "SELECT * FROM [MyTable] WHERE ID= @MyID", 
       p =>
       {
          p.Add("@MyID", SqlDbType.Int).Value = MyId;
       }
     );
}

请注意,这可以让您正确参数化查询,始终正确关闭和处置您的连接对象,让您在 3 层或服务架构中的每一层之间进行流水线处理(使其速度更快),并且这样做的代码开销最小。

【讨论】:

  • 我知道缺少结束语。我有点看到你的示例帖子来自哪里,但对于我的技能水平来说有点令人费解,哈哈。我重写了我的方法并使用数据表作为中间体。它可以工作,使用与我发布的传递 sql 查询相同的方式,但为了测试它,我让它从方法内部填充框。唯一的问题是,为了重用这段代码,我需要能够将该数据表返回到调用它的地方。有什么想法吗?
  • @Sina 不仅仅是你不调用 .Close()。即使您确实调用了.Close(),当您遇到不可避免的引发异常的查询时,您仍然可以保持连接处于打开状态。在您花时间了解 sql 注入以及如何执行查询参数之前,您不应该编写 任何 数据库代码。 不能通过字符串拼接构建sql查询!
  • 我不拒绝学习新东西的想法,我只是不知道你是如何接近它来把它分开的。我也很困惑你说我通过连接构建查询的地方,因为我不是 O_o。我使用的唯一连接是构建连接字符串,以便我可以通过打开文件对话框指定文件名/路径。目前,查询本身就是我所需要的。我终于让这个工作了。我只是将返回类型设为DataTable,将查询结果转储到数据适配器中,填充表格,然后返回表格。
  • DataTable employees = Query("Select [name] FROM [Employees$]"); for (int i = 0; i
  • 我希望我可以像在原始帖子中那样在 cmets 中发布代码。我告诉你我做了什么,这样我们就可以看看里面是否有任何危险
猜你喜欢
  • 2015-03-22
  • 1970-01-01
  • 1970-01-01
  • 2020-02-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-28
相关资源
最近更新 更多