针对实体列表的LINQ to SQL查询

时间:2009-04-08 13:05:19

标签: c# linq-to-sql

成分类:

class Ingredient
{
    public String Name { get; set; }
    public Double Amount { get; set; }
}

成分列表:

var ingredientsList = new List<Ingredient>();

我的“成分”表的数据库布局:

[Ingredients] (
    [IngredientsID] [int] IDENTITY(1,1) NOT NULL,
    [RecipeID] [int] NOT NULL,
    [IngredientsName] [nvarchar](512) NOT NULL,
    [IngredientsAmount] [float] NOT NULL
)



我可以在我的“Ingredients”表中查询我的ingredientsList,做一个类似这样的where子句(伪代码警报!):

SELECT * FROM Ingredients WHERE
IngredientsName = ["Name" property on entities in my ingredientsList] AND
IngredientsAmount <= ["Amount" property on entities in my ingredientsList]



我当然希望使用LINQ完成此操作,而不是使用动态生成的SQL查询。

6 个答案:

答案 0 :(得分:7)

LINQ是可组合的,但要在不使用UNION的情况下执行此操作,您必须滚动自己的Expression。基本上,我们(大概)想要创建以下形式的TSQL:

SELECT *
FROM   [table]
WHERE  (Name = @name1 AND Amount <= @amount1)
OR     (Name = @name2 AND Amount <= @amount2)
OR     (Name = @name3 AND Amount <= @amount3)
...

其中名称/数量对在运行时确定。在LINQ中有简单的措辞方式;如果每次都是“AND”,我们可以反复使用.Where(...)Union是候选人,但我看到一再有人对此有疑问。我们想做的是模仿我们编写LINQ查询,如:

var qry = from i in db.Ingredients
          where (  (i.Name == name1 && i.Amount <= amount1)
                || (i.Name == name2 && i.Amount <= amount2)
                ... )
          select i;

这是通过制作Expression来完成的,使用Expression.OrElse来组合每个 - 所以我们需要迭代我们的名称/数量对,从而使Expression更加丰富。

手工编写Expression代码有点像黑色艺术,但我的袖子上有一个非常相似的例子(来自我给出的演示文稿);它使用一些自定义扩展方法;用法通过:

    IQueryable query = db.Ingredients.WhereTrueForAny(
        localIngredient => dbIngredient =>
                   dbIngredient.Name == localIngredient.Name
                && dbIngredient.Amount <= localIngredient.Amount
            , args);

其中args是您的测试成分数组。它的作用是:对于localIngredient(我们的本地测试成分数组)中的每个args,它要求我们提供Expression(对于那个localIngredient)测试在数据库中应用。然后它将这些(反过来)与Expression.OrElse

组合在一起
public static IQueryable<TSource> WhereTrueForAny<TSource, TValue>(
    this IQueryable<TSource> source,
    Func<TValue, Expression<Func<TSource, bool>>> selector,
    params TValue[] values)
{
    return source.Where(BuildTrueForAny(selector, values));
}
public static Expression<Func<TSource, bool>> BuildTrueForAny<TSource, TValue>(
    Func<TValue, Expression<Func<TSource, bool>>> selector,
    params TValue[] values)
{
    if (selector == null) throw new ArgumentNullException("selector");
    if (values == null) throw new ArgumentNullException("values");
    // if there are no filters, return nothing
    if (values.Length == 0) return x => false;
    // if there is 1 filter, use it directly
    if (values.Length == 1) return selector(values[0]);

    var param = Expression.Parameter(typeof(TSource), "x");
    // start with the first filter
    Expression body = Expression.Invoke(selector(values[0]), param);
    for (int i = 1; i < values.Length; i++)
    { // for 2nd, 3rd, etc - use OrElse for that filter
        body = Expression.OrElse(body,
            Expression.Invoke(selector(values[i]), param));
    }
    return Expression.Lambda<Func<TSource, bool>>(body, param);
}

答案 1 :(得分:3)

在LINQ 2 SQL查询中使用本地集合的唯一程度是Contains()函数,它基本上是对SQL in子句的转换。例如......

var ingredientsList = new List<Ingredient>();

... add your ingredients

var myQuery = (from ingredient in context.Ingredients where ingredientsList.Select(i => i.Name).Contains(ingredient.Name) select ingredient);

这将生成相当于“...where ingredients.Name in (...)

的SQL

不幸的是,我不认为这对你有用,因为你必须原子地加入每一列。

另外,使用LINQ 2 SQL 动态生成的SQL查询。

当然,您可以在客户端进行加入,但这需要恢复整个Ingredients表,这可能性能过高,而且绝对不好实践。

答案 2 :(得分:1)

我认为你要么必须使用多个查询,要么将你的成分列表复制到临时表中并以这种方式进行数据库查询。

我的意思是,你可能有一个SQL语句:

SELECT * FROM Ingredients WHERE
(IngredientsName = 'Flour' AND IngredientsAmount < 10) OR   
(IngredientsName = 'Water' AND IngredientsAmount <= 5) OR
(IngredientsName = 'Eggs' AND IngredientsAmount <= 20)

但很快就会变丑。

我个人怀疑临时表解决方案将是最好的 - 但我不知道LINQ to SQL是否对它们有很多支持。

答案 3 :(得分:0)

List<string> ingredientNames = ingredientsList
  .Select( i => i.Name).ToList();
Dictionary<string, Double> ingredientValues = ingredientsList
  .ToDictionary(i => i.Name, i => i.Amount);
//database hit
List<Ingredient> queryResults = db.Ingredients
  .Where(i => ingredientNames.Contains(i.Name))
  .ToList();
//continue filtering locally - TODO: handle case-sensitivity
List<Ingredient> filteredResults = queryResults
  .Where(i => i.Amount <= ingredientValues[i.Name])
  .ToList();

答案 4 :(得分:0)

我在LINQPad中搞乱了这个解决方案,如果有的话,你可以看到转储输出。不确定它是否是你需要的,但从我的理解是它。我在我的Users表中使用它,但您可以将其替换为Ingredients,将“UserList”替换为“IngredientList”,将“Username”替换为“Ingredient Name”。您可以在if语句中添加更多“OR”过滤表达式。设置ID非常重要。

最后请注意,“Dump()”方法特定于LINQPad,不是必需的。

var userList = new List<User>();
userList.Add(new User() { ID = 1, Username = "goneale" });
userList.Add(new User() { ID = 2, Username = "Test" });

List<int> IDs = new List<int>();
//                       vv ingredients from db context
IQueryable<User> users = Users;
foreach(var user in userList)
{
    if (users.Any(x => x.Username == user.Username))
        IDs.Add(user.ID);
}
IDs.Dump();
userList.Dump();
users.Dump();
users = users.Where(x => IDs.Contains(x.ID));
users.Dump();

答案 5 :(得分:0)

我正在使用Union合并每个子查询的结果:

 public static IQueryable<TSource> WhereTrueForAny<TSource, TValue>(this IQueryable<TSource> source, Func<TValue, Expression<Func<TSource, bool>>> selector, params TValue[] values)
    {
        // code is based on Marc Gravells answer
        if (selector == null) throw new ArgumentNullException("selector");
        if (values == null) throw new ArgumentNullException("values");

        // if there are no filters, return nothing
        if (values.Length == 0) return source.Where(x => false);
        // if there is 1 filter, use it directly
        if (values.Length == 1) return source.Where(selector(values[0]));

        var lockingUpArray = values;

        var p = lockingUpArray.First();

        IQueryable<TSource> query = source.Where(selector(p));

        foreach (var param in lockingUpArray.Skip(1))
        {
            query = query.Union(source.Where(selector(param)));
        }

        return query;
    }