如何在 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 翻译?的主要内容,如果未能解决你的问题,请参考以下文章

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

如何在 EF Core Like 函数中使用通配符

EF Core中的多对多映射如何实现?

如何在 EF Core 中实例化 DbContext

如何在 EF Core 中将内部联接与计算列一起使用?

如何在 Core 2.1 中引用 EF(Database First Approach)项目程序集