3

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

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


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

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

在一个小项目中用 simple 和 generic 实现了这样的东西后ExecuteNonQuery,我ExecuteScalar很高兴看到代码更短且更易于维护。ReadManyRowsDataAccess<T>.ReadManyRows

我发现只有两个缺点:

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

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

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

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

4

5 回答 5

2

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

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

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

使用匿名类型而不是字典

编写一个将匿名类型转换为字典的助手很简单,但我认为它极大地改进了符号,你不需要编写new Dictionary<string, object>.

使用元组.创建

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

围绕 DataReader 创建一个强类型包装器

这将消除所有地方的那些丑陋的转换——实际上,你真的需要DataReader在那个 lambda 中访问吗?

我将通过代码为您的示例说明这一点。
感谢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));
    }
}

它将在内部实例化ReadOne并调用,并为选择器 lambdasReadMany提供对底层的方便(和有限)访问。DataReader

于 2011-02-06T03:09:14.687 回答
1

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

于 2011-02-05T23:36:38.013 回答
1

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

Java 的 Spring 框架大量使用了这些类型的模板类和帮助程序,例如JdbcTemplateHibernateTemplate消除了开发人员编写样板代码的需要。这个想法是一次编写和测试它,然后多次重复使用它。

于 2011-02-05T23:39:03.527 回答
1

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

您的抽象看起来不错,但更让我烦恼的是,您提供的第一个示例使SqlConnection打开的时间比需要的时间长。

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

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

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

于 2011-02-06T01:42:02.430 回答
0

委托确实让刚接触这种方法的人很难立即阅读/理解,但最终应该很容易上手。

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

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

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

于 2011-02-06T02:41:50.280 回答