使用为SqlDataReader中的每一行调用委托的方法有什么缺点?

时间:2011-02-05 23:12:33

标签: c# generics readability sqlconnection maintainability

当我找到一个新想法时,我总是坚持下去,而且我看不到它的任何弱点。当我开始在大型项目中使用新想法时发生了不好的事情,后来发现了一些飞蛾,这个想法非常糟糕,我不应该在任何项目中使用它。

这就是为什么,有一个新想法并准备在一个新的大型项目中使用它,我需要你的意见,特别是否定的


很长一段时间,我无聊地一次又一次地打字或者在必须直接访问数据库的项目中复制粘贴以下块:

string connectionString = Settings.RetrieveConnectionString(Database.MainSqlDatabase);
using (SqlConnection sqlConnection = new SqlConnection(connectionString))
{
    sqlConnection.Open();

    using (SqlCommand getProductQuantities = new SqlCommand("select ProductId, AvailableQuantity from Shop.Product where ShopId = @shopId", sqlConnection))
    {
        getProductQuantities.Parameters.AddWithValue("@shopId", this.Shop.Id);
        using (SqlDataReader dataReader = getProductQuantities.ExecuteReader())
        {
            while (dataReader.Read())
            {
                yield return new Tuple<int, int>((int)dataReader["ProductId"], Convert.ToInt32(dataReader["AvailableQuantity"]));
            }
        }
    }
}

所以我做了一个小班,允许写这样的东西做同样的事情:

IEnumerable<Tuple<int, int>> quantities = DataAccess<Tuple<int, int>>.ReadManyRows(
    "select ProductId, AvailableQuantity from Shop.Product where ShopId = @shopId",
    new Dictionary<string, object> { { "@shopId", this.Shop.Id } },
    new DataAccess<string>.Yield(
        dataReader =>
        {
            return new Tuple<int, int>(
                (int)dataReader["ProductId"],
                Convert.ToInt32(dataReader["AvailableQuantity"]);
        }));

第二种方法是:

  • 写作更短,

  • 更容易阅读(至少对我而言;有些人可能会说实际上,它的可读性更低),

  • 更难做出错误(例如,在第一种情况下,我经常忘记在使用之前打开连接,或者我忘记了while阻止等),

  • 在Intellisense的帮助下更快,

  • 更加浓缩,特别是对于简单的请求。

示例:

IEnumerable<string> productNames = DataAccess<string>.ReadManyRows(
    "select distinct ProductName from Shop.Product",
    new DataAccess<string>.Yield(dataReader => { return (string)dataReader["ProductName"]; }));

在一个小项目中使用简单ExecuteNonQueryExecuteScalarReadManyRows以及通用DataAccess<T>.ReadManyRows实现此类操作后,我很高兴看到代码更短并且更容易维护。

我发现只有两个缺点:

  • 需求的某些修改需要进行大量的代码更改。例如,如果需要添加事务,那么使用普通SqlCommand方法将非常容易。如果使用我的方法,则需要重写整个项目以使用SqlCommand和事务。

  • 在命令级别进行轻微修改需要从我的方法转移到标准SqlCommand。例如,在仅查询一行时,必须扩展DataAccess类以包含此情况,或者代码必须直接使用SqlCommand而不是ExecuteReader(CommandBehavior.SingleRow)

  • 可能会有轻微的性能损失(我还没有精确的指标)。

这种方法的其他弱点是什么,尤其是DataAccess<T>.ReadManyRows

5 个答案:

答案 0 :(得分:2)

你想要完成的是很好的,我实际上喜欢这种语法,我认为它非常灵活。但是我相信你需要更好地设计API。

代码是可读的,几乎很漂亮,但很难理解,主要是因为除非你确切知道每种类型的含义,否则大量的泛型没有多大意义。我会尽可能使用泛型类型推断来消除其中的一些。为此,请考虑使用泛型方法而不是泛型类型。

一些语法建议(我现在没有编译器所以它们基本上是想法):

使用匿名类型而不是词典

编写一个将匿名类型转换为字典的帮助器是微不足道的,但我认为它大大改进了符号,你不需要编写new Dictionary<string, object>

使用Tuple.Create

创建此静态方法是为了避免显式指定类型。

围绕DataReader

创建强类型包装器

这会删除那些地方的丑陋转换 - 实际上,你真的需要访问该lambda中的DataReader吗?

我将通过您的示例代码来说明这一点 感谢David Harkness链接主意。

var tuples = new DataAccess ("select ProductId, AvailableQuantity from Shop.Product where ShopId = @shopId")
    .With (new { shopId = this.Shop.Id }) // map parameters to values
    .ReadMany (row =>
         Tuple.Create (row.Value<int> ("ProductId"), row.Value<int> ("AvailableQuantity"))); 

var strings = new DataAccess ("select distinct ProductName from Shop.Product")
    .ReadMany (row => row.Value<string> ("ProductName")); 

我还可以看到它被扩展用于处理单行选择:

var productName = new DataAccess ("select ProductName from Shop.Product where ProductId = @productId")
    .With (new { productId = this.SelectedProductId }) // whatever
    .ReadOne (row => row.Value<string> ("ProductName")); 

这是Row班的粗略草稿:

class Row {
    DataReader reader;

    public Row (DataReader reader)
    {
        this.reader = reader;
    }

    public T Value<T> (string column)
    {
        return (T) Convert.ChangeType (reader [column], typeof (T));
    }
}

它将在ReadOneReadMany调用中实例化,并为选择器lambdas提供对基础DataReader的方便(和有限)访问。

答案 1 :(得分:1)

我的想法:你在代码中嵌入SQL,在字符串中(与使用LINQ相反,LINQ至少是语法检查,这有助于保持DBML或EDMX映射文件与数据库结构同步)。以这种方式在非语法检查代码中嵌入SQL很容易导致无法维护的代码,您(或其他人)稍后会以破坏应用程序的方式更改数据库结构或嵌入式SQL字符串。在字符串中嵌入SQL特别容易产生难以发现的错误,因为具有逻辑错误的代码仍然可以正确编译;这使得不熟悉代码库的开发人员更有可能获得虚假的安全感,他们所做的更改没有任何不利或意外的影响。

答案 2 :(得分:1)

您的抽象方法看起来很合理。由于额外的方法调用而导致的性能损失将是微不足道的,并且开发人员的时间远远超过CPU时间。当您需要添加事务或单行选择时,可以扩展库类。你在这里充分利用了Don't Repeat Yourself

Spring Framework for Java大量使用这些类型的模板类和帮助程序,如JdbcTemplateHibernateTemplate,以消除开发人员编写样板代码的需要。我们的想法是编写并测试一次并重复使用它多次。

答案 3 :(得分:1)

首先,不要为没有复制粘贴代码道歉。

你的抽象看起来很好,但是让我更麻烦的是你给出的第一个例子让SqlConnection打开时间超过了它需要的时间。

使用IEnumerable<T>非常棒,因为您将执行推迟到何时以及是否被使用。 但是,只要您没有到达枚举的末尾,连接就会保持打开状态。

您的方法的实现可以通过ToList()使用整个枚举,然后返回列表。您甚至可以通过实现一个小的自定义枚举器来支持延迟执行。

但是我需要对此进行一个警告,确认在枚举时没有任何旧代码做一些魔术。

答案 4 :(得分:0)

对于这种方法的新人来说,代表确实会立即阅读/理解它有点困难,但它应该很容易最终拿起来。

从可维护性的角度来看,当错误最终蔓延时,可能更难理解堆栈跟踪。主要是如果本节中发生错误:

new DataAccess<string>.Yield(
    dataReader =>
    {
        return new Tuple<int, int>(
            (int)dataReader["ProductId"],
            Convert.ToInt32(dataReader["AvailableQuantity"]);
    }));

使用yield会对您可以尝试/捕获的位置(Why can't yield return appear inside a try block with a catch?)施加一些限制,但这也是前一种方法中的一个问题,可能与您的方案无关。