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);
ブログには多くの投稿があり、各投稿には多くのコメントを含めることができます。
次に、ブログのCommentedPostCountForBlogに基づいて、特定のブログに対して少なくとも 1 つのコメントを含む投稿の数を返す、ユーザー定義関数Idを作成します。
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), [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 メソッドを使用して提供されます。
次の例では、2 つの整数の差の割合を計算する関数を作成します。
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), [typeof(double), typeof(int)]))
.HasTranslation(
args =>
new SqlBinaryExpression(
ExpressionType.Multiply,
new SqlConstantExpression(100, new IntTypeMapping("int", DbType.Int32)),
new SqlBinaryExpression(
ExpressionType.Divide,
new SqlFunctionExpression(
"ABS",
[
new SqlBinaryExpression(
ExpressionType.Subtract,
args.First(),
args.Skip(1).First(),
args.First().Type,
args.First().TypeMapping)
],
nullable: true,
argumentsPropagateNullability: [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(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 許容の構成
1 つ以上の引数が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
およびそれにマップされる 2 つの 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), [typeof(string), typeof(string)]))
.HasName("ConcatStrings");
modelBuilder.HasDbFunction(
typeof(BloggingContext).GetMethod(nameof(ConcatStringsOptimized), [typeof(string), typeof(string)]),
b =>
{
b.HasName("ConcatStrings");
b.HasParameter("prm1").PropagatesNullability();
b.HasParameter("prm2").PropagatesNullability();
});
最初の関数は標準の方法で構成されます。 2 番目の関数は、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)
2 番目のクエリでは、null 値の許容をテストするために関数自体を再評価する必要はありません。
注
この最適化は、関数がパラメーターがnullされている場合にのみnullを返すことができる場合にのみ使用する必要があります。
クエリ可能な関数をテーブル値関数にマッピングする
EF Core では、エンティティ型の IQueryable を返すユーザー定義 CLR メソッドを使用したテーブル値関数へのマッピングもサポートされており、EF Core は TVF をパラメーターにマップできます。 このプロセスは、スカラー ユーザー定義関数を SQL 関数にマッピングすることと似ています。データベースには TVF、LINQ クエリで使用される CLR 関数、2 つの間のマッピングが必要です。
たとえば、特定の "いいね" しきい値を満たすコメントが少なくとも 1 つ含まれるすべての投稿を返すテーブル値関数を使用します。
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), [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]
.NET