如何在 EF Core 中使用 DbFunction 翻译?
Posted
技术标签:
【中文标题】如何在 EF Core 中使用 DbFunction 翻译?【英文标题】:How to use DbFunction translation in EF Core? 【发布时间】:2019-12-04 00:17:45 【问题描述】:我正在寻找类似 @987654324@ 的东西,它在 SQL Server 中实现,但使用 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生成的结果:
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, Col2
和new 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 BY
和 ORDER 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 翻译?的主要内容,如果未能解决你的问题,请参考以下文章