.net 减少从数据库中获取数据时插入的EF往返次数

cclgggtu  于 2023-03-24  发布在  .NET
关注(0)|答案(2)|浏览(116)

TL;DR:有没有什么方法可以减少EF应用程序到数据库的往返次数,因为EF应用程序在执行插入之前需要查找一些内容。

场景:一个.NET 7 EF WebAPI应用程序运行在SQL数据库上。作为一个(非常)最小的复制应用程序,请考虑这个简化的约会应用程序:

  • 三种实体类型:会议、人员、地点
  • 关系:
  • 地点可以主持多个会议
  • 人们可以参加多个会议
  • 会议有多人参加
  • 因此,数据库中的四个表:Meeting、Person、Place和Meeting-People联接表
  • 所有表都有ID主键(以及预期的外键/关系)。
  • 所有三种实体类型都具有string Name属性,这些属性具有唯一索引

所有内容都是使用dotnet ef工具以标准方式搭建的。
要执行会议插入,我需要一个JSON有效负载,如下所示:

{
    "Name": "MyMeetingName",
    "PlaceName": "ThePlaceName",
    "StartTime": "2023-03-21T21:52:41.510Z",
    "OtherParameters": "Stuff",
    "ParticipantNames": [
        "Ferd Berfle",
        "John Q Public"
    ]
}

在服务器上,执行插入操作的代码如下所示(有效负载反序列化为meeting

var place = await _context.Places.Where(place => place.Name == meeting.PlaceName).FirstOrDefaultAsync();
var participants = await _context.People.Where(p => meeting.ParticipantNames.Contains(p.Name)).ToListAsync();
var newMeeting = new Meeting
{
    Name = meeting.Name,
    Place = place,
    StartTime = meeting.StartTime,
    Other = meeting.OtherParameters,
};
// add in the participants
foreach (var person in participants){
    newMeeting.Participants.Add(person);
}
_context.Meetings.Add(newMeeting);
await _context.SaveChangesAsync();
// cleanup and return
  • (这是从一个类似的系统用不同的名字转录,对不起,如果我搞砸了)*

这些都可以但是...
它对数据库进行了三次往返(一次是获取Place,一次是获取Participants,第三次是在await SaveChangesAsync上)。
我的第一个尝试是看看我是否可以在一个往返中完成两个提取。我通过捕获与两个提取相关的任务(异步,没有await),然后执行await WhenAll()并阅读Task.Result s来尝试。我可以在调试输出中看到两个单独的命令正在执行(每个都有一个持续时间),所以这不起作用。
看起来这(不是公认的答案,另一个)可能是一种允许在一次往返中完成两次提取的方法:Optimize Entity Framework 6 and reducing round trips
但是我真正希望的是有一种方法来表明我想在数据库服务器上进行两次获取,并以某种方式将这些查询与插入混合在一起。我在IQueryable中玩得很舒服,当我在做GET时,我会从单独的可查询中组合查询,但我不知道如何将可查询的内容混合到最终会产生SaveChanges的内容中。
是我卡住了还是我错过了什么

更新:(2023-03-22 14:00 UTC,09:00 CDT)

这真的只是一个最小化的例子,而不是我试图解决的真实的问题(事实上,它是一个简化的问题例子的更小化版本)。我精通SQL脚本,所以我知道事务和存储过程(以及如何从EF调用存储过程)。我也知道使用ID而不是索引字符串的好处。
我的问题实际上是关于将IQueryable s与SaveChangesAsync混合的。看来我在这方面的直觉是正确的(这不是一条真正的道路)。@StevePy描述DbContext.Attach的答案很有启发性-谢谢。

qeeaahzv

qeeaahzv1#

我可以推荐的优化方法是确保您使用索引值。鉴于此数据的创建涉及现有地点和现有人员的关联,那么可以通过相应的PK而不是Name来完成。名称对于阅读JSON可能很好,但它们更大,如果没有索引,查询成本更高。它们的索引成本也更高。并且容易出现诸如唯一约束的问题,即,在任何给定位置中存在远远多于一个“John Smith”的可能性。
给定如下结构:

{
    "Name": "MyMeetingName",
    "PlaceId": 16,
    "StartTime": "2023-03-21T21:52:41.510Z",
    "OtherParameters": "Stuff",
    "ParticipantIds": [
        25,
        19
    ]
}

您可以查询数据:

var place = await _context.Places.Single(place => place.Id == meeting.PlaceId);
var participants = await _context.People.Where(p => meeting.ParticipantIds.Contains(p.Id)).ToListAsync();
// consider checking that you have the correct number of participants:
// Assert.That(meeting.ParticipantIds.Count, Equals(participants.Count));

var newMeeting = new Meeting
{
    Name = meeting.Name,
    Place = place,
    StartTime = meeting.StartTime,
    Other = meeting.OtherParameters,
};
// add in the participants
foreach (var person in participants){
    newMeeting.Participants.Add(person);
}
_context.Meetings.Add(newMeeting);
await _context.SaveChangesAsync();

或者,如果您想追求性能,并且可以信任ID(内部系统,篡改风险低,并处理任何DB异常)

var place = await _context.Places.Local.SingleOrDefault(place => place.Id == meeting.PlaceId);
if (place == null)
{
    place = new Place{ Id = meeting.PlaceId };
    _context.Attach(place);
}

List<Person> participants = new List<Person>();
foreach(var personId in meeting.ParticipantIds)
{
    var person = _context.People.Local.SingleOrDefault(p => p.Id == personId);
    if(person == null)
    {
       person = new Person { Id = personId };
       _context.Attach(person);
    }
    participants.Add(person);
}

var newMeeting = new Meeting
{
    Name = meeting.Name,
    Place = place,
    StartTime = meeting.StartTime,
    Other = meeting.OtherParameters,
};
// add in the participants
foreach (var person in participants){
    newMeeting.Participants.Add(person);
}
_context.Meetings.Add(newMeeting);
await _context.SaveChangesAsync();

这在上下文本地跟踪缓存中查找可能被跟踪的任何请求地点或人员,否则,它会创建一个最小的占位符并将其附加。这不涉及到DB的往返,并且将使EF乐于插入新的会议,而不管是否跟踪了地点/人员。但是这种方法的警告是,如果任何其他代码碰巧请求了关于这些被存根化的实体之一的细节,它们会收到一个对存根的引用。(一个只设置了ID的实体)为了避免这种风险,你可以跟踪存根实体,然后在SaveChanges调用之后分离它们。

3phpmpom

3phpmpom2#

我认为进一步优化到1个数据库查询的唯一方法是在数据库中注册一个存储过程,然后使用实体框架调用它,或者将整个命令编写为动态SQL查询。

相关问题