在新实体中加载导航属性的最佳方式

本文关键字:属性 导航 最佳 方式 加载 新实体 实体 | 更新日期: 2023-09-27 18:14:26

我正在尝试使用EF添加新记录到SQL数据库。代码看起来像

    public void Add(QueueItem queueItem)
    {
        var entity = queueItem.ApiEntity;            

        var statistic = new Statistic
        {
            Ip = entity.Ip,
            Process = entity.ProcessId,
            ApiId = entity.ApiId,
            Result = entity.Result,
            Error = entity.Error,
            Source = entity.Source,
            DateStamp = DateTime.UtcNow,
            UserId = int.Parse(entity.ApiKey),
        };
        _statisticRepository.Add(statistic);
        unitOfWork.Commit();
    }    

在统计实体中有导航ApiUser属性,我想加载到新的统计实体中。我曾尝试使用下面的代码加载导航属性,但它会产生大量查询并降低性能。有什么建议如何以其他方式加载导航属性?

    public Statistic Add(Statistic statistic)
    {
        _context.Statistic.Include(p => p.Api).Load();
        _context.Statistic.Include(w => w.User).Load();
        _context.Statistic.Add(statistic);
        return statistic;
    }

你们中的一些人可能有问题,为什么我要加载导航属性,同时添加新的实体,这是因为我在移动实体到数据库之前在DbContext.SaveChanges()中执行一些计算。代码看起来像

public override int SaveChanges()
        {
            var addedStatistics = ChangeTracker.Entries<Statistic>().Where(e => e.State == EntityState.Added).ToList().Select(p => p.Entity).ToList();
            var userCreditsGroup = addedStatistics
                .Where(w => w.User != null)
                .GroupBy(g =>  g.User )
                .Select(s => new
                {
                    User = s.Key,
                    Count = s.Sum(p=>p.Api.CreditCost)
                })
                .ToList();      
//Skip code
}

所以上面的Linq不加载导航属性就不能工作,因为它使用了导航属性。

我还添加了统计实体的完整视图

  public class Statistic : Entity
    {
        public Statistic()
        {
            DateStamp = DateTime.UtcNow;
        }
        public int Id { get; set; }
        public string Process { get; set; }
        public bool Result { get; set; }
        [Required]
        public DateTime DateStamp { get; set; }
        [MaxLength(39)]
        public string Ip { get; set; }        
        [MaxLength(2083)]
        public string Source { get; set; }
        [MaxLength(250)]
        public string Error { get; set; }
        public int UserId { get; set; }
        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int ApiId { get; set; }
        [ForeignKey("ApiId")]
        public virtual Api Api { get; set; }
    }

在新实体中加载导航属性的最佳方式

正如您所说,针对上下文的以下操作将生成大型查询:

_context.Statistic.Include(p => p.Api).Load();
_context.Statistic.Include(w => w.User).Load();

将所有统计数据和相关api实体的对象图物化,然后将所有统计数据和相关用户物化到统计上下文

只需将其替换为如下的单个调用,就可以将其减少为单个往返:

_context.Statistic.Include(p => p.Api).Include(w => w.User).Load();

一旦这些被加载,实体框架更改跟踪器将修复新统计实体上的关系,从而一次性为所有新统计填充api和用户的导航属性。

根据一次创建的新统计数据的数量与数据库中现有统计数据的数量,我非常喜欢这种方法。

然而,查看SaveChanges方法,似乎每个新统计数据都发生一次关系修复。例如,每次添加新的统计数据时,您都要查询数据库中的所有统计数据以及相关的api和用户实体,以触发新统计数据的关系修复。

在这种情况下,我更倾向于这样做:

_context.Statistics.Add(statistic);
_context.Entry(statistic).Reference(s => s.Api).Load();
_context.Entry(statistic).Reference(s => s.User).Load();

这将只查询新统计的Api和User,而不是查询所有统计。也就是说,您将为每个新的统计数据生成2个单行数据库查询。

或者,如果你要在一个批处理中添加大量的统计数据,你可以通过预先加载所有用户和api实体来利用上下文的本地缓存。也就是说,把所有用户和api实体预先缓存为2个大查询。

// preload all api and user entities
_context.Apis.Load();
_context.Users.Load();
// batch add new statistics
foreach(new statistic in statisticsToAdd)
{
    statistic.User = _context.Users.Local.Single(x => x.Id == statistic.UserId);
    statistic.Api = _context.Api.Local.Single(x => x.Id == statistic.ApiId);
    _context.Statistics.Add(statistic);
}

有兴趣了解实体框架是否从其本地缓存中进行关系修复。例如,如果以下内容将在所有新统计数据上从本地缓存填充导航属性。待会儿有戏。

_context.ChangeTracker.DetectChanges();

免责声明:所有代码都是直接输入到浏览器中,所以要小心输入错误。

对不起,我没有时间测试,但EF映射实体到对象。因此不应该简单地给对象赋值:

public void Add(QueueItem queueItem)
{
    var entity = queueItem.ApiEntity;            

    var statistic = new Statistic
    {
        Ip = entity.Ip,
        Process = entity.ProcessId,
        //ApiId = entity.ApiId,
        Api = _context.Apis.Single(a => a.Id == entity.ApiId),
        Result = entity.Result,
        Error = entity.Error,
        Source = entity.Source,
        DateStamp = DateTime.UtcNow,
        //UserId = int.Parse(entity.ApiKey),
        User = _context.Users.Single(u => u.Id == int.Parse(entity.ApiKey)
    };
    _statisticRepository.Add(statistic);
    unitOfWork.Commit();
}    

我猜了一下你的名字,你应该在测试前调整

如何查找并只加载必要的列?

private readonly Dictionary<int, UserKeyType> _userKeyLookup = new Dictionary<int, UserKeyType>();

我不确定如何创建存储库,您可能需要在完成保存更改后或在事务开始时清理查找。

_userKeyLookup.Clean();

首先在查找中查找,如果没有找到则从上下文加载。

public Statistic Add(Statistic statistic)
{
    // _context.Statistic.Include(w => w.User).Load();
    UserKeyType key;
    if (_userKeyLookup.Contains(statistic.UserId))
    {
        key = _userKeyLookup[statistic.UserId];
    }
    else
    {
        key = _context.Users.Where(u => u.Id == statistic.UserId).Select(u => u.Key).FirstOrDefault();
        _userKeyLookup.Add(statistic.UserId, key);
    }
    statistic.User = new User { Id = statistic.UserId, Key = key };
    // similar code for api..
    // _context.Statistic.Include(p => p.Api).Load();
    _context.Statistic.Add(statistic);
    return statistic;
}

然后稍微改变一下分组。

var userCreditsGroup = addedStatistics
    .Where(w => w.User != null)
    .GroupBy(g => g.User.Id)
    .Select(s => new
    {
        User = s.Value.First().User,
        Count = s.Sum(p=>p.Api.CreditCost)
    })
    .ToList();