【问题标题】:How to Fetch a Lot of Records with EF6如何使用 EF6 获取大量记录
【发布时间】:2021-04-26 21:54:32
【问题描述】:

我需要使用 EF6 从 SQL Server 数据库中获取大量记录。它需要很多时间的问题。主要问题是名为Series 的实体,其中包含Measurements。它们大约有 250K,每个都有 2 个嵌套实体,称为 FrontDropPhotoSideDropPhoto

[Table("Series")]
public class DbSeries
{
    [Key] public Guid SeriesId { get; set; }
    public List<DbMeasurement> MeasurementsSeries { get; set; }
}

[Table("Measurements")]
public class DbMeasurement
{
    [Key] public Guid MeasurementId { get; set; }

    public Guid CurrentSeriesId { get; set; }
    public DbSeries CurrentSeries { get; set; }

    public Guid? SideDropPhotoId { get; set; }
    [ForeignKey("SideDropPhotoId")]
    public virtual DbDropPhoto SideDropPhoto { get; set; }

    public Guid? FrontDropPhotoId { get; set; }
    [ForeignKey("FrontDropPhotoId")]
    public virtual DbDropPhoto FrontDropPhoto { get; set; }
}

[Table("DropPhotos")]
public class DbDropPhoto
{
    [Key] public Guid PhotoId { get; set; }
}

我已经写了这样的 fetch 方法(为了清楚起见,省略了大部分属性):

public async Task<List<DbSeries>> GetSeriesByUserId(Guid dbUserId)
{
        using (var context = new DDropContext())
        {
            try
            {
                var loadedSeries = await context.Series
                    .Where(x => x.CurrentUserId == dbUserId)
                    .Select(x => new
                    {
                        x.SeriesId,
                    }).ToListAsync();

                var dbSeries = new List<DbSeries>();

                foreach (var series in loadedSeries)
                {
                    var seriesToAdd = new DbSeries
                    {
                        SeriesId = series.SeriesId,
                    };

                    seriesToAdd.MeasurementsSeries = await GetMeasurements(seriesToAdd);

                    dbSeries.Add(seriesToAdd);
                }

                return dbSeries;
            }
            catch (SqlException e)
            {
                throw new TimeoutException(e.Message, e);
            }
        }
}

public async Task<List<DbMeasurement>> GetMeasurements(DbSeries series)
{
        using (var context = new DDropContext())
        {
            var measurementForSeries = await context.Measurements.Where(x => x.CurrentSeriesId == series.SeriesId)
                .Select(x => new
                {
                    x.CurrentSeries,
                    x.CurrentSeriesId,
                    x.MeasurementId,
                })
                .ToListAsync();

            var dbMeasurementsForAdd = new List<DbMeasurement>();

            foreach (var measurement in measurementForSeries)
            {
                var measurementToAdd = new DbMeasurement
                {
                    CurrentSeries = series,
                    MeasurementId = measurement.MeasurementId,
                    FrontDropPhotoId = measurement.FrontDropPhotoId,
                    FrontDropPhoto = measurement.FrontDropPhotoId.HasValue
                        ? await GetDbDropPhotoById(measurement.FrontDropPhotoId.Value)
                        : null,
                    SideDropPhotoId = measurement.SideDropPhotoId,
                    SideDropPhoto = measurement.SideDropPhotoId.HasValue
                        ? await GetDbDropPhotoById(measurement.SideDropPhotoId.Value)
                        : null,
                };

                dbMeasurementsForAdd.Add(measurementToAdd);
            }

            return dbMeasurementsForAdd;
        }
}

private async Task<DbDropPhoto> GetDbDropPhotoById(Guid photoId)
{
        using (var context = new DDropContext())
        {
            var dropPhoto = await context.DropPhotos
                .Where(x => x.PhotoId == photoId)
                .Select(x => new
                {
                    x.PhotoId,
                }).FirstOrDefaultAsync();

            if (dropPhoto == null)
            {
                return null;
            }

            var dbDropPhoto = new DbDropPhoto
            {
                PhotoId = dropPhoto.PhotoId,
            };

            return dbDropPhoto;
        }
}

通过 FluentAPI 配置的关系:

        modelBuilder.Entity<DbSeries>()
            .HasMany(s => s.MeasurementsSeries)
            .WithRequired(g => g.CurrentSeries)
            .HasForeignKey(s => s.CurrentSeriesId)
            .WillCascadeOnDelete();

        modelBuilder.Entity<DbMeasurement>()
            .HasOptional(c => c.FrontDropPhoto)
            .WithMany()
            .HasForeignKey(s => s.FrontDropPhotoId);

        modelBuilder.Entity<DbMeasurement>()
            .HasOptional(c => c.SideDropPhoto)
            .WithMany()
            .HasForeignKey(s => s.SideDropPhotoId);

我需要所有这些数据来填充 WPF DataGrid。显而易见的解决方案是向此 DataGrid 添加分页。这个解决方案很诱人,但它会严重破坏我的应用程序的逻辑。我想在运行时使用这些数据创建图,所以我需要所有这些,而不仅仅是某些部分。我试图通过使每种方法都使用异步等待来对其进行一些优化,但这并没有足够的帮助。我已经尝试添加

.Configuration.AutoDetectChangesEnabled = false;

对于每个上下文,但加载时间仍然很长。如何解决这个问题?

【问题讨论】:

  • 对代码的表面级修改不会神奇地让它变得更快。但是,您可能会幸运地将其转换为原始 SQL。
  • 如果您并不真正需要 EF 围绕该任务的记录构建的所有基础架构,也许您应该将旧的 bare metal 方法与 SP 和经典 ADO.NET 一起使用.或者你可以尝试Raw Sql Feature 如果没有
  • 是将它加载到 DataGrid 还是从数据库中检索它所花费的时间?如果是前者,那么分页可能是您可以做出的唯一真正的改进(假设虚拟化正在工作)。如果是后者,您可以尝试自己编写 SQL 查询并像这样手动填充您的类。
  • (1) 设置 dbContext Database.Log 属性并检查 EF 生成的 SQL。 (2) 确保所有外键都有索引。

标签: c# entity-framework ef-fluent-api


【解决方案1】:

除了您打算返回的大量数据之外,主要问题是您的代码结构方式意味着,对于 250,000 个Series 中的每一个,您都需要再次访问数据库以获取Measurements 用于Series 和另外 2 次行程以获得每个 Measurement 的正面/侧面DropPhotos。除了 750,000 次调用的往返时间之外,这完全避免了利用 SQL 的基于集合的性能优化。

尽量确保 EF 提交尽可能少的查询来返回您的数据,最好是一个:

var loadedSeries = await context.Series
                .Where(x => x.CurrentUserId == dbUserId)
                .Select(x => new DbSeries
                {
                    SeriesId = x.SeriesId,
                    MeasurementsSeries = x.MeasurementsSeries.Select(ms => new DbMeasurement 
                    {
                         MeasurementId = ms.MeasurementId,
                         FrontDropPhotoId = ms.FrontDropPhotoId,
                         FrontDropPhoto = new DbDropPhoto
                         {
                             PhotoId = ms.FrontDropPhotoId
                          },
                         SideDropPhotoId = ms.SideDropPhotoId,
                         SideDropPhoto = new DbDropPhoto
                         {
                             PhotoId = ms.SideDropPhotoId
                          },
                    })
                }).ToListAsync();

【讨论】:

    【解决方案2】:

    首先,async/await 在这里帮不了你。这不是“更快”类型的操作,它是关于容纳“可以在此操作进行计算时做其他事情”的系统。如果有的话,它会使操作变慢,以换取系统的响应速度更快。

    我的建议是将您的顾虑分开:一方面您希望显示详细数据。另一方面,您想绘制一个整体图。把这些分开。用户不需要一次查看每条记录的详细信息,在服务器端对其进行分页将大大减少任何时候的原始数据量。图表想要查看所有数据,但它们并不关心位图等“繁重”的细节。

    接下来的事情是将视图模型与域模型(实体)分开。做这样的事情:

    var measurementToAdd = new DbMeasurement
    {
        CurrentSeries = series,
        MeasurementId = measurement.MeasurementId,
        FrontDropPhotoId = measurement.FrontDropPhotoId,
        FrontDropPhoto = measurement.FrontDropPhotoId.HasValue
            ? await GetDbDropPhotoById(measurement.FrontDropPhotoId.Value)
            : null,
        SideDropPhotoId = measurement.SideDropPhotoId,
        SideDropPhoto = measurement.SideDropPhotoId.HasValue
            ? await GetDbDropPhotoById(measurement.SideDropPhotoId.Value)
            : null,
    };
    

    ... 只是自找麻烦。任何接受 DbMeasurement 的代码都应该接收完整的或可完成的 DbMeasurement,而不是部分填充的实体。将来它烧死你。为数据网格定义一个视图模型并填充它。这样您就可以清楚地区分什么是实体模型和什么是视图模型。

    接下来,对于数据网格,绝对实现服务端分页:

    public ICollection<MeasurementViewModel> GetMeasurements(int seriesId, int pageNumber, int pageSize)
    {
        using (var context = new DDropContext())
        {
            var measurementsForSeries = await context.Measurements
               .Where(x => x.CurrentSeriesId == seriesId)
               .Select(x => new MeasurementViewModel
                {
                    MeasurementId = x.MeasurementId,
                    FromDropPhoto = x.FromDropPhoto.ImageData,
                    SideDropPhoto = x.SideDropPhoto.ImageData
                })
                .Skip(pageNumber*pageSize)
                .Take(pageSize)
                .ToList();
    
            return measurementsForSeries;
        }
    }
    

    这假设我们想要提取行的图像数据(如果可用)。在查询中利用相关数据的导航属性,而不是遍历结果并为每一行返回数据库。

    对于图表,您可以返回原始整数数据或仅针对所需字段的数据结构,而不是依赖于为网格返回的数据。可以为整个表拉取它,而无需“沉重”的图像数据。当数据可能已经加载过一次时,访问数据库可能会适得其反,但结果是两个高效查询,而不是一个非常低效的查询试图服务于两个目的。

    【讨论】:

      【解决方案3】:

      您为什么要重新发明轮子并手动加载和构建相关实体?您正在导致N+1 selects problem 导致可恶的性能。让EF通过.Include高效查询相关实体

      例子:

      var results = context.Series
          .AsNoTracking()
          .Include( s => s.MeasurementSeries )
              .ThenInclude( ms => ms.FrontDropPhoto )
          .Where( ... )
          .ToList(); // should use async 
      

      这将显着加快执行速度,但如果它需要构建数十万到数百万个对象,它可能仍然不足以满足您的要求,在这种情况下,您可以同时批量检索数据。

      【讨论】:

        猜你喜欢
        • 2015-03-02
        • 2018-07-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多