有EF Linq Select语句选择常数或函数

本文关键字:常数 函数 选择 语句 EF Linq Select | 更新日期: 2023-09-27 18:08:27

我有一个Select语句,当前格式为

dbEntity
.GroupBy(x => x.date)
.Select(groupedDate => new {
                             Calculation1 = doCalculation1 ? x.Sum(groupedDate.Column1) : 0),
                             Calculation2 = doCalculation2 ? x.Count(groupedDate) : 0)

在查询中,doccalculation1和doccalculation2是前面设置的bool。这将在生成的Sql中创建一个case语句,如

DECLARE @p1 int = 1
DECLARE @p2 int = 0
DECLARE @p3 int = 1
DECLARE @p4 int = 0
SELECT (Case When @p1 = 1 THEN Sum(dbEntity.Column1)
     Else @p2
     End) as Calculation1,
     (Case When @p3 = 1 THEN Count(*)
     Else @p4
     End) as Calculation2

我想要生成的sql是这样的当doccalculation1为真

SELECT SUM(Column1) as Calculation1, Count(*)  as Calculation2

和这样当doCalculation2为false

SELECT 0 as Calculation1, Count(*) as Calculation2

是否有办法通过EF强制查询这样做?

编辑:

bool doCalculation = true;
bool doCalculation2 = false;
            dbEntity
            .Where(x => x.FundType == "E")
            .GroupBy(x => x.ReportDate)
              .Select(dateGroup => new 
              {
                  ReportDate = dateGroup.Key,
                  CountInFlows = doCalculation2 ? dateGroup.Count(x => x.Flow > 0) : 0,
                  NetAssetEnd = doCalculation ? dateGroup.Sum(x => x.AssetsEnd) : 0
              })
              .ToList();

生成sql

-- Region Parameters
DECLARE @p0 VarChar(1000) = 'E'
DECLARE @p1 Int = 0
DECLARE @p2 Decimal(5,4) = 0
DECLARE @p3 Int = 0
DECLARE @p4 Int = 1
DECLARE @p5 Decimal(1,0) = 0
-- EndRegion
SELECT [t1].[ReportDate], 
    (CASE 
        WHEN @p1 = 1 THEN (
            SELECT COUNT(*)
            FROM [dbEntity] AS [t2]
            WHERE ([t2].[Flow] > @p2) AND ([t1].[ReportDate] = [t2].[ReportDate]) AND ([t2].[FundType] = @p0)
        )
        ELSE @p3
     END) AS [CountInFlows], 
    (CASE 
        WHEN @p4 = 1 THEN CONVERT(Decimal(33,4),[t1].[value])
        ELSE CONVERT(Decimal(33,4),@p5)
     END) AS [NetAssetEnd]
FROM (
    SELECT SUM([t0].[AssetsEnd]) AS [value], [t0].[ReportDate]
    FROM [dbEntity] AS [t0]
    WHERE [t0].[FundType] = @p0
    GROUP BY [t0].[ReportDate]
    ) AS [t1]

,它在执行计划中有许多索引扫描和一个假脱机和一个连接。在测试集上运行平均需要大约20秒,而生产集要大得多。

我希望它的运行速度与

等sql相同
select reportdate, 1, sum(AssetsEnd)
from vwDailyFundFlowDetail
where fundtype = 'E'
group by reportdate

平均运行时间约为12秒,并且在执行计划中将大部分查询绑定在单个索引查找中。实际的sql输出是什么并不重要,但是case语句的性能似乎要差得多。

至于我为什么这样做,我需要像我在动态生成Linq选择中要求的那样生成一个动态选择语句。用户可以选择执行一组计算中的一个或多个,在请求到来之前,我不知道选择了什么。这些请求开销很大,所以除非必要,否则我们不希望运行它们。我正在根据用户请求设置doccalculation工具

这个查询应该替换一些代码,这些代码从存储为字符串的硬编码sql查询中插入或删除字符,然后执行。它运行得相当快,但是维护

有EF Linq Select语句选择常数或函数

是一场噩梦。

从技术上讲,可以通过表达式树访问器在Select查询中传递表达式,该访问器检查三元操作符左侧的常量值,并用适当的子表达式替换三元表达式。

例如:

public class Simplifier : ExpressionVisitor
{
    public static Expression<T> Simplify<T>(Expression<T> expr)
    {
        return (Expression<T>) new Simplifier().Visit(expr);
    }
    protected override Expression VisitConditional(ConditionalExpression node)
    {
        var test = Visit(node.Test);
        var ifTrue = Visit(node.IfTrue);
        var ifFalse = Visit(node.IfFalse);
        var testConst = test as ConstantExpression;
        if(testConst != null)
        {
            var value = (bool) testConst.Value;
            return value ? ifTrue : ifFalse;
        }
        return Expression.Condition(test, ifTrue, ifFalse);
    }
    protected override Expression VisitMember(MemberExpression node)
    {
        // Closed-over variables are represented as field accesses to fields on a constant object.
        var field = (node.Member as FieldInfo);
        var closure = (node.Expression as ConstantExpression);
        if(closure != null)
        {
            var value = field.GetValue(closure.Value);
            return VisitConstant(Expression.Constant(value));
        }
        return base.VisitMember(node);
    }
}

使用例子:

void Main()
{
    var b = true;
    Expression<Func<int, object>> expr = i => b ? i.ToString() : "N/A";
    Console.WriteLine(expr.ToString()); // i => IIF(value(UserQuery+<>c__DisplayClass0).b, i.ToString(), "N/A")
    Console.WriteLine(Simplifier.Simplify(expr).ToString()); // i => i.ToString()
    b = false;
    Console.WriteLine(Simplifier.Simplify(expr).ToString()); // i => "N/A"
}
所以,你可以在你的代码中像这样使用它:

Expression<Func<IGrouping<DateTime, MyEntity>>, ClassYouWantToReturn> select = 
    groupedDate => new {
        Calculation1 = doCalculation1 ? x.Sum(groupedDate.Column1) : 0),
        Calculation2 = doCalculation2 ? x.Count(groupedDate) : 0
    };
var q = dbEntity
    .GroupBy(x => x.date)
    .Select(Simplifier.Simplify(select))

然而,这可能比它的价值更多的麻烦。SQL Server将毫无疑问地优化"1 == 1"的情况,允许实体框架产生不太漂亮的查询不应该被证明是一个性能问题。

更新

查看更新的问题,这似乎是少数几个产生正确查询的实例之一,在性能方面确实很重要。

除了我建议的解决方案之外,还有其他一些选择:您可以使用原始sql映射到您的返回类型,或者您可以使用LinqKit根据您想要的选择不同的表达式,然后在Select查询中"调用"该表达式。