SqlConnection的最佳重用实践

26

我之前有Java的经验,现在想学C#。我看过了SqlConnection SqlCommand SqlDataReader IDisposable这篇文章,明白了连接数据库的最佳实践是将SqlConnectionSqlCommandSqlDataReader包装在一个using块中。

但是在Java中,我们习惯把连接封装在一个工厂方法里,只创建一次,并在所有查询中重复使用,甚至在多线程情况下也是如此。只为每个查询创建并尽快关闭语句和结果集。

难道为每个查询创建新的SqlConnection不算有点浪费吗?它不能被重用吗?


你可以使用池化机制。 - Daniel A. White
2
http://msdn.microsoft.com/en-us/library/8xx3tyca%28v=vs.110%29.aspx - Daniel A. White
4
.NET框架为您提供了对象池机制(这就是为什么建议始终使用using块),以便您可以更好地管理资源。 - Rowland Shaw
1
@JamesLee我总是主张每个SqlConnection都在using块中 - 虽然在“快乐路径”上你看不到任何不同,但如果出现问题,你可能会最终导致线程池耗尽,而调试这个问题将会很“有趣”。 - Rowland Shaw
1
感谢@RowlandShaw - 感谢您抽出时间回答一个旧帖子上的问题! - James Lee
显示剩余3条评论
5个回答

50

创建一个新的SqlConnection类的实例不会创建到SQL Server的新网络连接,而是租用一个现有的连接(或创建一个新的连接)。.NET为您处理物理连接池。

当您完成连接(通过该连接可以发送多个查询)后,请使用Close()Dispose()(最好使用using{}块)将其关闭。

没有必要也不是良好的做法来缓存SqlConnection类的实例。


10
针对 MS SQL 是真实的。在使用 MySQL 进行测试时,重复利用连接可以获得相当大的性能优势。但是,需要自己创建“连接池”以使操作线程安全。从不同线程访问连接对象会导致异常。因此,对于 MySQL,我会针对相同的“ManagedThreadId”重复使用连接。 - Harry
8
很遗憾,MySql库的编写者没有遵循现有的SqlClient方法,但很高兴你指出了这一点。 - PhillipH
1
很棒的答案。官方文档链接:https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-connection-pooling#pool-creation-and-assignment - Sudhanshu Mishra

7
如VMAtm所说,.net会自动汇集连接,因此重新创建它们是完全可以的。因此,我通常会为整个过程编写一个包装器,像这样的一个包装器。
        public static void RunWithOpenSqlConnection(string connectionString, Action<SqlConnection> connectionCallBack)
    {
        SqlConnection conn = null;
        try
        {
            conn = new SqlConnection(connectionString);
            connectionCallBack(conn);
        }
        catch (Exception ex)
        {
            //Log Error Here
        }
        finally
        {
            if (conn != null)
                conn.Dispose(); //will close the connection
        }
    }

    public static void ExecuteSqlDataReader(string connectionString, string sqlCommand, Action<SqlDataReader> readerCallBack)
    {
        RunWithOpenSqlConnection(connectionString, delegate(SqlConnection conn)
        {
            SqlCommand cmd = null;
            SqlDataReader reader = null;
            try
            {
                cmd = new SqlCommand(sqlCommand, conn);
                reader = cmd.ExecuteReader();
                readerCallBack(reader);
            }
            catch (Exception ex)
            {
                //Log Error Here
            }
            finally
            {
                if (reader != null)
                    reader.Dispose();
                if (cmd != null)
                    cmd.Dispose();
            }
        });
    }

//Example calling these
            ExecuteSqlDataReader(ConfigurationManager.ConnectionStrings["myConnectionString"].ConnectionString, "Select EmployeeID FROM Employees;", delegate(SqlDataReader reader)
        {
            List<string> employeeIds = new List<string>();
            if (reader.HasRows)
            {
                while(reader.Read())
                {
                    employeeIds.Add((string)reader[0]);
                }
            }
        });

当然,你可以使用像Peta Poco这样的微型ORM,避免使用Entity Framework带来的所有臃肿,让整个世界变得轻松100倍。 - Ryan Mann
非常好的封装,想看看它是否可以用作单例模式,已点赞! - Transformer
4
自从我写下这段话以来,我们已经对其进行了大量修改。我会尽力记得发布一个更好的例子。基本上,我们已经变成了类似于“GetListFromProc<MyThing>(storedProcName, getItemFromReader)”这样的东西。它返回一个MyThing列表,“getItemFromReader”是一个函数,它将SqlDataReader行传递给你实例化MyThing并返回它。 - Ryan Mann

6

MS SQL Server在其自己的连接池中管理连接,它们实际上并没有被销毁。但是它们被关闭了,这样可以最小化网络流量并释放可用连接到您的服务器。

此外,您应该注意,如果您正在使用Linq-To-SQL,则数据上下文将不会在被处理之前释放连接,因此我建议您只使用已经工作的代码,不要尝试自己进行优化。


6
我认为是 .Net 客户端而不是服务器在管理这些连接并根据需要进行池化。 - Rowland Shaw
7
“而它们实际上并没有被释放,但是它们已经被关闭了” - 正确来说应该是相反的;如果你正确地使用了 using 等方法,它们就已经被释放了,并且很可能被垃圾回收(非确定性等),但是底层的非托管连接仍然保持 打开 状态。 - Marc Gravell
谢谢。我不使用Linq,它很垃圾。我使用纯净和优化的SQL。 - Hikari

3
回答你的具体问题,你可以为每个查询重用一个 SqlConnection。只需确保在运行另一个查询之前关闭当前查询 (SqlDataReader 等),即将它们包装在自己的 using 块中即可。

单元测试中的问题在于构建机器没有权限或访问数据库。您如何处理这种类型的测试? - user3846642

-3
可以创建一个全局 SqlConnection 实例。在我的情况下,我将 SqlConnection 作为 DataContext 的成员,并通过 Singleton 进行访问。
public class DatabaseDataContext : DataContext
{
    private static DatabaseDataContext instance;
    private SqlConnection sqlConnection;        
    private SqlTransaction sqlTransaction;

    //...

    public static DatabaseDataContext Instance
    {
        get
        {
            return instance ?? (instance = new DatabaseDataContext(connectionString));
        }
        set
        {
            instance = value;
        }
    }
}

您可以通过关闭和打开此连接来封装您的事务,例如:

DatabaseDataContext.Instance.sqlConnection.Open();

// your transactions...

sqlConnection.Close();

或者你可以保持连接处于打开状态,但是需要明确地开始和结束事务:

DatabaseDataContext.Instance.sqlConnection.Open();

sqlTransaction = sqlConnection.BeginTransaction("Example Insert users");

try{
    // ...your first transaction

    sqlTransaction.Commit();
}
catch{sqlTransaction.Rollback();}

sqlTransaction = sqlConnection.BeginTransaction("Update baked breads");

try{
    // ...your second transaction

    sqlTransaction.Commit();
}
catch{sqlTransaction.Rollback();}

// Close the connection at some point
sqlConnection.Close();

6
我认为这种方法存在缺陷。首先,在代码中缓存 SqlConnection 没有任何优势——框架已经替你做了,其次它会阻止你创建的 DbContext 进行多线程使用。 - PhillipH
PhillipH说得一点也没错。此外,重用现有的SqlConnection至少需要线程限制(加上所有用户之间的过度协调,以便它们不会互相干扰事务或遇到重用限制(MultipleActiveResultSets),即使一切都是单线程的)。使用单例与所有这些相反;在某种程度上,它几乎是所需的完全相反。 - DarthGizka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接