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

发布于 2024-10-16 05:03:31 字数 2767 浏览 4 评论 0原文

当我发现一个新想法时,我总是坚持下去,看不到它的任何弱点。当我开始在一个大型项目中使用这个新想法时,糟糕的事情就会发生,并且在几个月后发现这个想法非常糟糕,我不应该在任何项目中使用它。

这就是为什么,有了一个新想法并准备在一个新的大型项目中使用它,我需要您对此的意见,尤其是负面的意见


很长一段时间,我厌倦了在必须直接访问数据库的项目中一次又一次地输入或复制粘贴以下块:

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.ReadManyRows 实现此类操作后在一个小项目中,我很高兴看到代码更短并且更易于维护。

我只发现两个缺点:

  • 需求中的一些修改将需要大量的代码更改。例如,如果需要添加事务,使用普通的SqlCommand方法就很容易做到。如果改用我的方法,则需要重写整个项目以使用 SqlCommand 和事务。

  • 对命令级别的轻微修改将需要从我的方法转向标准 SqlCommand。例如,当仅查询一行时,必须扩展 DataAccess 类以包含这种情况,或者代码必须直接使用 SqlCommandExecuteReader(CommandBehavior.SingleRow) ) 相反。

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

这种方法的其他弱点是什么,特别是对于 DataAccess.ReadManyRows?

When I find a new idea, I always stick with it, and am unable to see any weak sides of it. Bad things happen when I start to use the new idea in a large project, and discover some moths later that the idea was extremely bad and I shouldn't use it in any project.

That's why, having a new idea and being ready to use it in a new large project, I need your opinion on it, especially negative one.


For a long time, I was bored to type again and again or copy-paste the following blocks in projects where database must be accessed directly:

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"]));
            }
        }
    }
}

So I've done a small class which allows to write something like that to do the same thing as above:

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"]);
        }));

The second approach is:

  • Shorter to write,

  • Easier to read (at least for me; some people may say that actually, it's much less readable),

  • Harder to make errors (for example in first case, I often forget to open the connection before using it, or I forget while block, etc.),

  • Faster with the help of Intellisense,

  • Much more condensed, especially for simple requests.

Example:

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

After implementing such thing with simple ExecuteNonQuery, ExecuteScalar and ReadManyRows and a generic DataAccess<T>.ReadManyRows in a small project, I was happy to see that the code is much shorter and easier to maintain.

I found only two drawbacks:

  • Some modifications in requirements will require heavy code changes. For example, if there is a need to add transactions, it will be very easy to do with ordinary SqlCommand approach. If my approach is used instead, it will require to rewrite the whole project to use SqlCommands and transactions.

  • Slight modifications on command level will require to move from my approach to standard SqlCommands. For example, when querying one row only, either DataAccess class must be extended to include this case, or the code must use directly SqlCommand with ExecuteReader(CommandBehavior.SingleRow) instead.

  • There might be a small performance loss (I don't have precise metrics yet).

What are the other weak points of this approach, especially for DataAccess<T>.ReadManyRows?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

三寸金莲 2024-10-23 05:03:31

你想要完成的事情很好,我实际上喜欢这种语法,而且我认为它非常灵活。不过我相信您需要更好地设计 API。

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

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

使用匿名类型而不是字典

编写一个将匿名类型转换为字典的帮助器很简单,但我认为它改进了符号大大,你不需要编写new Dictionary

使用 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 内实例化code> 调用并为选择器 lambda 提供对底层 DataReader 的便捷(且有限)访问。

What you're trying to accomplish is nice, I actually like this kind of syntax and I think it's pretty flexible. However I believe you need to design you APIs better.

The code is readable and nearly beautiful but it is hard to understand, primarily due to lots of generics that don't make much sense unless you know exactly what each type means. I'd use generic type inference wherever possible to eliminate some of them. To do so, consider using generic methods instead of generic types.

Some syntax suggestions (I don't have compiler now so they are basically ideas):

Use anonymous types instead of dictionaries

It's trivial to write a helper that converts anonymous type to a dictionary but I think it improves the notation greatly and you wouldn't need to write new Dictionary<string, object>.

Use Tuple.Create

This static method was created to avoid specifying types explicitly.

Create a strong-typed wrapper around DataReader

This would remove those ugly conversions all around the place—and actually, do you really need access to DataReader in that lambda?

I will illustrate this by code for your examples.
Kudos to David Harkness for chaining idea.

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")); 

I can also see it being extended for handling single row selection:

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

This is a rough draft for Row class:

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

It is to be instantiated inside ReadOne and ReadMany calls and provides convenient (and limited) access to underlying DataReader for selector lambdas.

怪我入戏太深 2024-10-23 05:03:31

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

My thoughts: you're embedding SQL in code, in a string (as opposed to using LINQ, which is at least syntax-checked, which helps provided you keep your DBML or EDMX mapping file synched with your database structure). Embedding SQL in non-syntax checked code in this way could easily lead to unmaintainable code, where you (or someone else) later changes the database structure or the embedded SQL string in a way that breaks the application. Embedding SQL in strings is particularly prone to producing hard-to-find bugs, because code with logical errors will still compile correctly; this makes it more likely that developers who are less familiar with the code base will obtain a false sense of security that changes they've made have not had any adverse or unintended effects.

时光与爱终年不遇 2024-10-23 05:03:31

你的抽象方法看起来不错。由于额外的方法调用而造成的任何性能损失都是微不足道的,而且开发人员的时间比 CPU 时间要昂贵得多。当您需要添加事务或单行选择时,您可以扩展您的库类。您在这里很好地利用了不要重复自己

Spring Framework for Java 大量使用这些类型的模板类和帮助器(例如 JdbcTemplate 和 HibernateTemplate),以消除开发人员编写样板代码的需要。这个想法是编写并测试一次并多次重用它。

Your abstraction approach looks sound. Any performance hit due to extra method calls will be trivial, and developer time is far more expensive than CPU time. When you need to add transactions or single-row selects, you can expand your library classes. You're making good use of Don't Repeat Yourself here.

The Spring Framework for Java makes heavy use of these types of template classes and helpers such as JdbcTemplate and HibernateTemplate to remove the need for developers to write boilerplate code. The idea is to write and test it well once and reuse it many times over.

若相惜即相离 2024-10-23 05:03:31

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

您的抽象看起来不错,但是更让我困扰的是您给出的第一个示例使 SqlConnection 保持打开的时间比需要的时间长。

使用 IEnumerable 非常棒,因为您可以将执行推迟到何时以及是否被消耗。
但是,只要您尚未到达枚举末尾,连接就会保持打开状态。

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

但我需要对此提出警告,验证在枚举时没有任何旧代码执行一些魔法。

First of all, never apologize for not copy-pasting code.

Your abstraction looks fine, however what troubles me a little more is the fact that the first example you gave keeps the SqlConnection open longer than it needs to be.

Using an IEnumerable<T> is great, since you defer execution to when and if it is consumed.
However, as long as you have not reached the end of your enumeration, the connection stays open.

The implementation of your method could consume the entire enumeration via a ToList(), and then return the list instead. You could even still support the deferred execution by implementing a small custom enumerator.

But I need to put a caveat around this, verify that there isn't any old code doing some magic while enumerating.

把昨日还给我 2024-10-23 05:03:31

对于刚接触这种方法的人来说,委托确实让立即阅读/理解有点困难,但最终应该很容易掌握。

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

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

yield 的使用对您可以尝试/捕获的位置施加了一些限制(为什么yield return不能出现在带有a的try块内catch?),但这也是之前方法中的一个问题,可能与您的场景无关。

The delegate does make it a bit harder to read/understand immediately for someone new to this approach but it should be easy enough to pick up eventually.

From a maintainability point of view it may be harder to understand the stack trace when errors eventually creep in. Primarily if an error occurs in this section:

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

The use of yield puts some limitations on where you can try/catch (Why can't yield return appear inside a try block with a catch?) but that is an issue in the previous approach as well and may not be relevant in your scenario.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文