如何使用 EF6 获取大量记录
How to Fetch a Lot of Records with EF6
我需要使用 EF6 从 SQL 服务器数据库中获取大量记录。它需要很多时间的问题。主要问题是名为 Series
的实体,其中包含 Measurements
。它们大约有 250K 个,每个都有 2 个嵌套实体,分别称为 FrontDropPhoto
和 SideDropPhoto
.
[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; }
}
我写过这样的获取方法(为清楚起见省略了大部分属性):
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;
对于每个上下文,但加载时间仍然很长。如何解决这个问题?
除了您打算 returning 的大量数据之外,主要问题是您的代码结构方式意味着对于 250,000 Series
执行另一次数据库访问以获得 Series
的 Measurements
,并进一步执行 2 次访问以获取每个 Measurement
的 front/side DropPhotos
。除了 750,000 次调用的往返时间之外,这完全避免了利用 SQL 基于集合的性能优化。
尽量确保 EF 向 return 您的数据提交尽可能少的查询,最好是一个:
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();
首先,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 的任何代码都应该接收完整的或完整的 table 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;
}
}
假设我们想要提取行的图像数据(如果可用)。利用查询中相关数据的导航属性,而不是遍历结果并为每一行返回数据库。
对于图表,您可以 return 原始整数数据或仅用于所需字段的数据结构,而不是依赖于为网格 return 编辑的数据。它可以在没有“大量”图像数据的情况下为整个 table 拉取。当数据可能已经加载一次时转到数据库似乎适得其反,但结果是两个高效的查询,而不是一个试图服务于两个目的的非常低效的查询。
您为什么要重新发明轮子并手动加载和构建您的相关实体?你导致 N+1 selects problem 导致令人厌恶的表现。让 EF 通过 .Include
高效查询相关实体
示例:
var results = context.Series
.AsNoTracking()
.Include( s => s.MeasurementSeries )
.ThenInclude( ms => ms.FrontDropPhoto )
.Where( ... )
.ToList(); // should use async
这将大大加快执行速度,但如果它需要构建数十万到数百万个对象,它可能仍然不能满足您的要求,在这种情况下,您可以在并发批次中检索数据。
我需要使用 EF6 从 SQL 服务器数据库中获取大量记录。它需要很多时间的问题。主要问题是名为 Series
的实体,其中包含 Measurements
。它们大约有 250K 个,每个都有 2 个嵌套实体,分别称为 FrontDropPhoto
和 SideDropPhoto
.
[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; }
}
我写过这样的获取方法(为清楚起见省略了大部分属性):
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;
对于每个上下文,但加载时间仍然很长。如何解决这个问题?
除了您打算 returning 的大量数据之外,主要问题是您的代码结构方式意味着对于 250,000 Series
执行另一次数据库访问以获得 Series
的 Measurements
,并进一步执行 2 次访问以获取每个 Measurement
的 front/side DropPhotos
。除了 750,000 次调用的往返时间之外,这完全避免了利用 SQL 基于集合的性能优化。
尽量确保 EF 向 return 您的数据提交尽可能少的查询,最好是一个:
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();
首先,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 的任何代码都应该接收完整的或完整的 table 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;
}
}
假设我们想要提取行的图像数据(如果可用)。利用查询中相关数据的导航属性,而不是遍历结果并为每一行返回数据库。
对于图表,您可以 return 原始整数数据或仅用于所需字段的数据结构,而不是依赖于为网格 return 编辑的数据。它可以在没有“大量”图像数据的情况下为整个 table 拉取。当数据可能已经加载一次时转到数据库似乎适得其反,但结果是两个高效的查询,而不是一个试图服务于两个目的的非常低效的查询。
您为什么要重新发明轮子并手动加载和构建您的相关实体?你导致 N+1 selects problem 导致令人厌恶的表现。让 EF 通过 .Include
示例:
var results = context.Series
.AsNoTracking()
.Include( s => s.MeasurementSeries )
.ThenInclude( ms => ms.FrontDropPhoto )
.Where( ... )
.ToList(); // should use async
这将大大加快执行速度,但如果它需要构建数十万到数百万个对象,它可能仍然不能满足您的要求,在这种情况下,您可以在并发批次中检索数据。