我有这个代码:
using (var context = new MyDbContext(connectionString))
{
context.Configuration.LazyLoadingEnabled = true;
context.Configuration.ProxyCreationEnabled = true;
context.Database.Log = logValue => File.AppendAllText(logFilePath, logValue);
var testItem1 = context.ParentTable
.FirstOrDefault(parent => parent.Id == 1)
.ChildEntities
.FirstOrDefault(child => child.ChildId == 2000);
}
在执行此代码并检查 EF 6 (logFilePath) 的日志文件时,我看到子实体加载了整个 ParentTable 记录,ID == 1,同时启用了 LazyLoad,并指定了子表的条件(子。子 ID == 2000)。
EF 不应该只加载相关的子项,还是先读取项目,然后在内存中数据上执行 FirstOrDefault ?
因为如果某个父级有许多子实体,这样,在加载有条件的子实体时,它会显着降低性能?
我想解决方法是单独加载子实体?
这是上述代码的完整日志文件(为了便于阅读,排除了一些行):
SELECT TOP (1)
....
FROM [dbo].[ParentTable] AS [Extent1]
WHERE 1 = [Extent1].[Id]
SELECT
...
FROM [dbo].[ChildTable] AS [Extent1]
WHERE [Extent1].[ParentId] = @EntityKeyValue1
-- EntityKeyValue1: '1' (Type = Int32, IsNullable = false)
注意:添加了相关类:
public class MyDbContext : DbContext
{
public DbSet<ParentTable> ParentTable { get; set; }
public DbSet<ChildTable> ChildTable { get; set; }
static MyDbContext()
{
Database.SetInitializer<MyDbContext>(null);
}
public MyDbContext(string connStr)
: base(connStr)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<ParentTable>()
.HasMany(t => t.ChildEntities);
}
}
[Table("ParentTable", Schema = "dbo")]
public class ParentTable
{
public int Id { get; set; }
public virtual ICollection<ChildTable> ChildEntities { get; set; }
}
[Table("ChildTable", Schema = "dbo")]
public class ChildTable
{
public int ChildId { get; set; }
public int ParentId { get; set; }
[ForeignKey("ParentId")]
public virtual ParentTable Parent { get; set; }
}
使用此查询:
var testItem1 = context.ChildTables
.Include(p=>p.ParentTable)
.Where(ch => ch.ChildId == 2000)
.FirstOrDefault();
您的问题与延迟加载无关。这是因为您在 LINQ 方法序列中使用FirstOrDefault
太早。
我将首先编写正确的查询,然后解释为什么该查询更好。
var result = dbContext.ParentTable
.Where(parent => parent.Id == 1)
.SelectMany(parent => parent.ChildEntities.Where(child => child.ChildId == 2000))
.FirstOrDefault();
如果仔细观察 LINQ 方法,您会发现有两种类型:返回IQueryable<...>
的方法和其他类型。第一组的 LINQ 方法是使用延迟执行,也称为延迟执行。这意味着这些语句不会执行查询。他们只会改变IQueryable的Expression
。尚未查询数据库。
来自后者的 LINQ 语句会在内部深入调用GetEnumerator()
并且大多数时候会反复调用MoveNext() / Current
。这会将IQueryable.Expression
发送给IQueryable.Provider
,将尝试将表达式转换为 SQL 并执行查询以从数据库中获取数据(准确地说:翻译并不总是必须是 SQL,这取决于提供者)。获取的数据显示为一个IEnumerator<...>
,您可以将其称为MoveNext() / Current
。
您的第一个FirstOrDefault
已经执行查询。除此之外,它执行得太早,并且可能会获取比您想要的更多的数据,您还可以遇到它返回的问题null
.
正确的方法是使用Select
.只有最后一条语句应该包含non_IQueryable方法,如FirstOrDefault
。
我使用了 SelectMany 而不是 Select,因为您只对父级的子实体感兴趣,而对任何父属性都不感兴趣。
var result = dbContext.ParentTable
.Where(parent => parent.Id == 1)
.SelectMany(parent => parent.ChildEntities.Where(child => child.ChildId == 2000))
.FirstOrDefault();
虽然这可以解决您的问题,但这将获取比您实际计划使用的数据更多的数据。例如,每个子项都有一个父项的外键。您知道父级的主键值等于 1,因此子项的外键的值也将为 1。为什么要转移它?
在这种情况下,我预计只有一个孩子,所以问题不会太大。但在其他情况下,您可能经常发送相同的值。
使用实体框架时,请始终使用"选择",并仅选择计划使用的属性。仅提取整行,或者在计划更新提取的项目时使用 Include。
如果您不使用 Select,会减慢流程的另一件事是,当您获取完整的行时,原始获取的数据及其副本会放在DbContext.ChangeTracker
中。这样做是为了可以检测在调用SaveChanges
时必须保存哪些值。如果您不打算更新提取的数据,请不要浪费处理能力将提取的数据放入更改跟踪器中。