ef core 6 新功能匯總(三)

ef core 6 新功能匯總(三)

在這篇文章中,我將重點居間 ef core 6 中 linq 查詢功能的增強。

最后更新 2022/6/2 下午10:02
liamwang 精致码农
预计阅读 14 分钟
分类
EF Core
标签
.NET C# EF Core ORM

在這篇文章中,我將重點居間 ef core 6 中 linq 查詢功能的增強。

這是 ef core 6 新功能匯總的第三篇文章:

EF Core 6.0 对 GroupBy 查询有更好的支持。

  • 翻译 GroupBy 后面的 FirstOrDefault
  • GroupBy 之后使用 ThenBy
  • 支持從一個組中選擇前 n 個結果
using var context = new ExampleContext();
var query = context.People
    .GroupBy(p => p.FirstName)
    .Select(g => g.OrderBy(e => e.FirstName)
        .ThenBy(e => e.LastName)
        .FirstOrDefault())
    .ToQueryString();
Console.WriteLine(query);

class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public int LastName { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Person> People { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6GroupBy");
}

翻譯後的 sql:

SELECT[t0].[Id], [t0].[FirstName], [t0].[LastName]
FROM (
SELECT[p].[FirstName]
   FROM [People] AS [p]
   GROUP BY [p].[FirstName]
) AS[t]
LEFT JOIN(
   SELECT[t1].[Id], [t1].[FirstName], [t1].[LastName]
   FROM (
       SELECT[p0].[Id], [p0].[FirstName], [p0].[LastName],
       ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName]
       ORDER BY [p0].[FirstName], [p0].[LastName]) AS[row]
       FROM[People] AS[p0]
   ) AS[t1]
   WHERE[t1].[row] <= 1
) AS[t0] ON[t].[FirstName] = [t0].[FirstName]

2 三四個參數的 string.concat 翻譯

以前 EF Core 翻译 string.Concat 时只有两个参数。EF Core 6.0 支持翻译三个和四个参数的 string.Concat

using var context = new ExampleContext();
string fullName = "SamuelLanghorneClemens";
var query = context.Blogs
    .Where(b => string.Concat(b.FirstName, b.MiddleName, b.LastName) == fullName)
    .ToQueryString();
Console.WriteLine(query);

class Blog
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6StringConcat");
}

翻譯後的 sql:

DECLARE @__fullName_0 nvarchar(4000) = N'SamuelLanghorneClemens';

SELECT[b].[Id], [b].[FirstName], [b].[LastName], [b].[MiddleName]
FROM[Blogs] AS[b]
WHERE(COALESCE([b].[FirstName], N'') + (COALESCE([b].[MiddleName], N'') +COALESCE([b].[LastName], N ''))) = @__fullName_0

3 ef.functions.freetext 支持二進位列

以前,尽管 SQL FreeText 函数支持二进制列,但你不能在二进制列上使用 EF.Functions.FreeText 方法。EF Core 6.0 解决了这个问题。

using var context = new ExampleContext();
var query = context.Posts
    .Where(p => EF.Functions.FreeText(EF.Property<string>(p, "Content"), "Searching text"))
    .ToQueryString();
Console.WriteLine(query);

class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public byte[] Content { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .Property(x => x.Content)
            .HasColumnType("varbinary(max)");
    }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6FlexibleTextSearch");
}

翻譯後的 sql:

SELECT "p"."Id", "p"."Name", "p"."PhoneNumber"
FROM "People" AS "p"
WHERE CAST("p"."PhoneNumber" AS TEXT) LIKE '%368%'

4 EF.Functions.Random

EF Core 6.0 引入了一个新的 EF.Functions.Random 方法。它映射了 SQL 函数 RAND()。已经实现了对 SQL Server、SQLite 和 Cosmos 的翻译。

using var context = new ExampleContext();
var query = context.Posts
    .Where(p => p.Rating == (int)(EF.Functions.Random() * 5.0) + 1)
    .ToQueryString();
Console.WriteLine(query);

class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int Rating { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6Random");
}

翻譯後的 sql:

SELECT[p].[Id], [p].[Rating], [p].[Title]
FROM[Posts] AS[p]
WHERE[p].[Rating] = (CAST((RAND() * 5.0E0) AS int) + 1)

5 改進了 sql server 的 isnullorwhitespace 的翻譯

以前,EF Core 将 string.IsNullOrWhiteSpace 翻译成在判断前将值进行 trim 操作。EF Core 6.0 已经不这么做了。

using var context = new ExampleContext();
var query = context.Entities
                    .Where(e => string.IsNullOrWhiteSpace(e.Property))
                    .ToQueryString();
Console.WriteLine(query);

class Entity
{
    public int Id { get; set; }
    public string Property { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Entity> Entities { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6IsNullOrWhiteSpace");
}

以前翻譯的 sql:

SELECT [e].[Id], [e].[Property]
FROM [Entities] AS[e]
WHERE [e].[Property] IS NULL OR (LTRIM(RTRIM([e].[Property])) = N'')

現在翻譯的 sql:

SELECT [e].[Id], [e].[Property]
FROM [Entities] AS[e]
WHERE [e].[Property] IS NULL OR ([e].[Property] = N'')

6 為內存資料庫定義查詢

在 EF Core 6.0 中,你可以通过一个新的方法 ToInMemoryQuery 来定义一个针对内存数据库的查询。这对于创建内存数据库的视图是最有用的。

using var context = new ExampleContext();
var blogEn = new Blog
{
    Title = "All about .NET",
    Language = "English",
    Posts = new List<Post>
        {
            new Post { Title = "Post one", Content = "Some content" },
            new Post { Title = "Post two", Content = "Some content" }
        }
};
var blogPl = new Blog
{
    Title = "Wszystko o .NET",
    Language = "Polish",
    Posts = new List<Post>
        {
            new Post { Title = "Pierwszy post", Content = "Treść" }
        }
};
context.Blogs.Add(blogEn);
context.Blogs.Add(blogPl);
await context.SaveChangesAsync();

var postsByLanguages = context.PostsByLanguages.ToList();
postsByLanguages
    .ForEach(p => Console.WriteLine($"{p.PostCount} posts in {p.Language}"));
// Output:
// 2 posts in English
// 1 posts in Polish

class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
}
class Blog
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Language { get; set; }
    public ICollection<Post> Posts { get; set; }
}
class PostsByLanguage
{
    public string Language { get; set; }
    public int PostCount { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<PostsByLanguage> PostsByLanguages { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<PostsByLanguage>()
            .HasNoKey()
            .ToInMemoryQuery(
                () => Blogs
                    .GroupBy(c => c.Language)
                    .Select(
                        g =>
                            new PostsByLanguage
                            {
                                Language = g.Key,
                                PostCount = g.Sum(b => b.Posts.Count)
                            }));
    }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseInMemoryDatabase("ToInMemoryQuery");
}

7 單參數的 substring 翻譯

以前,EF Core 只翻译有两个参数的 string.Substring 重载。EF Core 6.0 支持翻译单个参数的 string.Substring

using var context = new ExampleContext();
context.People.Add(new Person { Name = "John" });
context.People.Add(new Person { Name = "Bred" });
context.People.Add(new Person { Name = "Ron" });
await context.SaveChangesAsync();

var result = await context.People
    .Select(a => new { Name = a.Name.Substring(1) })
    .ToListAsync();
result.ForEach(p => Console.WriteLine(p.Name));
// Output:
// ohn
// red
// on

class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Person> People { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6Substring");
}

翻譯後的 sql:

SELECT SUBSTRING([p].[Name], 1 + 1, LEN([p].[Name])) AS [Name]
FROM [People] AS [p]

8 非導航集合的分割查詢

ef core 支持將一個 linq 查詢拆分成多個 sql 查詢。ef core 6.0 可以分割一個 linq 查詢,其中非導航集合屬性包含在查詢投影中。

using var context = new ExampleContext();
var blog = new Blog { Name = ".NET Blog"};
blog.Posts.Add(new Post { Title = "First .NET post" });
blog.Posts.Add(new Post { Title = "Second Java post" });
blog.Posts.Add(new Post { Title = "Third .NET post" });
context.Blogs.Add(blog);
await context.SaveChangesAsync();

var blogsWithDotnetPosts = await context.Blogs
    .Select(b => new
    {
        b,
        Posts = b.Posts.Where(p => p.Title.Contains(".NET")),
    })
    .AsSplitQuery()
    .ToListAsync();

class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Post> Posts { get; set; } = new List<Post>();
}
class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public Blog Blog { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6SplitQueries");
}

單個 sql 查詢(不用 assplitquery):

SELECT [b].[Id], [b].[Name], [t].[BlogId], [t].[Title]
FROM [Blogs] AS [b]
LEFT JOIN (
     SELECT [p].[Id], [p].[BlogId], [p].[Title]
     FROM [Posts] AS [p]
     WHERE [p].[Title] LIKE N'%.NET%'
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id]

多個 sql 查詢(使用了 assplitquery):

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
ORDER BY [b].[Id]

SELECT [t].[Id], [t].[BlogId], [t].[Title], [b].[Id]
FROM [Blogs] AS [b]
INNER JOIN (
     SELECT [p].[Id], [p].[BlogId], [p].[Title]
     FROM [Posts] AS [p]
     WHERE [p].[Title] LIKE N'%.NET%'
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id]

9 刪除最後的 order by 子句

當連接相關實體時,ef core 添加了 order by 子句,以確保給定實體的所有相關實體被分組。然而,最後一個子句並不是必須的,而且會對性能產生影響。ef core 6.0 刪除了它。

using var context = new ExampleContext();
var query = context.Blogs
    .Include(b => b.Posts.Where(p => p.Rating > 3))
    .ToQueryString();
Console.WriteLine(query);

class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Post> Posts { get; set; }
}
class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int Rating { get; set; }
    public Blog Blog { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6RemoveLastOrderByClause");
}

ef core 5.0 翻譯的 sql:

SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Rating], [t].[Title]
FROM [Blogs] AS [b]
LEFT JOIN (
    SELECT [p].[Id], [p].[BlogId], [p].[Rating], [p].[Title]
    FROM [Posts] AS [p]
    WHERE [p].[Rating] > 3
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id], [t].[Id]

ef core 6.0 翻譯的 sql:

SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Rating], [t].[Title]
FROM [Blogs] AS [b]
LEFT JOIN (
    SELECT [p].[Id], [p].[BlogId], [p].[Rating], [p].[Title]
    FROM [Posts] AS [p]
    WHERE [p].[Rating] > 3
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id]

10 用文件名和行號標記查詢

從 ef core 2.2 開始,你可以給你的查詢添加一個標籤,以達到更好的調試目的。ef core 6.0 更進一步,現在你可以用 linq 代碼的文件名和行號來標記查詢。

using var context = new ExampleContext();
var query = context.Blogs
    .TagWithCallSite()
    .OrderBy(b => b.CreationDate)
    .Take(10)
    .ToQueryString();
Console.WriteLine(query);

class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime CreationDate { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6TagWithCallSite");
}

翻譯後的 sql:

DECLARE @__p_0 int = 10;

--File: D:\EFCore6\TagWithCallSite\TagWithCallSite\Program.cs:6

SELECT TOP(@__p_0) [b].[Id], [b].[CreationDate], [b].[Name]
FROM[Blogs] AS[b]
ORDER BY[b].[CreationDate]

11 自有可選從屬關係處理

ef core 6.0 改變了一些對自有可選從屬關係的處理。當一個模型有自己的可選從屬關係時,ef core 會在你保存它時警告你所有缺失的屬性。

using var context = new ExampleContext();
var person = new Person
{
    FirstName = "Oleg",
    LastName = "Kyrylchuk",
    Address = new Address()
};
context.People.Add(person);
await context.SaveChangesAsync();

class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
}
class Address
{
    public string City { get; set; }
    public string Street { get; set; }
    public string PostalCode { get; set; }
}
class ExampleContext : DbContext
{
    public DbSet<Person> People { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<Person>()
            .OwnsOne(p => p.Address);
    }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options
        .EnableSensitiveDataLogging()
        .LogTo(Console.WriteLine)
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6OwnedDependentHandling");
}

警告日誌:

當你有嵌套自有可選從屬關係時,ef core 將不允許創建模型。

using var context = new ExampleContext();
var person = new Person
{
   FirstName = "Oleg",
   LastName = "Kyrylchuk",
   ContactInfo = new ContactInfo()
};
context.People.Add(person);
await context.SaveChangesAsync();

class Person
{
   public int Id { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public ContactInfo ContactInfo { get; set; }
}
class ContactInfo
{
   public string Phone { get; set; }
   public Address Address { get; set; }
}
class Address
{
   public string City { get; set; }
   public string Street { get; set; }
   public string PostalCode { get; set; }
}
class ExampleContext : DbContext
{
   public DbSet<Person> People { get; set; }
   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
       modelBuilder
           .Entity<Person>()
           .OwnsOne(p => p.ContactInfo)
           .OwnsOne(p => p.Address);
   }
   protected override void OnConfiguring(DbContextOptionsBuilder options)
       => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6OwnedDependentHandling");
}

創建模型後將會拋出異常。

這些變化迫使你避免這種情況。你可以通過以下方式解決這些問題。

  • 使從屬關係成為必需的。
  • 在從屬關係中至少有一個必需屬性。
  • 為可選的從屬關係創建自己的表,而不是與主體共享它們。

12 結尾

本文所有代码示例都可以在我的 GitHub 中找到:https://github.com/okyrylchuk/dotnet6_features/tree/main/EF%20Core%206#linq-query-enhancements

原文:https://blog.okyrylchuk.dev/linq-enhancements-in-entity-framework-core-6

作者:oleg kyrylchuk

翻譯:精緻碼農

Keep Exploring

延伸阅读

更多文章