Entity Framework Core 中的加载模式(延迟加载、显式加载和预先加载)

作者:微信公众号:【架构师老卢】
9-13 20:22
121

使用 Entity Framework Core 查询数据时,高效加载数据非常重要。数据库驱动的应用程序可以对数据库进行大量调用。如果数据库调用的编码不佳,应用程序的响应时间将受到显著影响。

简单来说,查询转到数据库,然后数据库必须执行此查询。然后,结果将返回到应用程序。查询越多,取回信息所需的时间就越长。每次访问数据库服务器都会消耗资源。

在处理相关数据时,选择正确的加载策略将对应用程序的速度产生显著影响。

加载数据的三种主要方法是 Lazy Loading、Explicit Loading 和 Eager Loading。让我们看看我们应该如何以及何时使用每种策略。

什么是 N+1 问题?

如前所述,我们希望限制访问数据库的次数。使用 Entity Framework Core 时,很容易在不知不觉中向数据库发出“隐藏”请求。

当对数据库的初始查询返回记录列表,然后对这些记录进行后续查询时,将发生隐藏请求。另一种方法是在单个更大的查询中获取我们需要的所有数据。

延迟加载和 N+1 问题

延迟加载也称为“延迟执行”,它减少了从数据库中检索的数据量。当我们使用 Lazy Loading 加载实体时,我们会忽略任何相关实体。仅当我们请求子元素时,才会收集来自子元素的数据,而不是预先提供子元素。

当您确信不需要任何相关数据时,延迟加载是理想的选择。

假设一个实体与实体具有一对多的关系。BreweryBeer

// Parent  
  
public class Brewery  
{  
    public Guid Id { get; set; }  
    public string Name { get; set; }  
    public string City { get; set; }  
    public string State { get; set; }  
    public string WebsiteUrl { get; set; }  
    // Navigation property  
    public ICollection\<Beer> Beers { get; set; }  
}  
  
// Child  
  
public class Beer  
{  
  public Guid Id { get; set; }  
  public string Name { get; set; }  
  public string Style { get; set; }  
    
  // Foreign Key  
  public Guid BreweryId { get; set; }  
}

我们的应用程序需要获取数据库中的所有条目。我们还需要获取与 .BreweryBeerBrewery

在下面的代码中,我们通过访问 : 来请求所有 the 。这是我们第一次调用数据库。breweriesDbContext_context.Breweries().ToList()

为了获得与给定 关联的啤酒,我们使用 For 循环遍历表中的每个条目:BreweryBeers

public class BreweryRepository : IBreweryRepository
{
  private readonly DataContext _context;

  public BreweryRepository(DataContext context)
  {
    _context = context;
  }

  public IEnumerable<Brewery> GetAllBreweriesWithTheirBeers()
  {
      List<Brewery> breweries = context.Breweries.ToList();

      /* 
        In each loop we cross-reference 
        Beer.BreweryId with the Id of our indexed Brewery.

        We then return all the matching Beer values as a list.
      */
      for(var i = 0; i < totalBreweries; i++)
      {
        breweries[i].Beers = _context.Beers.Where(
         b => b.BreweryId == breweries[i].Id)
         .ToList();
      }

      return _breweries;
  }
}

好消息是我们已经获得了所需的数据。我们有自己的条目列表,每个条目都有它们的值列表。但是,在此过程中,我们可能会对数据库进行数百次调用。想象一下,如果我们的数据库中有 2000+ 家啤酒厂,每家啤酒厂都有 50+ 种啤酒。查询量非常复杂。BreweryBeerBreweries

延迟加载和代理

有一种方法可以执行 Lazy Loading 并且仍然可以访问相关数据。 该包在运行时生成子实体的 “代理”。代理实体允许您查看相关数据,而无需从数据库中显式加载数据。Microsoft.EntityFrameworkCore.Proxies

要将代理引入您的应用程序,您需要安装以下包:

dotnet add package Microsoft.EntityFrameworkCore.Proxies

然后,我们需要在注册 :UseLazyLoadingProxies()DbContext

builder.Services.AddDbContext<DataContext>(
 options => options
  .UseLazyLoadingProxies() // <--- here
  .UseSqlite(
  builder.Configuration.GetConnectionString("DefaultConnection")));

然后将关键字添加到所有导航字段。这允许代理覆盖它正在生成的实体的值:virtual

public class Brewery  
{  
  public Guid Id { get; set; }  
  ...  
    
  public virtual ICollection\<Beer> Beers { get; set; }  
}  
  
  
public class Beer  
{  
  public Guid Id { get; set; }  
  ...  
    
  public virtual Brewery Brewery { get; set; }  
}

现在,当我们执行如下例所示的操作时,我们将收到一个值列表,并且每个字段都可以访问:BreweryBeers

public class BreweryRepository : IBreweryRepository
{
  private readonly DataContext _context;

  public BreweryRepository(DataContext context)
  {
    _context = context;
  }

  public IEnumerable<Brewery> GetAllBreweriesWithTheirBeers() {}

  public IEnumerable<Brewery> GetAllBreweries()
  {
    IEnumerable<Brewery> breweries = _context.Breweries.ToListAsync();

    return breweries;
  }
  
  ...

}

预先加载

尽管这是一项昂贵的操作,但 Eager Loading 将在单个查询中返回您的主记录及其相关数据。

预先加载的一个好处是代码更清晰、更清晰。在前面的示例中,我们依赖于 For 循环并比较值。Entity Framework Core 为我们提供了两种扩展方法,可以在更少的行中实现相同的结果:和 :IdInclude()ThenInclude()

public class BreweryRepository : IBreweryRepository
{
  private readonly DataContext _context;

  public BreweryRepository(DataContext context)
  {
    _context = context;
  }

  public IEnumerable<Brewery> GetAllBreweries() {}

  public IEnumerable<Brewery> GetAllBreweriesWithTheirBeers()
  {
    IEnumerable<Brewery> breweries = _context.Breweries
        .Include(b => b.Beers)
        .ToList(); 

    return breweries;
  }

  
  ...

}

如果与实体有另一个一对多的关系,我们甚至可以链式调用:BreweryInclude()

public IEnumerable<Brewery> GetAllBreweriesWithTheirBeersAndSpirits()
{
  IEnumerable<Brewery> breweries = _context.Breweries
      .Include(b => b.Beers)
      .Include(b => b.Spirits)
      .ToList(); 

  return breweries;
}

ThenInclude()工作方式相同,但有助于加载多个级别的相关实体。例如,如果实体与名为 的新实体具有一对多关系,我们还可以在单个调用中恢复该数据:BeerIngredient

public class BreweryRepository : IBreweryRepository
{
  private readonly DataContext _context;

  public BreweryRepository(DataContext context)
  {
    _context = context;
  }
  
  public IEnumerable<Brewery> GetAllBreweries() {}

  public IEnumerable<Brewery> GetAllBreweriesWithTheirBeers() {}

  public IEnumerable<Brewery> GetAllBreweriesWithTheirBeersAndSpirits() {}

  public IEnumerable<Brewery> GetAllBreweriesWithBeersAndBeerIngredients() 
  {
    IEnumerable<Brewery> breweries = _context.Breweries
            .Include(b => b.Beers)
              .ThenInclude(b => b.Ingredients)
            .ToList(); 
    
        return breweries;
  }
}

显式加载

使用 Eager Loading,我们可以创建返回大量相关数据的复杂查询。尽管它的用例范围很窄,但 Explicit Loading 会检索特定记录的相关数据。在加载父记录后,我们必须显式加载导航属性。

为什么要使用 Explicit Loading?根据用例,在处理大型数据集时,依赖 Eager 或 Lazy Loading 可能是不负责任的。该应用程序可能仅在特定情况下需要相关数据。该方案可能归结为唯一的业务逻辑,甚至是用户授权。

我们可以使用我们的 Brewery 应用程序作为参考点来描述 Eager Loading 将有用的用例。

需要按字母顺序返回所有啤酒厂。前 5 家啤酒厂需要额外的数据,因为它们在页面顶部会有更明显的概况。因此,我们需要获取相关数据,但仅限于前 5 家啤酒厂:

public class BreweryRepository : IBreweryRepository
{
  private readonly DataContext _context;

  public BreweryRepository(DataContext context)
  {
    _context = context;
  }
  
  ...

  public IEnumerable<Brewery> GetAllBreweriesWithExplicitLoading() 
  {
    // Step 1: Retrieve all breweries and sort them alphabetically
    var breweries = context.Breweries
        .OrderBy(b => b.Name)
        .ToList();

    // Step 2: Take the first 5 breweries from the sorted list
    var top5Breweries = breweries.Take(5).ToList();

    // Step 3: Explicitly load the related Beer entities for these 5 breweries
    foreach (var brewery in top5Breweries)
    {
        context.Entry(brewery)
            .Collection(b => b.Beers)
            .Load();
    }    

    return breweries;
  }
}

在此代码中,我们使用 Lazy Loading 来检索数据库中的条目。在这个阶段,我们没有关于 的数据。然后,我们对数据进行排序和筛选。我们以一个 For 循环结束,该循环将相关数据追溯性地加载到集合中。区别在于,只有前 5 个条目将包含相关数据。由于是一个集合,我们使用该方法来访问 navigation 属性。如果导航属性是一对一关系,则我们使用 .BreweryBeersBrewerybreweriesBeersCollection()Reference()

使用 Entity Framework Core 时,很容易编写效率低下的数据库查询。如前所述,可以进行隐藏查询,这些查询可能会聚合并导致应用程序性能缓慢。随着您的应用程序扩展并引入更多数据,这将成为一个更大的问题。

简单地从现有方法中复制代码,或依赖 For 循环等密集型策略,会给您的数据库服务器带来压力。如果您必须查询数据库,则需要思考_原因_。您需要什么数据?目的是什么?不要投影您的请求不需要的数据。

Lazy Loading、Explicit Loading 和 Eager Loading 都有效,原因各不相同。更深入地考虑您使用哪种策略将对您的应用程序的效率产生重大影响。

相关留言评论
昵称:
邮箱:
阅读排行