如何在 EF Core 中使用 DbFunction 翻译?

Posted

技术标签:

【中文标题】如何在 EF Core 中使用 DbFunction 翻译?【英文标题】:How to use DbFunction translation in EF Core? 【发布时间】:2019-12-04 00:17:45 【问题描述】:

我正在寻找类似 @​​987654324@ 的东西,它在 SQL Server 中实现,但使用 mysqlMATCH...AGAINST 语法。

这是我目前的工作流程: AspNetCore 2.1.1 EntityFrameworkCore 2.1.4 Pomelo.EntityFrameworkCore.MySql 2.1.4

问题是 MySQL 使用两个函数,我不知道如何用 DbFunction 解释它,并将每个参数分开。有谁知道如何实现这个?

这应该是 Linq 语法:

query.Where(x => DbContext.FullText(new[]  x.Col1, x.Col2, x.Col3 , "keywords"));

这应该是SQL生成的结果:

SELECT * FROM t WHERE MATCH(`Col1`, `Col2`, `Col3`) AGAINST('keywords');

我正在尝试使用HasTranslation 函数来遵循以下示例: https://github.com/aspnet/EntityFrameworkCore/issues/11295#issuecomment-511440395 https://github.com/aspnet/EntityFrameworkCore/issues/10241#issuecomment-342989770

注意:我知道可以用FromSql 解决,但这不是我要找的。​​em>

【问题讨论】:

所讨论的方法的签名是什么? 我们可以遵循他们在EF Core 中使用的相同设计模式,但使用数组来接受多个属性,例如public static bool FullText(string[] propertyReferences, string fullText) 问题是目前new [] … 不支持在表达式内部,所以我们的翻译根本不会被调用。不幸的是,您无法在查询之外创建数组,因为您需要从x => 访问x。创建具有多个重载(string, string)(string, string, string)(string, string, string, string) 等的函数虽然相对容易。让我知道这是否适合你。因为处理 new [] … 参数需要深入研究 EF Core 基础架构。 【参考方案1】:

当我需要 EF Core 中的 ROW_NUMBER 支持时,您的用例与我的非常相似。

例子:

// gets translated to
// ROW_NUMBER() OVER(PARTITION BY ProductId ORDER BY OrderId, Count)
DbContext.OrderItems.Select(o => new 
  RowNumber = EF.Functions.RowNumber(o.ProductId, new 
    o.OrderId,
    o.Count
  )
)

使用匿名类而不是数组

您要做的第一件事是从使用数组切换到匿名类,即您将调用从

DbContext.FullText(new[] x.Col1, x.Col2, x.Col3 , "keywords")

DbContext.FullText(new x.Col1, x.Col2, x.Col3 , "keywords")

参数的排序顺序将保持在查询中定义的顺序, 即new x.Col1, x.Col2 将被翻译成Col1, Col2new x.Col2, x.Col1 Col2, Col1

您甚至可以使用以下内容:new x.Col1, _ = x.Col1, Foo = "bar" 将被翻译为 Col1, Col1, 'bar'

实现自定义IMethodCallTranslator

如果您需要一些提示,可以在Azure DevOps: RowNumber Support 上查看我的代码,或者如果您可以等待几天,那么我将提供有关自定义函数实现的博客文章。

更新(2019 年 7 月 31 日)

博文:

Entity Framework Core: Custom Functions (using IMethodCallTranslator) Entity Framework Core: Custom Functions (using HasDbFunction)

更新(2019 年 7 月 27 日)

感谢下面的 cmets,我看到需要进行一些澄清。

1) 正如下面评论中所指出的,还有另一种方法。使用HasDbFunction,我可以节省一些输入,例如使用 EF 注册翻译器的代码,但我仍然需要RowNumberExpression,因为该函数有 2 组参数(PARTITION BYORDER BY)和现有的SqlFunctionExpression 不支持。 (或者我错过了什么?)我选择IMethodCallTranslator 方法的原因是因为我希望在设置DbContextOptionsBuilder 而不是在OnModelCreating 中完成此功能的配置。也就是说,这是我的个人喜好。

最后,线程创建者也可以使用HasDbFunction 来实现所需的功能。就我而言,代码如下所示:

// OnModelCreating
  var methodInfo = typeof(DemoDbContext).GetMethod(nameof(DemoRowNumber));

  modelBuilder.HasDbFunction(methodInfo)
            .HasTranslation(expressions => 
                 var partitionBy = (Expression[])((ConstantExpression)expressions.First()).Value;
                 var orderBy = (Expression[])((ConstantExpression)expressions.Skip(1).First()).Value;

                 return new RowNumberExpression(partitionBy, orderBy);
);

// the usage with this approach is identical to my current approach
.Select(c => new 
    RowNumber = DemoDbContext.DemoRowNumber(
                                  new  c.Id ,
                                  new  c.RowVersion )
    )

2) 匿名类型不能强制执行其成员的类型,因此如果使用 integer 而不是 @ 调用函数,您可能会遇到运行时异常987654347@。尽管如此,它仍然是有效的解决方案。根据您正在为解决方案工作的客户,您的解决方案可能或多或少可行,最终决定权在于客户。不提供任何替代方案也是一种可能的解决方案,但并不令人满意。 特别是,如果不希望使用 SQL(因为您从编译器获得的支持更少),那么运行时异常毕竟可能是一个很好的折衷方案。

但是,如果妥协仍然不可接受,那么我们可以研究如何添加对数组的支持。 第一种方法可能是实现自定义 IExpressionFragmentTranslator 以将数组的处理“重定向”给我们。

请注意,这只是一个原型,需要更多调查/测试:-)

// to get into EF pipeline
public class DemoArrayTranslator : IExpressionFragmentTranslator

    public Expression Translate(Expression expression)
    
       if (expression?.NodeType == ExpressionType.NewArrayInit)
       
          var arrayInit = (NewArrayExpression)expression;
          return new DemoArrayInitExpression(arrayInit.Type, arrayInit.Expressions);
       

       return null;
    


// lets visitors visit the array-elements
public class DemoArrayInitExpression : Expression

   private readonly ReadOnlyCollection<Expression> _expressions;

   public override Type Type  get; 
   public override ExpressionType NodeType => ExpressionType.Extension;

   public DemoArrayInitExpression(Type type, 
           ReadOnlyCollection<Expression> expressions)
   
      Type = type ?? throw new ArgumentNullException(nameof(type));
      _expressions = expressions ?? throw new ArgumentNullException(nameof(expressions));
   

   protected override Expression Accept(ExpressionVisitor visitor)
   
      var visitedExpression = visitor.Visit(_expressions);
      return NewArrayInit(Type.GetElementType(), visitedExpression);
   


// adds our DemoArrayTranslator to the others
public class DemoRelationalCompositeExpressionFragmentTranslator 
      : RelationalCompositeExpressionFragmentTranslator

    public DemoRelationalCompositeExpressionFragmentTranslator(
             RelationalCompositeExpressionFragmentTranslatorDependencies dependencies)
         : base(dependencies)
      
         AddTranslators(new[]  new DemoArrayTranslator() );
      
   

// Register the translator
services
  .AddDbContext<DemoDbContext>(builder => builder
       .ReplaceService<IExpressionFragmentTranslator,
                       DemoRelationalCompositeExpressionFragmentTranslator>());

为了测试,我引入了另一个包含Guid[] 作为参数的重载。 虽然,这种方法在我的用例中根本没有意义 :)

public static long RowNumber(this DbFunctions _, Guid[] orderBy) 

并调整了方法的使用

// Translates to ROW_NUMBER() OVER(ORDER BY Id)
.Select(c => new  
                RowNumber = EF.Functions.RowNumber(new Guid[]  c.Id )
) 

【讨论】:

很好的尝试,但是... (1) 过于复杂。添加简单方法的管道代码过多。如果该方法不需要自定义表达式,HasDbFunction + HasTranslation 应该足够了。 (2) new x.Col1, x.Col2, x.Col3 new string[] x.Col1, x.Col2, x.Col3 不同——后者是编译时安全的,而前者不是(不强制所有成员都是string 类型)。我理解您为什么建议这样做,但大多数阅读的人不会 - 当前 EF Core 方法参数转换缺乏new T[] ... 支持的类型不安全的解决方法。 @IvanStoev 感谢 cmets,他们给我带来了一些想法!我在原始答案中添加了几个部分。 添加了有关自定义函数实现的博客文章链接。 我尝试实现它但对我来说非常复杂,您认为您可以帮助我实现它吗? 嗨@PawelGerr - 我一直在努力让 EF.Functions.RowNumber 与我的 ASP.NET Core 3.1 项目配合得很好,但我一直得到“这个方法仅与 Entity Framework Core 一起使用,并且没有内存实现。”尽管在我的 Startup.cs 中的 .UseSqlServer 之后添加了 .AddRowNumberSupport() ,但仍出现错误。具体来说,尝试直接从 LatestEmpireEntries DbSet 中选择以下内容:RowNumber = EF.Functions.RowNumber(EF.Functions.OrderBy(o.EmpireId))

以上是关于如何在 EF Core 中使用 DbFunction 翻译?的主要内容,如果未能解决你的问题,请参考以下文章