我在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;
}
答案 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
})
以下几项性能改进:
答案 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();
稍后,您会发现需要重用方法,但这是另一个复杂的问题,需要即时修改表达式树。