Aracılığıyla paylaş


Kullanıcı tanımlı işlev eşleme

EF Core, sorgularda kullanıcı tanımlı SQL işlevlerinin kullanılmasına olanak tanır. Bunu yapmak için, model yapılandırması sırasında işlevlerin bir CLR yöntemine eşlenmesi gerekir. LINQ sorgusunu SQL'e çevirirken, eşleştirildiği CLR işlevi yerine kullanıcı tanımlı işlev çağrılır.

Bir yöntemi SQL işlevine eşleme

Kullanıcı tanımlı işlev eşlemesinin nasıl çalıştığını göstermek için aşağıdaki varlıkları tanımlayalım:

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; }
}

Ve aşağıdaki model yapılandırması:

modelBuilder.Entity<Blog>()
    .HasMany(b => b.Posts)
    .WithOne(p => p.Blog);

modelBuilder.Entity<Post>()
    .HasMany(p => p.Comments)
    .WithOne(c => c.Post);

Blogda birçok gönderi olabilir ve her gönderinin birçok yorumu olabilir.

Ardından, blogu temel alarak Idbelirli bir blog için en az bir yorum içeren gönderi sayısını döndüren kullanıcı tanımlı işlevini CommentedPostCountForBlogoluşturun:

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'da bu işlevi kullanmak için, kullanıcı tanımlı işlevle eşlediğimiz aşağıdaki CLR yöntemini tanımlarız:

public int ActivePostCountForBlog(int blogId)
    => throw new NotSupportedException();

CLR yönteminin gövdesi önemli değildir. EF Core bağımsız değişkenlerini çeviremedikçe yöntemi istemci tarafında çağrılmaz. Bağımsız değişkenler çevrilebiliyorsa EF Core yalnızca yöntem imzasını önemser.

Dekont

Örnekte, yöntemi üzerinde DbContexttanımlanır, ancak diğer sınıfların içinde statik bir yöntem olarak da tanımlanabilir.

Bu işlev tanımı artık model yapılandırmasında kullanıcı tanımlı işlevle ilişkilendirilebilir:

modelBuilder.HasDbFunction(typeof(BloggingContext).GetMethod(nameof(ActivePostCountForBlog), new[] { typeof(int) }))
    .HasName("CommentedPostCountForBlog");

VARSAYıLAN olarak EF Core, CLR işlevini aynı ada sahip kullanıcı tanımlı bir işlevle eşlemeye çalışır. Adlar farklıysa, eşlemek istediğimiz kullanıcı tanımlı işlev için doğru adı sağlamak için kullanabiliriz HasName .

Şimdi aşağıdaki sorguyu yürütüyoruz:

var query1 = from b in context.Blogs
             where context.ActivePostCountForBlog(b.BlogId) > 1
             select b;

Bu SQL'i oluşturur:

SELECT [b].[BlogId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
WHERE [dbo].[CommentedPostCountForBlog]([b].[BlogId]) > 1

Bir yöntemi özel SQL'e eşleme

EF Core, belirli bir SQL'e dönüştürülen kullanıcı tanımlı işlevlere de olanak tanır. SQL ifadesi, kullanıcı tanımlı işlev yapılandırması sırasında yöntemi kullanılarak HasTranslation sağlanır.

Aşağıdaki örnekte, iki tamsayı arasındaki yüzde farkını hesaplayan bir işlev oluşturacağız.

CLR yöntemi aşağıdaki gibidir:

public double PercentageDifference(double first, int second)
    => throw new NotSupportedException();

İşlev tanımı aşağıdaki gibidir:

// 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));

İşlevi tanımladıktan sonra sorguda kullanılabilir. EF Core, veritabanı işlevini çağırmak yerine, HasTranslation'dan derlenen SQL ifade ağacını temel alarak yöntem gövdesini doğrudan SQL'e çevirir. Aşağıdaki LINQ sorgusu:

var query2 = from p in context.Posts
             select context.PercentageDifference(p.BlogId, 3);

Aşağıdaki SQL'i üretir:

SELECT 100 * (ABS(CAST([p].[BlogId] AS float) - 3) / ((CAST([p].[BlogId] AS float) + 3) / 2))
FROM [Posts] AS [p]

Bağımsız değişkenlerine göre kullanıcı tanımlı işlevin null atanabilirliğini yapılandırma

Kullanıcı tanımlı işlev yalnızca bağımsız değişkenlerinden nullbiri veya daha fazlası olduğunda döndürebiliyorsa null EFCore bunu belirtmek için bir yol sağlar ve bu da daha yüksek performanslı SQL ile sonuçlanır. İlgili işlev parametreleri model yapılandırmasına bir PropagatesNullability() çağrı eklenerek yapılabilir.

Bunu göstermek için kullanıcı işlevini ConcatStringstanımlayın:

CREATE FUNCTION [dbo].[ConcatStrings] (@prm1 nvarchar(max), @prm2 nvarchar(max))
RETURNS nvarchar(max)
AS
BEGIN
    RETURN @prm1 + @prm2;
END

ve bu yöntemle eşleyen iki CLR yöntemi:

public string ConcatStrings(string prm1, string prm2)
    => throw new InvalidOperationException();

public string ConcatStringsOptimized(string prm1, string prm2)
    => throw new InvalidOperationException();

Model yapılandırması (iç OnModelCreating yöntem) aşağıdaki gibidir:

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();
    });

İlk işlev standart şekilde yapılandırılır. İkinci işlev, null olabilirlik yayma iyileştirmesi avantajından yararlanacak şekilde yapılandırılır ve işlevin null parametreler etrafında nasıl davrandığını daha fazla bilgi sağlar.

Aşağıdaki sorguları oluştururken:

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");

Şu SQL'i alacağız:

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)

İkinci sorgunun null atanabilirliğini test etmek için işlevin kendisini yeniden değerlendirmesi gerekmez.

Dekont

Bu iyileştirme yalnızca işlevin yalnızca parametreleri nullolduğunda döndürebildiği null durumlarda kullanılmalıdır.

Sorgulanabilir bir işlevi tablo değerli bir işleve eşleme

EF Core ayrıca varlık türlerinden birini döndüren kullanıcı tanımlı bir CLR yöntemi kullanarak tablo değerli bir IQueryable işleve eşlemeyi de destekler ve EF Core'un TVF'leri parametrelerle eşlemesine olanak tanır. İşlem, skaler kullanıcı tanımlı bir işlevi SQL işleviyle eşlemeye benzer: Veritabanında TVF, LINQ sorgularında kullanılan bir CLR işlevi ve ikisi arasındaki eşleme gerekir.

Örneğin, belirli bir "Beğen" eşiğine uyan en az bir açıklamaya sahip tüm gönderileri döndüren tablo değerli bir işlev kullanacağız:

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 yöntemi imzası aşağıdaki gibidir:

public IQueryable<Post> PostsWithPopularComments(int likeThreshold)
    => FromExpression(() => PostsWithPopularComments(likeThreshold));

Bahşiş

FromExpression CLR işlev gövdesindeki çağrı, işlevin normal bir DbSet yerine kullanılmasına olanak tanır.

Eşleme de aşağıdadır:

modelBuilder.Entity<Post>().ToTable("Posts");
modelBuilder.HasDbFunction(typeof(BloggingContext).GetMethod(nameof(PostsWithPopularComments), new[] { typeof(int) }));

Dekont

Sorgulanabilir bir işlev tablo değerli bir işlevle eşlenmelidir ve işlevini kullanamaz HasTranslation.

İşlev eşlendiğinde aşağıdaki sorgu:

var likeThreshold = 3;
var query5 = from p in context.PostsWithPopularComments(likeThreshold)
             orderby p.Rating
             select p;

Üretir:

SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [dbo].[PostsWithPopularComments](@likeThreshold) AS [p]
ORDER BY [p].[Rating]