用户定义的函数映射
EF Core 允许在查询中使用用户定义的 SQL 函数。 为此,需要在模型配置过程中将函数映射到 CLR 方法。 将 LINQ 查询转换为 SQL 时,将调用用户定义的函数,而不是它已映射到的 CLR 函数。
将方法映射到 SQL 函数
为了说明用户定义的函数映射的工作原理,让我们定义以下实体:
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public int? Rating { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int Rating { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
public List<Comment> Comments { get; set; }
}
public class Comment
{
public int CommentId { get; set; }
public string Text { get; set; }
public int Likes { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
}
以及以下模型配置:
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog);
modelBuilder.Entity<Post>()
.HasMany(p => p.Comments)
.WithOne(c => c.Post);
博客可以有多篇帖文,每篇帖文可以有多条评论。
接下来,根据博客 Id
,创建用户定义的函数 CommentedPostCountForBlog
,该函数将返回针对给定博客至少具有一条评论的帖文计数:
CREATE FUNCTION dbo.CommentedPostCountForBlog(@id int)
RETURNS int
AS
BEGIN
RETURN (SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE ([p].[BlogId] = @id) AND ((
SELECT COUNT(*)
FROM [Comments] AS [c]
WHERE [p].[PostId] = [c].[PostId]) > 0));
END
为了在 EF Core 中使用此函数,我们将定义以下 CLR 方法,并将其映射到用户定义的函数:
public int ActivePostCountForBlog(int blogId)
=> throw new NotSupportedException();
CLR 方法的主体并不重要。 不会在客户端调用此方法,除非 EF Core 不能转换其参数。 如果可以转换参数,EF Core 只关心方法签名。
注意
在此示例中,方法是基于 DbContext
定义的,但也可以将其定义为其他类中的静态方法。
此函数定义现在可以与模型配置中用户定义的函数关联:
modelBuilder.HasDbFunction(typeof(BloggingContext).GetMethod(nameof(ActivePostCountForBlog), new[] { typeof(int) }))
.HasName("CommentedPostCountForBlog");
默认情况下,EF Core 尝试将 CLR 函数映射到具有相同名称的用户定义的函数。 如果名称不同,可以使用 HasName
为要映射到的用户定义的函数提供正确的名称。
接下来,执行下面的查询:
var query1 = from b in context.Blogs
where context.ActivePostCountForBlog(b.BlogId) > 1
select b;
将生成此 SQL:
SELECT [b].[BlogId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
WHERE [dbo].[CommentedPostCountForBlog]([b].[BlogId]) > 1
将方法映射到自定义 SQL
EF Core 还允许将用户定义的函数转换为特定 SQL。 SQL 表达式是在配置用户定义的函数过程中使用 HasTranslation
方法提供的。
在下面的示例中,我们将创建一个函数,用于计算两个整数之间的百分比差异。
CLR 方法如下所示:
public double PercentageDifference(double first, int second)
=> throw new NotSupportedException();
函数定义如下:
// 100 * ABS(first - second) / ((first + second) / 2)
modelBuilder.HasDbFunction(
typeof(BloggingContext).GetMethod(nameof(PercentageDifference), new[] { typeof(double), typeof(int) }))
.HasTranslation(
args =>
new SqlBinaryExpression(
ExpressionType.Multiply,
new SqlConstantExpression(
Expression.Constant(100),
new IntTypeMapping("int", DbType.Int32)),
new SqlBinaryExpression(
ExpressionType.Divide,
new SqlFunctionExpression(
"ABS",
new SqlExpression[]
{
new SqlBinaryExpression(
ExpressionType.Subtract,
args.First(),
args.Skip(1).First(),
args.First().Type,
args.First().TypeMapping)
},
nullable: true,
argumentsPropagateNullability: new[] { true, true },
type: args.First().Type,
typeMapping: args.First().TypeMapping),
new SqlBinaryExpression(
ExpressionType.Divide,
new SqlBinaryExpression(
ExpressionType.Add,
args.First(),
args.Skip(1).First(),
args.First().Type,
args.First().TypeMapping),
new SqlConstantExpression(
Expression.Constant(2),
new IntTypeMapping("int", DbType.Int32)),
args.First().Type,
args.First().TypeMapping),
args.First().Type,
args.First().TypeMapping),
args.First().Type,
args.First().TypeMapping));
定义函数后,可在查询中使用函数。 EF Core 会根据从 HasTranslation 构造的 SQL 表达式树将方法主体直接转换为 SQL,而不是调用数据库函数。 下面的 LINQ 查询可以:
var query2 = from p in context.Posts
select context.PercentageDifference(p.BlogId, 3);
生成以下 SQL:
SELECT 100 * (ABS(CAST([p].[BlogId] AS float) - 3) / ((CAST([p].[BlogId] AS float) + 3) / 2))
FROM [Posts] AS [p]
基于用户定义函数的参数配置该函数的为 null 性
如果用户定义的函数只能在其一个或多个参数为 null
时返回 null
,则 EFCore 会提供指定该参数的方法,从而提高 SQL 的性能。 可通过向相关函数参数模型配置添加 PropagatesNullability()
调用来完成此操作。
为了说明这一点,请定义用户函数 ConcatStrings
:
CREATE FUNCTION [dbo].[ConcatStrings] (@prm1 nvarchar(max), @prm2 nvarchar(max))
RETURNS nvarchar(max)
AS
BEGIN
RETURN @prm1 + @prm2;
END
和两个映射到该函数的 CLR 方法:
public string ConcatStrings(string prm1, string prm2)
=> throw new InvalidOperationException();
public string ConcatStringsOptimized(string prm1, string prm2)
=> throw new InvalidOperationException();
模型配置(在 OnModelCreating
方法中)如下所示:
modelBuilder
.HasDbFunction(typeof(BloggingContext).GetMethod(nameof(ConcatStrings), new[] { typeof(string), typeof(string) }))
.HasName("ConcatStrings");
modelBuilder.HasDbFunction(
typeof(BloggingContext).GetMethod(nameof(ConcatStringsOptimized), new[] { typeof(string), typeof(string) }),
b =>
{
b.HasName("ConcatStrings");
b.HasParameter("prm1").PropagatesNullability();
b.HasParameter("prm2").PropagatesNullability();
});
第一个函数是以标准方式配置的。 第二个函数配置为利用为 Null 性传播优化,并提供详细信息介绍函数如何围绕 null 参数做出行为。
发出以下查询时:
var query3 = context.Blogs.Where(e => context.ConcatStrings(e.Url, e.Rating.ToString()) != "https://mytravelblog.com/4");
var query4 = context.Blogs.Where(
e => context.ConcatStringsOptimized(e.Url, e.Rating.ToString()) != "https://mytravelblog.com/4");
获取此 SQL:
SELECT [b].[BlogId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
WHERE ([dbo].[ConcatStrings]([b].[Url], CONVERT(VARCHAR(11), [b].[Rating])) <> N'Lorem ipsum...') OR [dbo].[ConcatStrings]([b].[Url], CONVERT(VARCHAR(11), [b].[Rating])) IS NULL
SELECT [b].[BlogId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
WHERE ([dbo].[ConcatStrings]([b].[Url], CONVERT(VARCHAR(11), [b].[Rating])) <> N'Lorem ipsum...') OR ([b].[Url] IS NULL OR [b].[Rating] IS NULL)
第二个查询不需要重新计算函数本身来测试它的为 Null 性。
注意
仅当函数只有在参数为 null
才返回 null
时,才应使用此优化。
将可查询函数映射到表值函数
EF Core 还支持映射到表值函数,方法是使用返回实体类型的 IQueryable
的用户定义的 CLR 方法并允许 EF Core 映射带参数的 TVF。 此过程类似于将标量用户定义的函数映射到 SQL 函数:我们需要数据库中的 TVF、在 LINQ 查询中使用的 CLR 函数,以及两者之间的映射。
例如,我们将使用表值函数返回至少具有一个符合给定“赞”阈值的评论的所有帖文:
CREATE FUNCTION dbo.PostsWithPopularComments(@likeThreshold int)
RETURNS TABLE
AS
RETURN
(
SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Posts] AS [p]
WHERE (
SELECT COUNT(*)
FROM [Comments] AS [c]
WHERE ([p].[PostId] = [c].[PostId]) AND ([c].[Likes] >= @likeThreshold)) > 0
)
CLR 方法签名如下所示:
public IQueryable<Post> PostsWithPopularComments(int likeThreshold)
=> FromExpression(() => PostsWithPopularComments(likeThreshold));
提示
CLR 函数主体中的 FromExpression
调用允许使用函数而不是常规 DbSet。
下面是映射:
modelBuilder.Entity<Post>().ToTable("Posts");
modelBuilder.HasDbFunction(typeof(BloggingContext).GetMethod(nameof(PostsWithPopularComments), new[] { typeof(int) }));
注意
可查询函数必须映射到表值函数,并且不能使用 HasTranslation
。
映射函数后,请考虑以下查询:
var likeThreshold = 3;
var query5 = from p in context.PostsWithPopularComments(likeThreshold)
orderby p.Rating
select p;
生成:
SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [dbo].[PostsWithPopularComments](@likeThreshold) AS [p]
ORDER BY [p].[Rating]