您需要了解的 5 个 EF 核心功能

作者:微信公众号:【架构师老卢】
8-17 18:14
13

概述:深入研究 EF Core 的每个角落和缝隙可能不是您的首要任务。但事情是这样的:EF Core 功能强大,了解一些关键功能可以为你节省大量时间和挫败感。因此,我不会用阳光下的每一个 EF Core 功能来轰炸你。取而代之的是,我挑选了五个你真正需要知道的基本要素。我们将通过:查询拆分 — 数据库的新好朋友批量更新和删除 — 类固醇的效率原始 SQL 查询 — 当您需要流氓时查询过滤器 — 保持整洁急切加载 — 因为懒惰不是那么好让我们开始吧!查询拆分查询拆分是你很少需要的 EF Core 功能之一。直到有一天,你做到了。在急需加载多个集合的情况下,查询拆分非常有用。它帮助我们避免笛卡尔爆炸问题

深入研究 EF Core 的每个角落和缝隙可能不是您的首要任务。

但事情是这样的:EF Core 功能强大,了解一些关键功能可以为你节省大量时间和挫败感。

因此,我不会用阳光下的每一个 EF Core 功能来轰炸你。

取而代之的是,我挑选了五个你真正需要知道的基本要素。

我们将通过:

  • 查询拆分 — 数据库的新好朋友
  • 批量更新和删除 — 类固醇的效率
  • 原始 SQL 查询 — 当您需要流氓时
  • 查询过滤器 — 保持整洁
  • 急切加载 — 因为懒惰不是那么好

让我们开始吧!

查询拆分

查询拆分是你很少需要的 EF Core 功能之一。直到有一天,你做到了。在急需加载多个集合的情况下,查询拆分非常有用。它帮助我们避免笛卡尔爆炸问题。

假设我们想要检索一个部门及其所有团队、员工和他们各自的任务。我们可以编写一个像这样的查询:

List<Department> departments =  
    context.Departments  
        .Include(d => d.Teams)  
        .ThenInclude(t => t.Employees)  
        .ThenInclude(e => e.Tasks)  
        .ToList();

这转换为具有多个 JOIN 的单个 SQL 查询。假设一个部门有很多团队,每个团队都有很多员工。这可能导致笛卡尔爆炸。在这种情况下,数据库将返回许多行,从而显著影响性能。

以下是我们可以通过查询拆分避免这些性能问题的方法:

List<Department> departments =  
    context.Departments  
        .Include(d => d.Teams)  
        .ThenInclude(t => t.Employees)  
        .ThenInclude(e => e.Tasks)  
        .AsSplitQuery()  
        .ToList();

使用 ,EF Core 将为每个集合导航执行额外的 SQL 查询。AsSplitQuery

但是,请注意不要过度使用查询拆分。当我_测量_到它们始终表现得更好时,我会使用拆分查询。

拆分查询与数据库的往返次数更多,如果数据库延迟较高,则往返次数可能会较慢。此外,还不能保证多个 SQL 查询之间的一致性。

批量更新和删除

EF Core 7 添加了两个用于执行批量更新和删除的新 API,以及 .它们允许您在一次往返到数据库的行程中有效地更新大量行。ExecuteUpdateExecuteDelete

这是一个实际的例子。

公司已决定为“销售”部门的所有员工加薪 5%。如果不进行批量更新,我们可能会遍历每个员工并单独更新他们的工资:

var salesEmployees = context.Employees  
    .Where(e => e.Department == "Sales")  
    .ToList();  
  
foreach (var employee in salesEmployees)  
{  
    employee.Salary *= 1.05m;  
}  
  
context.SaveChanges();

这种方法涉及多个数据库往返,这可能效率低下,尤其是对于大型数据集。

我们可以使用以下方法在一次往返中实现相同的目标:ExecuteUpdate

context.Employees  
    .Where(e => e.Department == "Sales")  
    .ExecuteUpdate(s => s.SetProperty(e => e.Salary, e => e.Salary * 1.05m));

这执行单个 SQL 语句,直接修改数据库中的工资,而无需将实体加载到内存中,从而提高了我们的性能。UPDATE

这是另一个例子。假设一个电子商务平台想要删除所有超过一年的购物车。

以下是我们如何做到这一点:ExecuteDelete

context.Carts  
    .Where(o => o.CreatedOn < DateTime.Now.AddYears(-1))  
    .ExecuteDelete();

这将产生一个 SQL 语句,直接从数据库中删除旧的购物车。DELETE

但是,批量更新会绕过 EF 更改跟踪器。这可能会有问题,我在本文中写了关于批量更新的注意事项

原始 SQL 查询

EF Core 8 添加了一项新功能,允许我们使用原始 SQL 查询未映射的类型。

假设我们想要从数据库视图、存储过程或不直接对应于任何实体类的表中检索数据。

例如,我们想要检索每个产品的销售摘要。使用 EF Core 8,我们可以定义一个表示结果集结构的简单类,并直接查询它:ProductSummary

public class ProductSummary  
{  
    public int ProductId { get; set; }  
    public string ProductName { get; set; }  
    public decimal TotalSales { get; set; }  
}  
  
var productSummaries = await context.Database  
    .SqlQuery<ProductSummary>(  
        @$"""  
 SELECT p.ProductId, p.ProductName, SUM(oi.Quantity * oi.UnitPrice) AS TotalSales  
 FROM Products p  
 JOIN OrderItems oi ON p.ProductId = oi.ProductId  
 WHERE p.CategoryId = {categoryId}  
 GROUP BY p.ProductId, p.ProductName  
 """)  
    .ToListAsync();

该方法返回一个 ,它允许您使用 LINQ 编写原始 SQL 查询。这将原始 SQL 的强大功能与 LINQ 的表现力相结合。SqlQueryIQueryable

请记住使用参数化查询来防止 SQL 注入漏洞。该方法接受 ,这意味着您可以安全地使用插值字符串。每个参数都转换为一个 SQL 参数。SqlQueryFormattableString

您可以在本文中了解有关原始 SQL 查询的更多信息。

查询筛选器

查询筛选器类似于可重用的子句,可以应用于实体。每当检索相应类型的实体时,这些筛选器将自动添加到 LINQ 查询中。这样可以避免在应用程序中的多个位置重复编写相同的过滤逻辑。WHERE

查询筛选器通常用于以下方案:

  • 软删除:过滤掉标记为已删除的记录。
  • 多租户:根据当前租户筛选数据。
  • 行级别安全性:根据用户角色或权限限制对特定记录的访问。

在多租户应用程序中,您经常需要根据当前租户筛选数据。查询过滤器使我们能够轻松处理此要求:

public class Product  
{  
    public int Id { get; set; }  
    public string Name { get; set; }  
    // Associate products with tenants  
    public int TenantId { get; set; }  
}  
  
protected override void OnModelCreating(ModelBuilder modelBuilder)  
{  
    // The current TenantId is set based on the current request/context  
    modelBuilder.Entity<Product>().HasQueryFilter(p => p.TenantId == \_currentTenantId);  
}  
  
// Now, queries automatically filter based on the tenant:  
var productsForCurrentTenant = context.Products.ToList();

在同一实体上配置多个查询筛选器将仅应用最后一个查询筛选器。您可以使用 (AND) 和 (OR) 运算符组合多个查询筛选器。&&||

如果需要,您可以使用它来绕过特定查询中的筛选器。IgnoreQueryFilters

急切加载

预先加载是 EF Core 中的一项功能,可用于在单个数据库查询中加载相关实体以及主实体。通过在单个查询中获取所有必要的数据,您可以提高应用程序性能。在处理复杂的对象图时,或者当延迟加载会导致许多小的、低效的查询时,尤其如此。

下面是一个示例用例。我们想加载 an 并急切地使用该方法加载 a,因为我们想同时修改两个实体。VerifyEmailEmailVerificationTokenUserInclude

internal sealed class VerifyEmail(AppDbContext context)  
{  
    public async Task<bool> Handle(Guid tokenId)  
    {  
        EmailVerificationToken? token = await context.EmailVerificationTokens  
            .Include(e => e.User)  
            .FirstOrDefaultAsync(e => e.Id == tokenId);  
  
        if (token is null || token.ExpiresOnUtc < DateTime.UtcNow || token.User.EmailVerified)  
        {  
            return false;  
        }  
  
        token.User.EmailVerified = true;  
  
        context.EmailVerificationTokens.Remove(token);  
  
        await context.SaveChangesAsync();  
  
        return true;  
    }  
}

EF Core 将生成一个 SQL 查询,该查询联接 and 表,一次性检索所有必要的数据。EmailVerificationTokenUser

急切加载(以及我们之前提到的查询拆分)并不是灵丹妙药。如果只需要来自相关实体的特定属性,请考虑使用投影,以避免提取不必要的数据。

所以,你有它!坦率地说,你不能_不知道_的五个 EF Core 功能。请记住,掌握 EF Core 需要时间,但这些功能为构建提供了坚实的基础。

另一条建议是深入了解数据库的工作原理。掌握 SQL 还可以让您从 EF Core 中获得最大价值。

虽然我们重点介绍了五个关键功能,但还有许多其他 EF Core 功能值得探索:

EF Core 在不断发展,因此请密切关注最新的更新和发布,以保持领先地位。

阅读排行