如何重构在字典中存储大量SqlCommand的大型类

时间:2019-05-17 14:42:55

标签: c# architecture refactoring data-access-layer legacy-code

我最近开始重构由OOP经验不足的人设计的旧系统。幸运的是,(几乎)对数据库的所有访问都在一个3000行长的文件内。该文件包含一个Dictionary<string, SqlCommand>SqlConnection,这是一个很长的函数,它将每个SQL查询添加到字典中,如下所示:

cmd = new SqlCommand(null, _sqlConnection);
cmd.CommanText = "SELECT * FROM User WHERE User.UserID = @id;" // Most queries are far from being this simple
cmd.Parameters.Add(new SqlParameter("@id", SqlDbType.Int, 0));
cmd.Prepare();

_cmds.Add("getUser", cmd);

这些查询由同一文件中的函数使用,如下所示:

public void deleteUser(int userId) 
{
    if (_cmds.TryGetValue("deleteUser", out SqlCommand cmd)) 
    {
        lock(cmd) 
        { 
            cmd.Parameters[0].Value = userId;
            cmd.ExecuteNonQuery();
        }
    }
}

public int isConnected(int userId, out int amount) 
{
    bool result = false;
    amount = 0;

    if (_cmds.TryGetValue("userInfo", out SqlCommand cmd)) 
    {
        lock (cmd) 
        {
            cmd.Parameters[0].Value = userId;

            using (SqlDataReader reader = new cmd.ExecuteReader()) 
            {
                 if (reader.HasRows)
                     while (reader.Read()) 
                     {
                         amount = (int)Math.Round(reader.GetDecimal(0));
                         result = reader.GetInt32(1);
                     }
            }
        }
    }


  return result;
}

现在,使用和维护它太可怕了。我终于有时间重构它。我想将其转换为带有存储库的适当DAL,该存储库将由服务使用并且可以注入依赖项。

我不太在乎更改功能或查询(例如使用ORM)。我更感兴趣的是将文件拆分为许多文件,使我可以更轻松地模拟,测试和修改它。我正在寻找一种更好地构造现有代码的方法,尽管我知道将需要大量复制/粘贴和重新编码。

3 个答案:

答案 0 :(得分:1)

建议使用诸如 NHibernate 之类的对象关系映射器替换手动编写的对象映射代码,这将节省创建和维护数据访问层的时间和精力。

答案 1 :(得分:1)

签出Dapper。它是一个“微型ORM”,可提供高性能的面向对象的数据访问。您可以继续使用所有现有查询,但是用Dapper替换所有样板ADO.NET代码。

答案 2 :(得分:1)

这将需要一些重复的工作,但是这里有一些如何处理它的想法。这不会使代码处于理想状态,但是可能会使代码更易于管理。一个挑战是,每种方法在两个地方都有部分-一个在方法中,一个在命令存储在字典中。

  • 永远不要向此类添加更多SQL。开始定义和使用所需的新存储库。
  • 能够模拟它也很容易。您可以使用extract interface重构来创建接口,以便您可以模拟此类,即使以其当前形式也是如此。这将是一个大而丑陋的界面,但至少可以根据需要模拟方法。

那是容易的部分。如何重构整个类而不破坏它的任何一部分?这些步骤只是一些想法:

第一步只是注入类所需的连接字符串:

public class YourDataAccessClass
{
    private readonly string _connectionString;

    public YourDataAccessClass(string connectionString)
    {
        _connectionString = connectionString;
    }
}

您一次只能使用一种方法。最初,您可以按原样离开大部分班级,包括字典。这样,您尚未修改的方法将继续起作用。

接下来,您可以在两个单独的窗口中打开该类,以便可以看到包含SQL的字典函数和并排使用它的函数。如果必须向上和向下滚动,这将变得更加困难。

您可能希望将每个函数的SQL移入该函数。您可以在重构每个函数时执行此操作,但是一次完成所有操作可能会比较省心,从而可以从重复中获得效率。

您可以在每个函数中定义一个新变量,然后复制并粘贴:

var sql = "SELECT * FROM User WHERE User.UserID = @id;";

(再次,不是我通常写的方式。)

现在您拥有一个或多个看起来像这样的函数:

public void deleteUser(int userId) 
{
    var sql = "DELETE User WHERE User.UserID = @id;";
    if (_cmds.TryGetValue("deleteUser", out SqlCommand cmd)) 
    {
        lock(cmd) 
        { 
            cmd.Parameters[0].Value = userId;
            cmd.ExecuteNonQuery();
        }
    }
}

对于非查询命令,您可以在您的类中编写这样的函数,该函数将消除用于打开连接,创建命令等的重复代码:

private void ExecuteNonQuery(string sql, Action<SqlCommand> addParameters = null)
{
    using (var connection = new SqlConnection(_connectionString))
    using (var command = new SqlCommand(sql))
    {
        addParameters?.Invoke(command);
        connection.Open();
        command.ExecuteNonQuery();
    }
}

保存以下代码段。您甚至可能大部分时间都只能将其保存在剪贴板中。将其粘贴到SQL下方的每个非查询方法中:

ExecuteNonQuery(sql, command =>
{

});

粘贴后,将添加参数的一行移动到cmd参数的主体(命名为cmd,以便可以在不更改变量名称的情况下移动这些行),然后然后删除以前执行查询的现有代码。

ExecuteNonQuery(sql, cmd =>
{
    cmd.Parameters[0].Value = userId;
});

现在您的函数如下:

public void deleteUser(int userId) 
{
    var sql = "DELETE User WHERE User.UserID = @id;";
    ExecuteNonQuery(sql, cmd =>
    {
        cmd.Parameters[0].Value = userId;
    });
}

我并不是说这很有趣,但是它将使编辑这些功能的过程更加高效,因为您键入的内容更少,并且只是一遍又一遍地反复移动内容。

实际返回数据的方法虽然不那么有趣,但是仍然易于管理。

首先,采用几乎相同的样板代码。可能会有所改善,因为它仍然有些重复,但至少它是独立的:

using (var connection = new SqlConnection(_connectionString))
using (var cmd = new SqlCommand(sql)) // again, named "cmd" on purpose
{

    connection.Open();        

}

从此开始:

public int isConnected(int userId, out int name) 
{
    var sql = "SELECT * FROM User WHERE User.UserID = @id;";'
    bool result = false;
    amount = 0;

    if (_cmds.TryGetValue("userInfo", out SqlCommand cmd)) 
    {
        lock (cmd) 
        {
            cmd.Parameters[0].Value = userId;

            using (SqlDataReader reader = new cmd.ExecuteReader()) 
            {
                 if (reader.HasRows)
                     while (reader.Read()) 
                     {
                         amount = (int)Math.Round(reader.GetDecimal(0));
                         result = reader.GetInt32(1);
                     }
            }
        }
    }
}

将样板粘贴到方法中

public int isConnected(int userId, out int name) 
{
    var sql = "SELECT * FROM User WHERE User.UserID = @id;";'
    bool result = false;
    amount = 0;

    using (var connection = new SqlConnection(_connectionString))
    using (var cmd = new SqlCommand(sql)) // again, named "cmd" on purpose
    {

        connection.Open();        

    }

    if (_cmds.TryGetValue("userInfo", out SqlCommand cmd)) 
    {
        lock (cmd) 
        {
            cmd.Parameters[0].Value = userId;

            using (SqlDataReader reader = new cmd.ExecuteReader()) 
            {
                 if (reader.HasRows)
                     while (reader.Read()) 
                     {
                         amount = (int)Math.Round(reader.GetDecimal(0));
                         result = reader.GetInt32(1);
                         // was this a typo? The code in the question doesn't
                         // return anything or set the "out" variable. But
                         // if that's in the method then that will be part of
                         // what gets copied.
                     }
            }
        }
    }
}

然后,像以前一样,将要在其中添加参数的部分移到connection.Open();上方,并将要使用命令的部分移到connection.Open();下方,然后删除剩下的部分。结果是这样的:

public int isConnected(int userId, out int name) 
{
    var sql = "SELECT * FROM User WHERE User.UserID = @id;";'
    bool result = false;
    amount = 0;

    using (var connection = new SqlConnection(_connectionString))
    using (var cmd = new SqlCommand(sql)) // again, named "cmd" on purpose
    {
        cmd.Parameters[0].Value = userId;
        connection.Open();        
        using (SqlDataReader reader = new cmd.ExecuteReader()) 
        {
             if (reader.HasRows)
                 while (reader.Read()) 
                 {
                     amount = (int)Math.Round(reader.GetDecimal(0));
                     result = reader.GetInt32(1);
                 }
        }
    }
}

您可能会陷入困境,每隔一两分钟就能做一次,这意味着只需要几个小时。

完成所有这些操作后,您可以删除大量的词典功能。现在,该类依赖于注入的连接字符串,并且可以正常打开和关闭连接,而不是反复存储和使用连接。

您也可以将其分解。一种方法是将连接字符串和helper函数移到基类中(或仅复制helper函数-它确实很小),并且您可以将任何查询函数移到较小的类中,因为每个函数都是独立的。 / p>