Share via


Door de gebruiker gedefinieerde functie-mapping

EF Core maakt het gebruik van door de gebruiker gedefinieerde SQL-functies in query's mogelijk. Hiervoor moeten de functies tijdens de modelconfiguratie worden toegewezen aan een CLR-methode. Bij het vertalen van de LINQ-query naar SQL wordt de door de gebruiker gedefinieerde functie aangeroepen in plaats van de CLR-functie waaraan deze is toegewezen.

Een methode toewijzen aan een SQL-functie

Laten we de volgende entiteiten definiëren om te laten zien hoe door de gebruiker gedefinieerde functietoewijzing werkt:

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

En de volgende modelconfiguratie:

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

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

Blog kan veel berichten bevatten en elk bericht kan veel opmerkingen bevatten.

Maak vervolgens de door de gebruiker gedefinieerde functie CommentedPostCountForBlog, die het aantal berichten met ten minste één opmerking voor een bepaalde blog retourneert, op basis van het blog 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

Als u deze functie in EF Core wilt gebruiken, definiëren we de volgende CLR-methode, die we toewijzen aan de door de gebruiker gedefinieerde functie:

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

De inhoud van de CLR-methode is niet belangrijk. De methode wordt niet aangeroepen aan de clientzijde, tenzij EF Core de argumenten niet kan omzetten. Als de argumenten kunnen worden vertaald, geeft EF Core uitsluitend om de handtekening van de methode.

Opmerking

In het voorbeeld wordt de methode gedefinieerd op DbContext, maar deze kan ook worden gedefinieerd als een statische methode in andere klassen.

Deze functiedefinitie kan nu worden gekoppeld aan de door de gebruiker gedefinieerde functie in de modelconfiguratie:

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

Standaard probeert EF Core de CLR-functie toe te wijzen aan een door de gebruiker gedefinieerde functie met dezelfde naam. Als de namen verschillen, kunnen we HasName de juiste naam opgeven voor de door de gebruiker gedefinieerde functie waaraan we willen toewijzen.

Voer nu de volgende query uit:

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

Hiermee wordt deze SQL geproduceerd:

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

Een methode toewijzen aan een aangepaste SQL

EF Core biedt ook door de gebruiker gedefinieerde functies die worden geconverteerd naar een specifieke SQL. De SQL-expressie wordt geleverd met behulp van HasTranslation de methode tijdens de door de gebruiker gedefinieerde functieconfiguratie.

In het onderstaande voorbeeld maken we een functie waarmee het percentageverschil tussen twee gehele getallen wordt berekend.

De CLR-methode is als volgt:

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

De functiedefinitie is als volgt:

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

Zodra we de functie hebben gedefinieerd, kan deze worden gebruikt in de query. In plaats van de databasefunctie aan te roepen, vertaalt EF Core de methodebody direct in SQL op basis van de SQL-expressieboom die is geconstrueerd vanuit de HasTranslation. De volgende LINQ-query:

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

Produceert de volgende SQL:

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

Nullbaarheid van gebruikersgedefinieerde functie configureren op basis van zijn argumenten

Als de door de gebruiker gedefinieerde functie alleen kan worden geretourneerd null wanneer een of meer van de argumenten zijn null, biedt EFCore een manier om dat op te geven, wat resulteert in meer presterende SQL. U kunt dit doen door een PropagatesNullability() aanroep toe te voegen aan de configuratie van het relevante functieparametersmodel.

Om dit te illustreren, definieert u de gebruikersfunctie ConcatStrings:

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

en twee CLR-methoden die ermee overeenkomen:

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

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

De modelconfiguratie (binnen OnModelCreating de methode) is als volgt:

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

De eerste functie wordt op de standaard manier geconfigureerd. De tweede functie is geconfigureerd om te profiteren van de optimalisatie voor het doorgeven van null-waarden, zodat u meer informatie krijgt over hoe de functie zich gedraagt rond null-parameters.

Bij het uitvoeren van de volgende queries:

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

We krijgen deze 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)

De tweede query hoeft de functie zelf niet opnieuw te evalueren om de null-waarde ervan te testen.

Opmerking

Deze optimalisatie mag uitsluitend worden gebruikt wanneer de functie alleen null kan teruggeven als de parameters null zijn.

Een opvraagbare functie toewijzen aan een tabelwaarde-functie

EF Core biedt ook ondersteuning voor toewijzing aan een tabelwaardefunctie met behulp van een door de gebruiker gedefinieerde CLR-methode die een IQueryable entiteitstype retourneert, zodat EF Core TVF's kan toewijzen aan parameters. Het proces is vergelijkbaar met het toewijzen van een scalaire door de gebruiker gedefinieerde functie aan een SQL-functie: we hebben een TVF in de database nodig, een CLR-functie die wordt gebruikt in de LINQ-query's en een toewijzing tussen de twee.

Als voorbeeld gebruiken we een tabelwaardefunctie die alle berichten retourneert met ten minste één opmerking die voldoet aan een bepaalde drempelwaarde voor Vind ik leuk:

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
)

De CLR methodesignatuur is als volgt:

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

Aanbeveling

De aanroep FromExpression in de functiebody van de CLR maakt het mogelijk de functie te gebruiken in plaats van een gewone DbSet.

Hieronder ziet u de mapping:

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

Opmerking

Een doorzoekbare functie moet worden toegewezen aan een tabelwaardefunctie en kan geen gebruik maken van HasTranslation.

Wanneer de functie is toegewezen, voert u de volgende query uit:

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

Produceert:

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