将 List<> 传递给 SQL 存储过程

Posted

技术标签:

【中文标题】将 List<> 传递给 SQL 存储过程【英文标题】:Passing List<> to SQL Stored Procedure 【发布时间】:2010-09-17 14:30:06 【问题描述】:

我经常不得不将多个项目加载到数据库中的特定记录。例如:网页显示单个报表要包含的项目,所有这些项目都是数据库中的记录(Report 是 Report 表中的记录,Items 是 Item 表中的记录)。用户正在通过 Web 应用程序选择要包含在单个报告中的项目,假设他们选择了 3 个项目并提交。该过程将通过将记录添加到名为 ReportItems (ReportId,ItemId) 的表中来将这 3 个项目添加到此报告中。

目前,我会在代码中做这样的事情:

public void AddItemsToReport(string connStr, int Id, List<int> itemList)

    Database db = DatabaseFactory.CreateDatabase(connStr);

    string sqlCommand = "AddItemsToReport"
    DbCommand dbCommand = db.GetStoredProcCommand(sqlCommand);

    string items = "";
    foreach (int i in itemList)
        items += string.Format("0~", i);

    if (items.Length > 0)
        items = items.Substring(0, items.Length - 1);

    // Add parameters
    db.AddInParameter(dbCommand, "ReportId", DbType.Int32, Id);
    db.AddInParameter(dbCommand, "Items", DbType.String, perms);
    db.ExecuteNonQuery(dbCommand);

这在存储过程中:

INSERT INTO ReportItem (ReportId,ItemId)
SELECT  @ReportId,
          Id
FROM     fn_GetIntTableFromList(@Items,'~')

函数返回一列整数表。

我的问题是:有没有更好的方法来处理这样的事情?请注意,我不是在问数据库规范化或类似的问题,我的问题与代码特别相关。

【问题讨论】:

【参考方案1】:

如果您可以选择使用 SQL Server 2008,那么有一个名为“表值参数”的新功能可以解决这个确切的问题。

查看有关 TVP here 和 here 的更多详细信息,或者只是向 Google 询问“SQL Server 2008 表值参数” - 您会找到大量信息和示例。

强烈推荐 - 如果您可以迁移到 SQL Server 2008...

【讨论】:

【参考方案2】:

您的字符串连接逻辑可能可以简化:

string items = 
    string.Join("~", itemList.Select(item=>item.ToString()).ToArray());

这将为您节省一些字符串连接,这在 .Net 中很昂贵。

我认为您保存项目的方式没有任何问题。您正在限制对数据库的访问,这是一件好事。如果您的数据结构比整数列表更复杂,我建议使用 XML。

注意:我在 cmets 中被问到这是否会为我们节省任何字符串连接(确实如此)。我认为这是一个很好的问题,并愿意跟进。

如果您使用Reflector 剥离打开的string.Join,您将看到Microsoft 使用了一些不安全的(在.Net 的意义上)技术,包括使用char 指针和称为UnSafeCharBuffer 的结构。当你真正归结为它时,他们正在做的是使用指针遍历一个空字符串并建立连接。请记住,.Net 中字符串连接如此昂贵的主要原因是每次连接都会在堆上放置一个新的字符串对象,因为字符串是不可变的。这些内存操作很昂贵。 String.Join(..) 本质上是分配一次内存,然后用指针对其进行操作。非常快。

【讨论】:

这真的能让你免于串联吗?你知道这个方法是不是用StringBuilder实现的吗? 是的,它将使您免于串联。 String.Join 使用 UnsafeCharBuffer 进行连接(在 Reflector 中查看)。就 LINQ 而言,这只是在列表中枚举调用 ToString(),这是不可避免的,并创建一个数组。数组创建可能很昂贵。 我不知道你可以那样做,谢谢你的提示! 请注意,在 .NET 4 中,您可以删除 .ToArray()【参考方案3】:

您的技术的一个潜在问题是它不能处理非常大的列表 - 您可能会超过数据库的最大字符串长度。我使用了一个辅助方法,它将整数值连接成一个字符串枚举,每个字符串都小于指定的最大值(以下实现还可以选择检查并删除重复的 id):

public static IEnumerable<string> ConcatenateValues(IEnumerable<int> values, string separator, int maxLength, bool skipDuplicates)

    IDictionary<int, string> valueDictionary = null;
    StringBuilder sb = new StringBuilder();
    if (skipDuplicates)
    
        valueDictionary = new Dictionary<int, string>();
    
    foreach (int value in values)
    
        if (skipDuplicates)
        
            if (valueDictionary.ContainsKey(value)) continue;
            valueDictionary.Add(value, "");
        
        string s = value.ToString(CultureInfo.InvariantCulture);
        if ((sb.Length + separator.Length + s.Length) > maxLength)
        
            // Max length reached, yield the result and start again
            if (sb.Length > 0) yield return sb.ToString();
            sb.Length = 0;
        
        if (sb.Length > 0) sb.Append(separator);
        sb.Append(s);
    
    // Yield whatever's left over
    if (sb.Length > 0) yield return sb.ToString();

然后你使用它:

using(SqlCommand command = ...)

    command.Connection = ...;
    command.Transaction = ...; // if in a transaction
    SqlParameter parameter = command.Parameters.Add("@Items", ...);
    foreach(string itemList in ConcatenateValues(values, "~", 8000, false))
    
        parameter.Value = itemList;
        command.ExecuteNonQuery();
    

【讨论】:

好点,我没想过超过字符串长度。虽然我怀疑我会遇到甚至接近的案例,但学习这一点很好,谢谢!【参考方案4】:

你要么做你已经得到的,传入一个分隔的字符串,然后解析为一个表值,或者另一个选择是传入一个 XML 和有点相同:

http://weblogs.asp.net/jgalloway/archive/2007/02/16/passing-lists-to-sql-server-2005-with-xml-parameters.aspx

我还没有机会查看 SQL 2008,看看他们是否添加了任何新功能来处理这类事情。

【讨论】:

【参考方案5】:

为什么不使用表值参数? https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/table-valued-parameters

【讨论】:

因为它需要用户定义的表类型,除非您保证在每次调用后删除此类型,否则它可能会永久保留在数据库中。 它们有点难以管理,例如,如果您必须对实时部署进行脚本更改。【参考方案6】:

请参阅http://www.sommarskog.se/arrays-in-sql-2005.html,详细讨论此问题以及您可以使用的不同方法。

【讨论】:

【参考方案7】:

下面是来自 sqlteam.com 的对表值参数的非常清晰的解释:Table Valued Parameters

【讨论】:

【参考方案8】:

在存储过程中查询单个字段的多个值 http://www.norimek.com/blog/post/2008/04/Query-a-Single-Field-for-Multiple-Values-in-a-Stored-Procedure.aspx

【讨论】:

以上是关于将 List<> 传递给 SQL 存储过程的主要内容,如果未能解决你的问题,请参考以下文章

将列表传递给 HQL 或 SQL 中的 IN 子句?

将 Dictionary<string,int> 传递给存储过程 T-SQL

SQL Server (2008) 将 ArrayList 或字符串传递给 SP 用于 IN()

为啥我不能将 List<List<Foo>> 传递给 IEnumerable<IEnumerable<Foo>>

将 java List<Double> 传递给 dart List<double> 时出错

xUnit - 将 List<> 传递给我的测试会引发错误 [重复]