使用带有LINQ-to-SQL ExecuteQuery的IN子句

时间:2012-03-20 17:42:23

标签: sql-server linq linq-to-sql

LINQ to SQL在翻译我的一个查询时做了很糟糕的工作,所以我手工重写了它。问题是重写必然涉及IN子句,我不能为我的生活弄清楚如何为此目的将集合传递给ExecuteQuery。我在这里看到的唯一可以提出的是在整个查询字符串上使用string.Format来克服它 - 但这会阻止查询在查询缓存中结束

这样做的正确方法是什么?

注意:请注意我正在使用传递给 ExecuteQuery的原始SQL。我在第一句话中说过。告诉我使用Contains没有帮助,除非您知道将Contains与原始SQL混合的方法。

5 个答案:

答案 0 :(得分:7)

表值参数

在Cheezburger.com上,我们经常需要将AssetID或UserID列表传递给存储过程或数据库查询。

糟糕的方式:动态SQL

传递此列表的一种方法是使用动态SQL。

 IEnumerable<long> assetIDs = GetAssetIDs();
 var myQuery = "SELECT Name FROM Asset WHERE AssetID IN (" + assetIDs.Join(",") + ")";
 return Config.GetDatabase().ExecEnumerableSql(dr=>dr.GetString("Name"), myQuery);

这是一件非常糟糕的事情:

  1. 动态SQL通过简化SQL注入攻击为攻击者提供了一个弱点 由于我们通常只是将数字连接在一起,所以这是不太可能的,但是 如果你开始连接字符串,只需一个用户输入 ';DROP TABLE Asset;SELECT ' 我们的网站已经死了。
  2. 存储过程无法使用动态SQL,因此查询必须存储在代码中,而不是存储在数据库架构中。
  3. 每次运行此查询时,都必须重新计算查询计划。对于复杂的查询,这可能非常昂贵。
  4. 但是,它确实具有以下优点:在DB端不需要额外的解码,因为资产ID是由查询解析器找到的。

    好方法:表值参数

    SQL Server 2008增加了一项新功能:用户可以定义表值数据库类型。 大多数其他类型是标量(它们只返回一个值),但表值类型可以包含多个值,只要值是表格式。

    我们定义了三种类型:varchar_arrayint_arraybigint_array

    CREATE TYPE bigint_array AS TABLE (Id bigint NOT NULL PRIMARY KEY)
    

    存储过程和以编程方式定义的SQL查询都可以使用这些表值类型。

      IEnumerable<long> assetIDs = GetAssetIDs();
      return Config.GetDatabase().ExecEnumerableSql(dr=>dr.GetString("Name"),
          "SELECT Name FROM Asset WHERE AssetID IN (SELECT Id FROM @AssetIDs)", 
          new Parameter("@AssetIDs", assetIDs));
    

    <强>优点

    1. 可以毫不费力地在存储过程和程序化SQL中使用
    2. 不易受SQL注入攻击
    3. 可缓存,稳定的查询
    4. 不锁定架构表
    5. 不限于8k数据
    6. DB服务器和Mine应用程序完成的工作量减少,因为没有CSV字符串的连接或解码。
    7. &#34;典型用途&#34;统计信息可以通过查询分析器获得,这可以带来更好的性能。
    8. <强>缺点

      1. 仅适用于SQL Server 2008及更高版本。
      2. 传言TVP在执行查询之前已完全预先缓冲,这意味着服务器可能会拒绝非常大的TVP。 对此谣言的进一步调查正在进行中。
      3. 进一步阅读

        This article是了解有关TVP的更多信息的绝佳资源。

答案 1 :(得分:3)

如果你不能使用表值参数,这个选项比xml选项快一点,同时仍然允许你远离动态sql:将连接的值列表作为字符串参数传递,并解析分隔的字符串返回查询中的值。有关如何有效解析的说明,请参阅this article

答案 2 :(得分:2)

我怀疑你是在使用SQL Server 2005.表值参数直到2008年才添加,但您仍然可以使用XML data type在客户端和服务器之间传递集合。 / p>

答案 3 :(得分:0)

这适用于SQL Server 2005(及更高版本):

create procedure IGetAListOfValues
   @Ids xml -- This will recevie a List of values
as
begin
    -- You can load then in a temp table or use it as a subquery:
    create table #Ids (Id int);
    INSERT INTO #Ids
    SELECT DISTINCT params.p.value('.','int') 
    FROM @Ids.nodes('/params/p') as params(p);
    ...
end

您必须使用如下参数调用此过程:

exec IGetAListOfValues
@Ids = '<params> <p>1</p> <p>2</p> </params>' -- xml parameter

nodes函数使用xPath表达式。在这种情况下,它是/params/p,这就是XML使用<params>作为root,<p>作为元素的方式。

value函数将每个p元素内的文本强制转换为int,但您可以轻松地将其与其他数据类型一起使用。在此示例中,有一个DISTINCT以避免重复值,但是,当然,您可以根据您想要实现的目标将其删除。

我有一个辅助(扩展)方法,用于转换字符串中的IEnumerable<T>,该字符串类似于执行示例中显示的字符串。创建一个很容易,并在需要时让它为您完成工作。 (您必须测试T的数据类型并转换为可在SQL Server端解析的适当字符串)。这样你的C#代码更干净,你的SP遵循相同的模式来接收参数(你可以根据需要传入尽可能多的列表)。

一个优点是您无需在数据库中创建任何特殊内容即可使其正常工作。

当然,您不需要像我的示例中那样创建临时表,但您可以将查询直接用作IN谓词中的子查询

    WHERE MyTableId IN (SELECT DISTINCT params.p.value('.','int') 
    FROM @Ids.nodes('/params/p') as params(p) )

答案 4 :(得分:0)

我不是100%确定我正确理解了这个问题,但是LinqToSql的ExecuteQuery有一个参数重载,并且查询应该使用类似于string.Format的格式。

使用此重载对于SQL注入是安全的,并且在幕后LinqToSql将其转换为使用带参数的sp_executesql。

以下是一个例子:

string sql = "SELECT * FROM city WHERE city LIKE {0}";
db.ExecuteQuery(sql, "Lon%"); //Note that we don't need the single quotes 

这样即使使用动态sql,也可以使用参数化查询的好处。

然而,当使用带有动态数量参数的IN时,有两个选项:

  1. 动态构造字符串,然后将值作为数组传递,如:

    string sql = "SELECT * FROM city WHERE zip IN (";
    List<string> placeholders = new List<string>();
    for(int i = 0; i < zips.Length;i++)
    {
          placeholders.Add("{"+i.ToString()+"}");
    }
    sql += string.Join(",",placeholders.ToArray());
    sql += ")";
    db.ExecuteQuery(sql, zips.ToArray());
    
  2. 我们可以使用Linq扩展方法使用更紧凑的方法,如

    string sql = "SELECT * FROM city WHERE zip IN ("+
      string.Join("," , zips.Select(z => "{" + zips.IndexOf(f).ToString() + "}"))
    +")";
    db.ExecuteQuery(sql, zips.ToArray());