EF 加载所有子项,即使使用"首选位置"或"默认位置"



我有这个代码:

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时必须保存哪些值。如果您不打算更新提取的数据,请不要浪费处理能力将提取的数据放入更改跟踪器中。

相关内容

  • 没有找到相关文章

最新更新