如何在EF Core中使用DbFunction转换?

5
我正在寻找与SQL Server中实现的EF.Functions.FreeText类似的东西,但使用MySQL的MATCH...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生成的结果:

这应该是在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解决,但这并不是我要找的。

这个问题中的方法签名是什么? - Ivan Stoev
我们可以遵循EF Core中使用的相同设计模式,但使用数组来接受多个属性,类似于这样 public static bool FullText(string[] propertyReferences, string fullText) - alejandroldev
问题在于,不幸的是目前new [] { … }不支持在表达式内部,因此我们的转换根本不会被调用。而且不幸的是,您无法在查询之外创建数组,因为您需要从x =>访问x。但是,创建一个带有多个重载(string, string)(string, string, string)(string, string, string, string)等的函数相对容易。如果这对您有用,请告诉我。因为处理new [] {…}参数将需要深入EF Core基础设施。 - Ivan Stoev
1个回答

6

当我需要在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日):
博客文章:
- 使用IMethodCallTranslator创建Entity Framework Core自定义函数 - 使用HasDbFunction创建Entity Framework Core自定义函数 更新(2019年7月27日):
感谢下方的评论,我发现需要一些澄清。 1) 如下方评论所指出的一样,还有另一种方法。使用 HasDbFunction 可以省去与 EF 注册翻译器的代码敲打,但仍需要 RowNumberExpression,因为该函数有两组参数(用于 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而不是string,则可能会出现运行时异常。尽管如此,它仍然可以是有效的解决方案。根据您为之工作的客户,解决方案可能更或者更少可行,最终决策取决于客户。不提供任何替代方案也是一种可能的解决方案,但并不令人满意。 特别是,如果不希望使用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[] { ... } 支持的方法。 - Ivan Stoev
@IvanStoev 感谢您的评论,它们给了我一些想法!我已经在我的原始答案中添加了几个部分。 - Pawel Gerr
嗨@PawelGerr-我一直在努力让EF.Functions.RowNumber与我的ASP.NET Core 3.1项目兼容,但是尽管在Startup.cs中添加了.AddRowNumberSupport()在.UseSqlServer之后,我仍然不断地收到"This method is for use with Entity Framework Core only and has no in-memory implementation."错误。具体来说,我正在尝试直接从LatestEmpireEntries DbSet中选择以下内容: RowNumber = EF.Functions.RowNumber(EF.Functions.OrderBy(o.EmpireId)) - JohannSig
@JohannSig,你是否正在使用2.0.0-beta004版本(https://www.nuget.org/packages/Thinktecture.EntityFrameworkCore.SqlServer/)? 如果是,并且你遇到了一些问题,那么你可以在devops中提交一个问题: https://dev.azure.com/pawelgerr/Thinktecture.EntityFrameworkCore/_wiki/wikis/Thinktecture.EntityFrameworkCore.wiki/14/RowNumber-Support - Pawel Gerr
@Pawel Gerr,很遗憾,由于公司的限制,我无法使用Thinktecture Nuget包。但是,我正在尝试使用上述方法实现分区的行号。我在提取分区和排序信息时遇到了问题。我得到的参数信息是以SQL表达式的形式呈现的。另外,请问一下使用的示例方法签名是什么。我正在使用.NET Core 3.1和EF Core 3.1。 - nallas
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接