加快实体框架调用

时间:2020-11-09 10:44:02

标签: c# entity-framework linq optimization query-optimization

我在API中有一个用于数据调用的存储库,目前,经测试,该存储库可以按预期正常运行,尽管使用较大的数据集时,存储速度似乎成倍下降。尽管我似乎找不到太多与包含有关的信息,并使用Entity Framework加快了数据查询的速度,但我一直在研究尝试更多方法来提高性能。

以下是我的较慢通话之一的示例。我想问一下,进行这种查询的更好的方法是什么。病态还包括数据库的关系图,以供参考,以供其澄清。有什么想法吗?

        public async Task<List<Logic.Objects.Client>> GetAllClients()
        {
            List<Logic.Objects.Client> clientList = (await santaContext.Client
                /* Surveys and responses */
                .Include(c => c.SurveyResponse)
                    .ThenInclude(sr => sr.SurveyQuestion.SurveyQuestionOptionXref)
                        .ThenInclude(sqox => sqox.SurveyOption)
                .Include(c => c.SurveyResponse)
                    .ThenInclude(sr => sr.SurveyQuestion.SurveyQuestionXref)

                .Include(c => c.SurveyResponse)
                    .ThenInclude(sr => sr.Survey.EventType)

                /* Sender/Assignment info and Tags */
                .Include(c => c.ClientRelationXrefRecipientClient)
                    .ThenInclude(crxsc => crxsc.SenderClient.ClientTagXref)
                        .ThenInclude(txr => txr.Tag)
                .Include(c => c.ClientRelationXrefSenderClient)
                    .ThenInclude(crxrc => crxrc.RecipientClient.ClientTagXref)
                        .ThenInclude(txr => txr.Tag)

                /* Relationship event */
                .Include(c => c.ClientRelationXrefSenderClient)
                    .ThenInclude(crxsc => crxsc.EventType)
                .Include(c => c.ClientRelationXrefRecipientClient)
                    .ThenInclude(crxrc => crxrc.EventType)

                /* Chat messages */
                .Include(c => c.ClientRelationXrefRecipientClient)
                    .ThenInclude(cm => cm.ChatMessage)
                .Include(c => c.ClientRelationXrefSenderClient)
                    .ThenInclude(cm => cm.ChatMessage)

                /* Assignment statuses */
                .Include(c => c.ClientRelationXrefRecipientClient)
                    .ThenInclude(stat => stat.AssignmentStatus)
                .Include(c => c.ClientRelationXrefSenderClient)
                    .ThenInclude(stat => stat.AssignmentStatus)

                /* Tags */
                .Include(c => c.ClientTagXref)
                    .ThenInclude(t => t.Tag)

                /* Notes */
                .Include(c => c.Note)

                /* Client approval status */
                .Include(c => c.ClientStatus)
                .AsNoTracking()
                .ToListAsync())
                .Select(Mapper.MapClient).ToList();

            return clientList;
        }

ERD

2 个答案:

答案 0 :(得分:1)

查询速度慢的原因之一是因为您使用Include来获取子集合。

如果客户端[10]具有1000个ChatMessages,则每个ChatMessage都将具有值为10的外键ClientId。如果获取“ Client [10]及其ChatMessages”,则此值将发送10次超过1000次。

此外,如果您使用“包含”来获取子项目并获取完整的对象,则所获取的项目也会存储在DbContext.ChangeTracker中。 从DbContext获取完整的对象之前,将检查该项是否已在ChangeTracker中,如果没有,则从数据库管理系统中查询数据并将其放入ChangeTracker中。

在使用实体框架时,请始终使用“选择”,然后仅选择您实际计划使用的属性。仅选择完整的对象,如果打算更新获取的数据,则仅使用“包含”。

某些情况下,一定不要使用“包含”来节省您的键入时间。

此外,明智的做法是不要在返回中使用原始表行类,而要使用匿名类型,或者如果需要从方法返回,请使用类似于原始表的特殊存储库类。

中间存储库类的优点使您可以自由隐藏数据库布局。您甚至隐藏自己正在使用数据库。它可以是XML文件,JSON或用于字典的单元测试。因为您将表布局与方法用户获得的数据断开连接,所以可以将返回的结构与数据库表不同。这样可以对不同的用户类型进行不同的查询。将来可以更改数据库布局,而不必更改存储库查询。

例如,如果您决定添加属性IsObsolete,则可以选择给普通用户查询返回Clients而没有该属性的查询。如果普通用户查询“ ...的所有客户端”,则他将获得“ ...的所有非过期客户端”。如果发生错误,超级用户可以声明客户已过时,也可以撤消该请求。如果超级用户查询客户端,则将获得另一种类型的存储库客户端,即具有额外属性IsObsolete的存储库客户端。管理员用户可以在几个月内定期删除过时的表中的所有行,而SuperUsers则不能。

无需更改数据库布局即可更改表的示例:

学生有地址。当前,您的DbContext Student类类似于Repository Student类:

class Student
{
    public int Id {get; set;}
    public string Name {get; set;}
    public string Street {get; set;}
    public string City {get; set;}
    ...
}

之后,您发现街道/城市经常包含输入错误。因此,您购买了一张CDROM,该CDROM包含该表中该国家/地区的所有地址,并决定在地址和学生之间建立一对多的关系:每个地址有零个或多个学生,每个学生居住在一个地址上,即外键AddressId引用。

class Student
{
    public int Id {get; set;}
    public string Name {get; set;}

    public int AddressId {get; set;}
}

由于您知道自己的地址表正确无误,因此可以确定每个学生都住在一个现有的地址中,而不会出现任何键入错误。如果政府决定重命名街道,则不必更新居住在该街道上的学生,只需更新更改的地址即可。

如果学生搬家,您所要做的就是指向新地址,并且所有地址行均已正确更改。

由于您将DbContext Student与Repository Student分开,因此来自Repository的仅查询用户在查询Student时不会注意到任何更改。只有学生更新者会看到界面更改。

回到您的问题

因此,如果执行以下操作,查询将大大加快:

List<Logic.Objects.Client> clientList = (await santaContext.Clients
.Select(client => new Repository.Client()
{
    // only Select the Client properties that you plan to use
    Id = client.Id,
    Name = client.Name,
    ...

    SurveyResponses = client.SurveyReponses.Select(surveryResponse => new Repository.SurveyResponse
    {
         // again: only the properties that you plan to use:
         Id = surveyResponse.Id,
         ...

         // not needed, you already got the value:
         // ClientId = surveyResponse.ClientId,

         Questions = surveyResponse.SurveyQuestions.Select(...).ToList(),
         Responses = surveyReponses...
    })
    .ToList(),

    // similar for Sender / Assignment / Tag / etc
})

以下几项性能改进:

  • 由于使用了Select,因此dbContext不会检查数据是否已在ChangeTracker中,并且在获取数据后也不会将其添加到ChangeTracker中。
  • 因为只选择了计划使用的属性,所以不会获取无关的数据,也不会获取已经知道该值的数据,例如外键的值。
  • 这种改进不会加快查询速度,但会加快将来的更改:由于您将数据库布局与存储库类分开,因此您可以在数据库中启用将来的更改,而无需用户注意到这些更改。

答案 1 :(得分:0)

我不认为您在所有情况下都需要所有这些相关实体。因此,请不要这样做。根据具体情况向存储库添加更多方法。

无论如何,EF Core 5都具有AsSplitQuery扩展名,这可能会加快噩梦的速度。

Mapper.MapClient-尝试使用表达式(在ToList之前选择)执行此操作,您的查询将仅获取所需的数据。 如果通过表达式映射对象,则甚至根本不需要包含。

理想情况下,您的查询应该看起来像(我更喜欢在没有Automapper的情况下手动进行查询)

await santaContext.Client.Select(c => 
  new Logic.Objects.Client
  {
     ClientId = c.Id,
     SurveyResponse = c.SurveyResponse,
     Questions = c.SurveyResponse.SurveyQuestion.Select(sc => new {...}).ToList()
     ...
  }).ToListAsync();

可以通过扩展方法完成

public static IQueryable<Logic.Objects.Client> ToDto(this IQueryable<Client> query, bool includeQuestions)
{
    if (includeQuestions)
       return query.Select(/* projection with questions */)
    return query.Select(/* projection without questions */)
}
....

await santaContext.Client.ToDto(true).ToListAsync();

稍后,您会发现需要重用方法,但这是另一个复杂的问题,需要即时修改表达式树。