Linq 采取不同的方式

Posted

技术标签:

【中文标题】Linq 采取不同的方式【英文标题】:Linq take distinct 【发布时间】:2021-11-04 20:08:37 【问题描述】:

我正在尝试实现 Linq to SQL 运算符(EF 核心)

q.TakeDistinct(n, src => src.Field)

所以它返回来自 q 的前 n 个元素,只计算不同的 Field

基本上我想要一个Take,它不会计算字段中没有差异的元素

例如:

Job Status
10   start
10   progress
10   alert
10   done
12   start
12   error
12   done
32   start
32   info
32   done

然后

ctx.Table.TakeDistinct(2, v => v.Job)

将返回:

10   start
10   progress
10   alert
10   done
12   start
12   error
12   done

我尝试了 TakeDistinct 的各种组合,但没有运气......任何线索都值得赞赏。

注意我正在寻找可以通过 Linq-to-SQL 转换为合理高效 SQL 的东西(不在内存处理中)

编辑:

这行得通:

ctx.Table.Where(z => 
  Table.Select(x => x.Field).Distinct().Take(n)
  .Contains(z.Field)
);

如果 真的 很慢(在 LinqPad 中).. 有更好的方法吗?

【问题讨论】:

@Llama - 我强烈怀疑 OP 正在寻找与***.com/questions/489258/… 等效的 SQL(按属性区分对象)。 @Llama Distinct 实际上不会给出一个对象......你会想要来自 MoreLinq 的 DistinctBy (或类似的东西)。 IQueryable 或 IEnumerable 需要它? @SvyatoslavDanyliv IQueryable 【参考方案1】:

以下扩展生成表达式树,它使Distinct().Take(x) 生成并连接到原始查询。它还接受 x => new x.Id, x.Id2 参数作为不同的键。

public static class QueryableExtensions

    public static IQueryable<T> TakeDistinct<T, TKey>(this IQueryable<T> source, int take,
        Expression<Func<T, TKey>> distinctBy)
    
        var distinctQuery = source.Select(distinctBy).Distinct().Take(take);

        var distinctParam = Expression.Parameter(typeof(TKey), "d");
        var entityParam   = distinctBy.Parameters[0];

        var mapping = MapMembers(distinctBy.Body, distinctParam).ToList();

        var whereExpr = mapping.Select(t => Expression.Equal(t.Item1, t.Item2))
            .Aggregate(Expression.AndAlso);
        var whereLambda = Expression.Lambda(whereExpr, entityParam);

        var selectManySelector =
            Expression.Lambda(
                Expression.Convert(
                    Expression.Call(typeof(Queryable), nameof(Queryable.Where), new[]  typeof(T) ,
                        source.Expression,
                        whereLambda),
                    typeof(IEnumerable<T>)
                ),
                distinctParam
            );

        var selectManyQuery = Expression.Call(typeof(Queryable), nameof(Queryable.SelectMany),
            new[]  typeof(TKey), typeof(T) , distinctQuery.Expression, selectManySelector);


        return source.Provider.CreateQuery<T>(selectManyQuery);
    

    private static IEnumerable<Tuple<Expression, Expression>> MapMembers(Expression expr, Expression projectionPath)
    
        switch (expr.NodeType)
        
            case ExpressionType.New:
            
                var ne = (NewExpression)expr;

                for (int i = 0; i < ne.Arguments.Count; i++)
                
                    foreach (var e in MapMembers(ne.Arguments[i], Expression.MakeMemberAccess(projectionPath, ne.Members[i])))
                    
                        yield return e;
                    
                
                break;
            

            default:
                yield return Tuple.Create(projectionPath, expr);
                break;
        
    


【讨论】:

谢谢!我看不出 EF 如何将其转换为 SQL 什么意思?它应该转换为 SQL。或者您需要说明如何启用日志记录? 坦克很多。有没有办法提供用例和为 EF 核心创建的 SQL?我不相信我可以将其粘贴到 LinqPad 中? 为什么不呢? LINQPad 只执行 C# 编译器,然后执行您的代码。【参考方案2】:

我理解你的想法。我写了这个扩展方法来实现。 但它需要优化,因为滥用 LinQ 可能会影响性能。

public static IEnumerable<T> TakeDistinct<T, TProperty>(this IEnumerable<T> source, int take, Expression<Func<T, TProperty>> distinctBy)
    
        var orderList = source.AsQueryable().GroupBy(distinctBy).OrderBy(x=>x.Key).Take(take).Select(x=>x.Key).ToList();
        var mappingFunc = distinctBy.Compile();
        return source.Where(x => orderList.Contains(mappingFunc.Invoke(x)));
    
static void Main()
    

        var jobLines = @"10   start
10   progress
10   alert
10   done
12   start
12   error
12   done
32   start
32   info
32   done";

        var jobs = jobLines.Split('\n').Select(line =>
        
            var splited = line.Split(new[]  "   " , StringSplitOptions.RemoveEmptyEntries);
            return new JobItem
            
                Job = Convert.ToInt32(splited[0]),
                Status = splited[1]
            ;
        );

        var newJobs = jobs.TakeDistinct(2, x => x.Job);
        foreach (var job in newJobs)
        
            Console.WriteLine($"job.Job - job.Status");
        
        Console.ReadLine();
    

    class JobItem
    
        public int Job  get; set; 
        public string Status  get; set; 
    

    public static IEnumerable<T> TakeDistinct<T, TProperty>(this IEnumerable<T> source, int take, Expression<Func<T, TProperty>> distinctBy)
    
        var orderList = source.AsQueryable().GroupBy(distinctBy).OrderBy(x=>x.Key).Take(take).Select(x=>x.Key).ToList();
        var mappingFunc = distinctBy.Compile();
        return source.Where(x => orderList.Contains(mappingFunc.Invoke(x)));
    

--- 已更新----

本次更新是为了支持 LinQ to SQL

public static IQueryable<T> TakeDistinct<T, TProperty>(this IQueryable<T> source, int take, Expression<Func<T, TProperty>> distinctBy)

    var orderList = source.AsQueryable().GroupBy(distinctBy).OrderBy(x => x.Key).Take(take).SelectMany(x => x);
    return orderList;

--- 已更新----

我从上面的语句中添加了生成的 SQL。 我创建了假表名JobItems,列:IdJobStatus

SELECT 
    [Extent2].[Id] AS [Id], 
    [Extent2].[Job] AS [Job], 
    [Extent2].[Status] AS [Status]
    FROM   (SELECT TOP (2) [Distinct1].[Job] AS [Job]
        FROM ( SELECT DISTINCT 
            [Extent1].[Job] AS [Job]
            FROM [dbo].[JobItems] AS [Extent1]
        )  AS [Distinct1]
        ORDER BY [Distinct1].[Job] ASC ) AS [Limit1]
    INNER JOIN [dbo].[JobItems] AS [Extent2] ON [Limit1].[Job] = [Extent2].[Job]

【讨论】:

谢谢!我认为这不会很好地转化为 SQL :( 对不起,我忘记了 SQL 部分。它不适用于 SQL :) @kofifus:Linq to SQL 支持请参考我最后的更新。 上面 OrderBy 中的Key 是什么? @kofifus:这取决于distinctBy 参数。 KeyIGrouping&lt;TKey, TElement&gt; 的财产。在这种情况下,KeyJob【参考方案3】:

在 SQL 中,我会查看 WHERE IN 子查询。

SELECT JobId, Status
FROM Jobs
WHERE Status In (SELECT DISTINCT TOP 2 Status FROM Jobs)

没有太多使用 LinqToSQL 的经验,但这可能会直接起作用:

var TakeJobs = db.Jobs.Select(j => j.JobId).Distinct().Take(2);
var entries = db.Jobs.Where(j => TakeJobs.Contains(j.JobId));

显然,加入子查询可能会更便宜。我不确定它在 Linq 中的外观。

SELECT JobId, Status FROM Jobs j
RIGHT JOIN (SELECT DISTINCT TOP 2 JobId FROM Jobs) t ON t.JobId = j.JobId;

【讨论】:

以上是关于Linq 采取不同的方式的主要内容,如果未能解决你的问题,请参考以下文章

状态机/迭代器/LINQ/协程

策略模式

LINQ 的可扩展性如何? [关闭]

linq:在不同类型的条件下左加入 linq

LINQ:不同的值

Linq 过滤器避免循环